Rename project to UploadShield: update runtime, configs, docs, and provisioning; run lint/tests

This commit is contained in:
2026-02-12 14:48:07 +01:00
parent d1310e0844
commit 7c8bccc911
20 changed files with 95 additions and 104 deletions

2
.gitignore vendored
View File

@@ -8,7 +8,7 @@
uploads.log uploads.log
# Peek allow marker (local only) # Peek allow marker (local only)
/.upload_logger_allow_peek /.uploadshield_allow_peek
# Local environment files # Local environment files
.env .env

View File

@@ -1,6 +1,6 @@
# Configuration Reference # Configuration Reference
This file maps the top-level configuration keys used by `upload-logger.json` (UploadShield) to their effect and defaults. Use absolute paths in production where possible. This file maps the top-level configuration keys used by `uploadshield.json` (UploadShield) to their effect and defaults. Use absolute paths in production where possible.
## Top-level sections ## Top-level sections
@@ -48,7 +48,7 @@ This file maps the top-level configuration keys used by `upload-logger.json` (Up
- `allow_xml_eval` (bool) — relax `eval()` detection for XML/SVG content when true. - `allow_xml_eval` (bool) — relax `eval()` detection for XML/SVG content when true.
- `custom_patterns` (array) — array of PCRE patterns (as strings) applied to the file head. Invalid patterns are ignored. - `custom_patterns` (array) — array of PCRE patterns (as strings) applied to the file head. Invalid patterns are ignored.
## Example `upload-logger.json` ## Example `uploadshield.json`
```json ```json
{ {
@@ -67,6 +67,6 @@ This file maps the top-level configuration keys used by `upload-logger.json` (Up
- Use absolute paths in `paths.*` when deploying under systemd/Ansible to avoid cwd surprises. - Use absolute paths in `paths.*` when deploying under systemd/Ansible to avoid cwd surprises.
- Ensure `quarantine_dir` is inaccessible from the web and set to owner `root` and mode `0700`; files inside should be `0600`. - Ensure `quarantine_dir` is inaccessible from the web and set to owner `root` and mode `0700`; files inside should be `0600`.
If you want, I can generate a per-site `upload-logger.json` filled with your preferred absolute paths and ownership values. If you want, I can generate a per-site `uploadshield.json` filled with your preferred absolute paths and ownership values.
-- --

View File

@@ -51,7 +51,7 @@ Add CONFIG_REFERENCE.md mapping configuration options
- A basic smoke harness exists under `tests/smoke/`. To run locally: - A basic smoke harness exists under `tests/smoke/`. To run locally:
```bash ```bash
php -S 127.0.0.1:8000 -t tests/smoke/public -d auto_prepend_file=$(pwd)/upload-logger.php php -S 127.0.0.1:8000 -t tests/smoke/public -d auto_prepend_file=$(pwd)/uploadshield.php
# then POST files with curl or a test client # then POST files with curl or a test client
``` ```

View File

@@ -2,7 +2,7 @@
This document complements the installation steps in [docs/INSTALLATION.md](docs/INSTALLATION.md) by focusing on detector tuning, allowlists, and advanced integrations (log forwarding, Fail2Ban, etc.). This document complements the installation steps in [docs/INSTALLATION.md](docs/INSTALLATION.md) by focusing on detector tuning, allowlists, and advanced integrations (log forwarding, Fail2Ban, etc.).
Example `upload-logger.json` (simplified) for UploadShield: Example `uploadshield.json` (simplified) for UploadShield:
```json ```json
{ {
@@ -50,7 +50,7 @@ Notes:
### Content detector tuning ### Content detector tuning
- The `ContentDetector` performs a fast head-scan to detect PHP open-tags and common webshell indicators (e.g., `passthru`, `system`, `exec`, `base64_decode`, `eval`, `assert`). - The `ContentDetector` performs a fast head-scan to detect PHP open-tags and common webshell indicators (e.g., `passthru`, `system`, `exec`, `base64_decode`, `eval`, `assert`).
- Tuning options (in `upload-logger.json`): - Tuning options (in `uploadshield.json`):
- `limits.sniff_max_bytes` (default 8192) — how many bytes to scan from the file head. - `limits.sniff_max_bytes` (default 8192) — how many bytes to scan from the file head.
- `limits.sniff_max_filesize` (default 2097152) — only scan files up to this size. - `limits.sniff_max_filesize` (default 2097152) — only scan files up to this size.
- `detectors.content.allow_xml_eval` — relax `eval()` detection for XML/SVG when appropriate. - `detectors.content.allow_xml_eval` — relax `eval()` detection for XML/SVG when appropriate.
@@ -110,14 +110,14 @@ filebeat.inputs:
json.keys_under_root: true json.keys_under_root: true
json.add_error_key: true json.add_error_key: true
fields: fields:
source: php-upload-logger source: php-uploadshield
output.logstash: output.logstash:
hosts: ["logserver:5044"] hosts: ["logserver:5044"]
``` ```
## Logrotate & SELinux notes ## Logrotate & SELinux notes
Per-site `logrotate` snippets are included in `examples/logrotate.d/upload-logger`. Use `copytruncate` or reload PHP-FPM after rotation. Per-site `logrotate` snippets are included in `examples/logrotate.d/uploadshield`. Use `copytruncate` or reload PHP-FPM after rotation.
If SELinux is enabled, the provisioning script attempts to register fcontexts and run `restorecon`. Verify contexts manually as needed. If SELinux is enabled, the provisioning script attempts to register fcontexts and run `restorecon`. Verify contexts manually as needed.
@@ -125,6 +125,6 @@ If SELinux is enabled, the provisioning script attempts to register fcontexts an
- Use observe mode (`ops.block_suspicious: false`) while tuning. - Use observe mode (`ops.block_suspicious: false`) while tuning.
- After tuning, enable blocking in a controlled rollout (canary hosts first). - After tuning, enable blocking in a controlled rollout (canary hosts first).
- Keep `upload-logger.php` and `.security` owned by `root` and ensure logs and quarantine are not web-accessible. - Keep `uploadshield.php` and `.security` owned by `root` and ensure logs and quarantine are not web-accessible.
For installation steps and per-site configuration, see `docs/INSTALLATION.md`. For installation steps and per-site configuration, see `docs/INSTALLATION.md`.

View File

@@ -2,13 +2,13 @@
This repository contains UploadShield (formerly "Upload Logger"): a hardened PHP upload protection helper. It provides a single-file monitor that logs uploads, detects common evasion techniques, quarantines suspicious files, and can optionally block malicious uploads. This repository contains UploadShield (formerly "Upload Logger"): a hardened PHP upload protection helper. It provides a single-file monitor that logs uploads, detects common evasion techniques, quarantines suspicious files, and can optionally block malicious uploads.
Primary file: [upload-logger.php](upload-logger.php) Primary file: [uploadshield.php](uploadshield.php)
Summary Summary
- Purpose: Log normal uploads and raw-body uploads, detect double-extension tricks, fake images, PHP payloads embedded in files, and provide flood detection. - Purpose: Log normal uploads and raw-body uploads, detect double-extension tricks, fake images, PHP payloads embedded in files, and provide flood detection.
- Runs only for HTTP requests; recommended to enable via `auto_prepend_file` in a per-site PHP-FPM pool for broad coverage. - Runs only for HTTP requests; recommended to enable via `auto_prepend_file` in a per-site PHP-FPM pool for broad coverage.
Key configuration (top of `upload-logger.php`) Key configuration (top of `uploadshield.php`)
- `$logFile` — path to the log file (default: `__DIR__ . '/logs/uploads.log'`). - `$logFile` — path to the log file (default: `__DIR__ . '/logs/uploads.log'`).
- `$BLOCK_SUSPICIOUS` — if `true` the script returns `403` and exits when suspicious uploads are detected. - `$BLOCK_SUSPICIOUS` — if `true` the script returns `403` and exits when suspicious uploads are detected.
- `$MAX_SIZE` — threshold for `WARN big_upload` (default 50 MB). - `$MAX_SIZE` — threshold for `WARN big_upload` (default 50 MB).
@@ -31,7 +31,7 @@ Logging and alerts
- Other notes: `WARN big_upload`, `NOTE archive_upload`, `MULTIPART_NO_FILES`, and `RAW_BODY` are emitted when appropriate. - Other notes: `WARN big_upload`, `NOTE archive_upload`, `MULTIPART_NO_FILES`, and `RAW_BODY` are emitted when appropriate.
Integration notes Integration notes
- Preferred deployment: set `php_admin_value[auto_prepend_file]` in the site-specific PHP-FPM pool to the absolute path of `upload-logger.php` so it runs before application code. - Preferred deployment: set `php_admin_value[auto_prepend_file]` in the site-specific PHP-FPM pool to the absolute path of `uploadshield.php` so it runs before application code.
- If using sessions for user identification, the script safely reads `$_SESSION['user_id']` only when a session is active; do not rely on it being present unless your app starts sessions earlier. - If using sessions for user identification, the script safely reads `$_SESSION['user_id']` only when a session is active; do not rely on it being present unless your app starts sessions earlier.
- The script uses `is_uploaded_file()`/`finfo` where available; ensure the PHP `fileinfo` extension is enabled for best MIME detection. - The script uses `is_uploaded_file()`/`finfo` where available; ensure the PHP `fileinfo` extension is enabled for best MIME detection.
- The script uses `is_uploaded_file()`/`finfo` where available; ensure the PHP `fileinfo` extension is enabled for best MIME detection. - The script uses `is_uploaded_file()`/`finfo` where available; ensure the PHP `fileinfo` extension is enabled for best MIME detection.
@@ -39,16 +39,16 @@ Integration notes
Content detector & tuning Content detector & tuning
- `ContentDetector` is now included and performs a fast head-scan of uploaded files to detect PHP open-tags and common webshell indicators (e.g., `passthru()`, `system()`, `exec()`, `shell_exec()`, `proc_open()`, `popen()`, `base64_decode()`, `eval()`, `assert()`). - `ContentDetector` is now included and performs a fast head-scan of uploaded files to detect PHP open-tags and common webshell indicators (e.g., `passthru()`, `system()`, `exec()`, `shell_exec()`, `proc_open()`, `popen()`, `base64_decode()`, `eval()`, `assert()`).
- The detector only scans the first N bytes of a file to limit CPU/io work; tune these limits in `upload-logger.json`: - The detector only scans the first N bytes of a file to limit CPU/io work; tune these limits in `uploadshield.json`:
- `limits.sniff_max_bytes` — number of bytes to scan from file head (default `8192`). - `limits.sniff_max_bytes` — number of bytes to scan from file head (default `8192`).
- `limits.sniff_max_filesize` — only scan files up to this size in bytes (default `2097152` / 2MB). - `limits.sniff_max_filesize` — only scan files up to this size in bytes (default `2097152` / 2MB).
- Behavior note: `eval()` and similar tokens commonly appear inside SVG/JS contexts. The detector uses the detected MIME to be more permissive for XML/SVG-like content, but you should test and tune for your application's upload patterns to avoid false positives (see `INTEGRATION.md`). - Behavior note: `eval()` and similar tokens commonly appear inside SVG/JS contexts. The detector uses the detected MIME to be more permissive for XML/SVG-like content, but you should test and tune for your application's upload patterns to avoid false positives (see `INTEGRATION.md`).
- If your application legitimately accepts encoded or templated payloads, add application-specific allowlist rules (URI or content-type) in `allowlist.json` or extend `upload-logger.json` with detector-specific tuning before enabling blocking mode. If your application legitimately accepts encoded or templated payloads, add application-specific allowlist rules (URI or content-type) in `allowlist.json` or extend `uploadshield.json` with detector-specific tuning before enabling blocking mode.
Further integration Further integration
- Read the `INTEGRATION.md` for detector tuning, allowlists, and examples for log forwarding and Fail2Ban. - Read the `INTEGRATION.md` for detector tuning, allowlists, and examples for log forwarding and Fail2Ban.
- See `docs/INSTALLATION.md` for a step-by-step per-site install and `auto_prepend_file` examples. - See `docs/INSTALLATION.md` for a step-by-step per-site install and `auto_prepend_file` examples.
- Provision the required directories (`quarantine`, `state`) and set ownership/SELinux via the included provisioning script: `scripts/provision_dirs.sh`. - Provision the required directories (`quarantine`, `state`) and set ownership/SELinux via the included provisioning script: `scripts/provision_dirs.sh`.
- Example automation: `scripts/ansible/upload-logger-provision.yml` and `scripts/systemd/upload-logger-provision.service` are included as examples to run provisioning at deploy-time or boot. - Example automation: `scripts/ansible/uploadshield-provision.yml` and `scripts/systemd/uploadshield-provision.service` are included as examples to run provisioning at deploy-time or boot.
Operational recommendations Operational recommendations
- Place the `logs/` directory outside the webroot or deny web access to it. - Place the `logs/` directory outside the webroot or deny web access to it.
@@ -61,16 +61,16 @@ Limitations & safety
- Content sniffing is limited to a head-scan to reduce CPU and false positives; tune `$SNIFF_MAX_BYTES` and `$SNIFF_MAX_FILESIZE` to balance coverage and performance. - Content sniffing is limited to a head-scan to reduce CPU and false positives; tune `$SNIFF_MAX_BYTES` and `$SNIFF_MAX_FILESIZE` to balance coverage and performance.
Quick start Quick start
1. Place `upload-logger.php` in a per-site secure folder (see `INTEGRATION.md`). 1. Place `uploadshield.php` in a per-site secure folder (see `INTEGRATION.md`).
2. Ensure the `logs/` directory exists and is writable by PHP-FPM. 2. Ensure the `logs/` directory exists and is writable by PHP-FPM.
3. Enable as an `auto_prepend_file` in the site pool and reload PHP-FPM. 3. Enable as an `auto_prepend_file` in the site pool and reload PHP-FPM.
4. Monitor `logs/uploads.log` and adjust configuration options at the top of the script. 4. Monitor `logs/uploads.log` and adjust configuration options at the top of the script.
Support & changes Support & changes
- For changes, edit configuration variables at the top of `upload-logger.php` or adapt detection helpers as needed. - For changes, edit configuration variables at the top of `uploadshield.php` or adapt detection helpers as needed.
--- ---
Generated for upload-logger.php (UploadShield v3). Generated for uploadshield.php (UploadShield v3).
## Additional documentation ## Additional documentation

View File

@@ -7,9 +7,9 @@
"archive_inspect": true "archive_inspect": true
}, },
"paths": { "paths": {
"quarantine_dir": "/var/lib/upload-logger/quarantine", "quarantine_dir": "/var/lib/uploadshield/quarantine",
"state_dir": "/var/lib/upload-logger/state", "state_dir": "/var/lib/uploadshield/state",
"allowlist_file": "/etc/upload-logger/allowlist.json" "allowlist_file": "/etc/uploadshield/allowlist.json"
}, },
"limits": { "limits": {
"max_size": 52428800, "max_size": 52428800,

View File

@@ -7,9 +7,9 @@
"archive_inspect": true "archive_inspect": true
}, },
"paths": { "paths": {
"quarantine_dir": "/var/lib/upload-logger/quarantine", "quarantine_dir": "/var/lib/uploadshield/quarantine",
"state_dir": "/var/lib/upload-logger/state", "state_dir": "/var/lib/uploadshield/state",
"allowlist_file": "/etc/upload-logger/allowlist.json" "allowlist_file": "/etc/uploadshield/allowlist.json"
}, },
"limits": { "limits": {
"max_size": 52428800, "max_size": 52428800,

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace UploadLogger\Core; namespace UploadLogger\Core;
/** /**
* Simple immutable configuration holder for UploadShield (upload-logger.json). * Simple immutable configuration holder for UploadShield (uploadshield.json).
*/ */
final class Config final class Config
{ {

View File

@@ -1,6 +1,6 @@
# Installation & Production Deployment Guide # Installation & Production Deployment Guide
This guide shows a minimal, secure installation and rollout path for UploadShield's primary script (`upload-logger.php`). This guide shows a minimal, secure installation and rollout path for UploadShield's primary script (`uploadshield.php`).
Follow these steps in a staging environment first; do not enable blocking until detectors are tuned. Follow these steps in a staging environment first; do not enable blocking until detectors are tuned.
**Prerequisites** **Prerequisites**
@@ -9,9 +9,9 @@ Follow these steps in a staging environment first; do not enable blocking until
- SSH/privileged access to configure the site pool and run provisioning scripts. - SSH/privileged access to configure the site pool and run provisioning scripts.
**Quick overview** **Quick overview**
1. Place `upload-logger.php` in a secure per-site folder (recommended `.security`). 1. Place `uploadshield.php` in a secure per-site folder (recommended `.security`).
2. Create `logs/`, `quarantine/`, `state/` and set strict ownership and permissions. 2. Create `logs/`, `quarantine/`, `state/` and set strict ownership and permissions.
3. Configure `upload-logger.json` for your environment; keep `ops.block_suspicious` off for initial tuning. 3. Configure `uploadshield.json` for your environment; keep `ops.block_suspicious` off for initial tuning.
4. Enable `auto_prepend_file` in the site PHP-FPM pool to run the logger before application code. 4. Enable `auto_prepend_file` in the site PHP-FPM pool to run the logger before application code.
5. Verify logging, tune detectors, deploy log rotation, and enable alerting. 5. Verify logging, tune detectors, deploy log rotation, and enable alerting.
@@ -31,7 +31,7 @@ vendor/bin/phpstan analyse -c phpstan.neon
``` ```
**2. Recommended file layout (per-site)** **2. Recommended file layout (per-site)**
Use a hidden per-site security folder so `upload-logger.php` is not web-accessible. Use a hidden per-site security folder so `uploadshield.php` is not web-accessible.
Example layout: Example layout:
@@ -41,16 +41,16 @@ Example layout:
├── app/ ├── app/
├── uploads/ ├── uploads/
├── .security/ ├── .security/
│ ├── upload-logger.php │ ├── uploadshield.php
│ └── logs/ │ └── logs/
│ └── uploads.log │ └── uploads.log
├── quarantine/ ├── quarantine/
└── state/ └── state/
``` Place `uploadshield.php` into `.security/uploadshield.php`.
**3. Copy files & configure** **3. Copy files & configure**
- Place `upload-logger.php` into `.security/upload-logger.php`. - Place `uploadshield.php` into `.security/uploadshield.php`.
- Copy `upload-logger.json` from the repository to the same directory and edit paths to absolute values, e.g.: - Copy `uploadshield.json` from the repository to the same directory and edit paths to absolute values, e.g.:
- `paths.log_file` → `/var/www/sites/example-site/.security/logs/uploads.log` - `paths.log_file` → `/var/www/sites/example-site/.security/logs/uploads.log`
- `paths.quarantine_dir` → `/var/www/sites/example-site/quarantine` - `paths.quarantine_dir` → `/var/www/sites/example-site/quarantine`
- `paths.state_dir` → `/var/www/sites/example-site/state` - `paths.state_dir` → `/var/www/sites/example-site/state`
@@ -81,14 +81,14 @@ chmod 0640 /var/www/sites/example-site/.security/logs/uploads.log
Alternatively use the included provisioning scripts on the host: Alternatively use the included provisioning scripts on the host:
- `scripts/provision_dirs.sh` — run as root; idempotent and attempts to set SELinux fcontext. - `scripts/provision_dirs.sh` — run as root; idempotent and attempts to set SELinux fcontext.
- `scripts/ansible/upload-logger-provision.yml` — Ansible playbook for bulk provisioning. - `scripts/ansible/uploadshield-provision.yml` — Ansible playbook for bulk provisioning.
**5. PHPFPM configuration (per-site pool)** **5. PHPFPM configuration (per-site pool)**
Edit the site's FPM pool (example: `/etc/php/8.1/fpm/pool.d/example-site.conf`) and add: Edit the site's FPM pool (example: `/etc/php/8.1/fpm/pool.d/example-site.conf`) and add:
php_admin_value[auto_prepend_file] = /var/www/sites/example-site/.security/uploadshield.php
```ini ```ini
; Ensure upload-logger runs before application code ; Ensure uploadshield runs before application code
php_admin_value[auto_prepend_file] = /var/www/sites/example-site/.security/upload-logger.php php_admin_value[auto_prepend_file] = /var/www/sites/example-site/.security/uploadshield.php
``` ```
Then reload PHP-FPM: Then reload PHP-FPM:
@@ -98,11 +98,11 @@ sudo systemctl reload php8.1-fpm
``` ```
Notes: Notes:
- Use the absolute path to `upload-logger.php`. - Use the absolute path to `uploadshield.php`.
- If your host uses a shared PHP-FPM pool, consider enabling per-vhost `auto_prepend_file` via nginx/apache or create a dedicated pool for the site. - If your host uses a shared PHP-FPM pool, consider enabling per-vhost `auto_prepend_file` via nginx/apache or create a dedicated pool for the site.
**6. Log rotation** **6. Log rotation**
Create `/etc/logrotate.d/upload-logger` with content adapted to your paths: Create `/etc/logrotate.d/uploadshield` with content adapted to your paths:
``` ```
/var/www/sites/example-site/.security/logs/uploads.log { /var/www/sites/example-site/.security/logs/uploads.log {
@@ -129,10 +129,10 @@ Create `/etc/logrotate.d/upload-logger` with content adapted to your paths:
- Upload a file containing `<?php` to confirm detectors log `suspicious_upload` entries. - Upload a file containing `<?php` to confirm detectors log `suspicious_upload` entries.
- Check `logs/uploads.log` for events. - Check `logs/uploads.log` for events.
Example quick test (on host): php -S 127.0.0.1:8000 -t . -d auto_prepend_file=/var/www/sites/example-site/.security/uploadshield.php
```bash ```bash
# from site root # from site root
php -S 127.0.0.1:8000 -t . -d auto_prepend_file=/var/www/sites/example-site/.security/upload-logger.php php -S 127.0.0.1:8000 -t . -d auto_prepend_file=/var/www/sites/example-site/.security/uploadshield.php
# then curl a file POST to your test endpoint # then curl a file POST to your test endpoint
curl -F "file=@/path/to/sample.txt" http://127.0.0.1:8000/upload_test.php curl -F "file=@/path/to/sample.txt" http://127.0.0.1:8000/upload_test.php
``` ```
@@ -146,7 +146,7 @@ curl -F "file=@/path/to/sample.txt" http://127.0.0.1:8000/upload_test.php
**9. Gradual enable blocking** **9. Gradual enable blocking**
- After tuning, enable blocking in a controlled manner: - After tuning, enable blocking in a controlled manner:
1. Set `ops.block_suspicious``true` in `upload-logger.json` on a small subset of sites or a canary host. 1. Set `ops.block_suspicious``true` in `uploadshield.json` on a small subset of sites or a canary host.
2. Monitor errors, rollback quickly if issues appear. 2. Monitor errors, rollback quickly if issues appear.
3. Gradually roll out to remaining hosts. 3. Gradually roll out to remaining hosts.
@@ -159,17 +159,17 @@ curl -F "file=@/path/to/sample.txt" http://127.0.0.1:8000/upload_test.php
- Ensure `quarantine/` and `.security/logs` are not accessible from the web server. - Ensure `quarantine/` and `.security/logs` are not accessible from the web server.
- Verify SELinux/AppArmor contexts after running provisioning. - Verify SELinux/AppArmor contexts after running provisioning.
- Ensure owner/group are set to root and web group (e.g., `root:www-data`) and modes match the guide. - Ensure owner/group are set to root and web group (e.g., `root:www-data`) and modes match the guide.
- Keep `upload-logger.php` readable by root (644) and the logs readable only by the intended group (640). Keep `uploadshield.php` readable by root (644) and the logs readable only by the intended group (640).
**Rollback** **Rollback**
- Disable `php_admin_value[auto_prepend_file]` in the pool and reload PHP-FPM. - Disable `php_admin_value[auto_prepend_file]` in the pool and reload PHP-FPM.
- Remove or rotate the `upload-logger` files if needed. - Remove or rotate the `uploadshield` files if needed.
**Further reading & files** **Further reading & files**
- Integration notes: [INTEGRATION.md](INTEGRATION.md) - Integration notes: [INTEGRATION.md](INTEGRATION.md)
- Provisioning script: `scripts/provision_dirs.sh` - Provisioning script: `scripts/provision_dirs.sh`
- Ansible playbook: `scripts/ansible/upload-logger-provision.yml` - Ansible playbook: `scripts/ansible/uploadshield-provision.yml`
- Example configuration: `upload-logger.json` - Example configuration: `uploadshield.json`
--- ---
If you want, I can: (a) generate a site-specific copy of these snippets for your exact paths/PHP version, (b) open a PR with the updated documentation, or (c) produce a one-command installer playbook that runs the provisioning and copies files to a remote host. Tell me which option you prefer. If you want, I can: (a) generate a site-specific copy of these snippets for your exact paths/PHP version, (b) open a PR with the updated documentation, or (c) produce a one-command installer playbook that runs the provisioning and copies files to a remote host. Tell me which option you prefer.

View File

@@ -1,10 +1,10 @@
# Release & Deploy Checklist # Release & Deploy Checklist
This checklist helps you deploy UploadShield's primary script (`upload-logger.php`) to production safely. This checklist helps you deploy UploadShield's primary script (`uploadshield.php`) to production safely.
## Pre-release ## Pre-release
- [ ] Review and pin configuration in `upload-logger.json` (see `examples/upload-logger.json`). - [ ] Review and pin configuration in `uploadshield.json` (see `examples/uploadshield.json`).
- [ ] Ensure unit tests pass and CI workflows are green for the release branch. - [ ] Ensure unit tests pass and CI workflows are green for the release branch.
- [ ] Run static analysis (`vendor/bin/phpstan analyse`) and fix any new issues. - [ ] Run static analysis (`vendor/bin/phpstan analyse`) and fix any new issues.
- [ ] Run `composer audit` to confirm no advisories remain. - [ ] Run `composer audit` to confirm no advisories remain.
@@ -28,7 +28,7 @@ This checklist helps you deploy UploadShield's primary script (`upload-logger.ph
## Configuration ## Configuration
- [ ] Create `upload-logger.json` from `examples/upload-logger.json` and adjust values: - [ ] Create `uploadshield.json` from `examples/uploadshield.json` and adjust values:
- `paths.quarantine_dir` — absolute path to `quarantine/`. - `paths.quarantine_dir` — absolute path to `quarantine/`.
- `paths.state_dir` — absolute path to `state/`. - `paths.state_dir` — absolute path to `state/`.
- `paths.allowlist_file` — path to `allowlist.json`. - `paths.allowlist_file` — path to `allowlist.json`.
@@ -37,7 +37,7 @@ This checklist helps you deploy UploadShield's primary script (`upload-logger.ph
## Deployment ## Deployment
- [ ] Ensure `php_admin_value[auto_prepend_file]` is configured in the site pool for PHP-FPM to include `upload-logger.php` (UploadShield). - [ ] Ensure `php_admin_value[auto_prepend_file]` is configured in the site pool for PHP-FPM to include `uploadshield.php` (UploadShield).
- [ ] Reload or restart PHP-FPM gracefully after changing pool settings. - [ ] Reload or restart PHP-FPM gracefully after changing pool settings.
- [ ] Verify the web server denies direct access to `logs/` and `quarantine/`. - [ ] Verify the web server denies direct access to `logs/` and `quarantine/`.
@@ -50,7 +50,7 @@ This checklist helps you deploy UploadShield's primary script (`upload-logger.ph
## Post-release ## Post-release
- [ ] Configure log rotation (see `examples/logrotate.d/upload-logger`). - [ ] Configure log rotation (see `examples/logrotate.d/uploadshield`).
- [ ] Set up monitoring/alerting on log file growth, error events, and flood alerts. - [ ] Set up monitoring/alerting on log file growth, error events, and flood alerts.
- [ ] Schedule periodic dependency checks (Dependabot and weekly `composer audit`). - [ ] Schedule periodic dependency checks (Dependabot and weekly `composer audit`).
- [ ] Periodically review `allowlist.json` and detector tuning to reduce false positives. - [ ] Periodically review `allowlist.json` and detector tuning to reduce false positives.

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true"> <phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites> <testsuites>
<testsuite name="upload-logger"> <testsuite name="uploadshield">
<directory>tests</directory> <directory>tests</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>

View File

@@ -5,11 +5,11 @@
- hosts: web - hosts: web
become: true become: true
vars: vars:
upload_logger_root: "{{ playbook_dir | default('.') | dirname | realpath }}" uploadshield_root: "{{ playbook_dir | default('.') | dirname | realpath }}"
logs_dir: "{{ upload_logger_root }}/logs" logs_dir: "{{ uploadshield_root }}/logs"
quarantine_dir: "{{ upload_logger_root }}/quarantine" quarantine_dir: "{{ uploadshield_root }}/quarantine"
state_dir: "{{ upload_logger_root }}/state" state_dir: "{{ uploadshield_root }}/state"
examples_dir: "{{ upload_logger_root }}/examples" examples_dir: "{{ uploadshield_root }}/examples"
quarantine_owner: "root" quarantine_owner: "root"
quarantine_group: "www-data" quarantine_group: "www-data"
quarantine_perms: "0700" quarantine_perms: "0700"
@@ -17,8 +17,8 @@
logs_perms: "0750" logs_perms: "0750"
log_file_mode: "0640" log_file_mode: "0640"
selinux_fcontext: "httpd_sys_rw_content_t" selinux_fcontext: "httpd_sys_rw_content_t"
tmpfiles_conf: "/etc/tmpfiles.d/upload-logger.conf" tmpfiles_conf: "/etc/tmpfiles.d/uploadshield.conf"
logrotate_dest: "/etc/logrotate.d/upload-logger" logrotate_dest: "/etc/logrotate.d/uploadshield"
tasks: tasks:
- name: Ensure logs directory exists - name: Ensure logs directory exists
@@ -45,14 +45,14 @@
group: "{{ quarantine_group }}" group: "{{ quarantine_group }}"
mode: "{{ state_perms }}" mode: "{{ state_perms }}"
- name: Ensure example upload-logger.json is copied (only when missing) - name: Ensure example uploadshield.json is copied (only when missing)
copy: copy:
src: "{{ examples_dir }}/upload-logger.json" src: "{{ examples_dir }}/uploadshield.json"
dest: "{{ upload_logger_root }}/upload-logger.json" dest: "{{ uploadshield_root }}/uploadshield.json"
owner: "{{ quarantine_owner }}" owner: "{{ quarantine_owner }}"
group: "{{ quarantine_group }}" group: "{{ quarantine_group }}"
mode: "0644" mode: "0644"
when: not (upload_logger_root + '/upload-logger.json') | path_exists when: not (uploadshield_root + '/uploadshield.json') | path_exists
- name: Install tmpfiles.d entry to recreate dirs at boot - name: Install tmpfiles.d entry to recreate dirs at boot
copy: copy:
@@ -66,12 +66,12 @@
- name: Install logrotate snippet if example exists - name: Install logrotate snippet if example exists
copy: copy:
src: "{{ examples_dir }}/logrotate.d/upload-logger" src: "{{ examples_dir }}/logrotate.d/uploadshield"
dest: "{{ logrotate_dest }}" dest: "{{ logrotate_dest }}"
owner: root owner: root
group: root group: root
mode: '0644' mode: '0644'
when: (examples_dir + '/logrotate.d/upload-logger') | path_exists when: (examples_dir + '/logrotate.d/uploadshield') | path_exists
- name: Set SELinux fcontext for directories when selinux enabled - name: Set SELinux fcontext for directories when selinux enabled
when: ansible_selinux.status == 'enabled' when: ansible_selinux.status == 'enabled'

View File

@@ -1,13 +1,13 @@
--- ---
# Ansible playbook snippet to provision UploadShield directories and permissions. # Ansible playbook snippet to provision UploadShield directories and permissions.
# Usage: ansible-playbook -i inventory scripts/ansible/upload-logger-provision.yml # Usage: ansible-playbook -i inventory scripts/ansible/uploadshield-provision.yml
- hosts: web - hosts: web
become: true become: true
vars: vars:
upload_logger_root: "{{ playbook_dir | default('.') | dirname | realpath }}" uploadshield_root: "{{ playbook_dir | default('.') | dirname | realpath }}"
quarantine_dir: "{{ upload_logger_root }}/quarantine" quarantine_dir: "{{ uploadshield_root }}/quarantine"
state_dir: "{{ upload_logger_root }}/state" state_dir: "{{ uploadshield_root }}/state"
quarantine_owner: "root" quarantine_owner: "root"
quarantine_group: "www-data" quarantine_group: "www-data"
quarantine_perms: "0700" quarantine_perms: "0700"

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Provision quarantine and state directories for UploadShield (upload-logger.php) # Provision quarantine and state directories for UploadShield (uploadshield.php)
# Usage: sudo ./provision_dirs.sh [--config path/to/upload-logger.json] # Usage: sudo ./provision_dirs.sh [--config path/to/uploadshield.json]
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CFG="${1:-$ROOT_DIR/upload-logger.json}" CFG="${1:-$ROOT_DIR/uploadshield.json}"
QUIET=0 QUIET=0
if [[ "${2:-}" == "--quiet" ]]; then QUIET=1; fi if [[ "${2:-}" == "--quiet" ]]; then QUIET=1; fi
@@ -85,7 +85,7 @@ else
fi fi
# Optional tmpfiles.d entry to recreate directories at boot (idempotent) # Optional tmpfiles.d entry to recreate directories at boot (idempotent)
TMPFILE="/etc/tmpfiles.d/upload-logger.conf" TMPFILE="/etc/tmpfiles.d/uploadshield.conf"
if [[ -w /etc/tmpfiles.d || $QUIET -eq 1 ]]; then if [[ -w /etc/tmpfiles.d || $QUIET -eq 1 ]]; then
info "Writing tmpfiles.d entry to ${TMPFILE}" info "Writing tmpfiles.d entry to ${TMPFILE}"
cat > "$TMPFILE" <<EOF cat > "$TMPFILE" <<EOF

View File

@@ -4,9 +4,9 @@
set -euo pipefail set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
ACTIVE_CFG="$ROOT_DIR/upload-logger.json" ACTIVE_CFG="$ROOT_DIR/uploadshield.json"
PROD_CFG="$ROOT_DIR/config/upload-logger.prod.json" PROD_CFG="$ROOT_DIR/config/uploadshield.prod.json"
BLOCK_CFG="$ROOT_DIR/config/upload-logger.blocking.json" BLOCK_CFG="$ROOT_DIR/config/uploadshield.blocking.json"
BACKUP_DIR="$ROOT_DIR/config/backups" BACKUP_DIR="$ROOT_DIR/config/backups"
DRY_RUN=0 DRY_RUN=0
CONFIRM=0 CONFIRM=0
@@ -46,8 +46,8 @@ fi
mkdir -p "$BACKUP_DIR" mkdir -p "$BACKUP_DIR"
TS=$(date +%Y%m%dT%H%M%S) TS=$(date +%Y%m%dT%H%M%S)
if [[ -f "$ACTIVE_CFG" ]]; then if [[ -f "$ACTIVE_CFG" ]]; then
cp -a "$ACTIVE_CFG" "$BACKUP_DIR/upload-logger.json.bak.$TS" cp -a "$ACTIVE_CFG" "$BACKUP_DIR/uploadshield.json.bak.$TS"
echo "Backed up current config to $BACKUP_DIR/upload-logger.json.bak.$TS" echo "Backed up current config to $BACKUP_DIR/uploadshield.json.bak.$TS"
fi fi
cp -a "$BLOCK_CFG" "$ACTIVE_CFG" cp -a "$BLOCK_CFG" "$ACTIVE_CFG"

View File

@@ -4,7 +4,7 @@ After=network.target
[Service] [Service]
Type=oneshot Type=oneshot
ExecStart=/usr/local/bin/upload-logger-provision.sh /opt/upload-logger/upload-logger.json ExecStart=/usr/local/bin/uploadshield-provision.sh /opt/uploadshield/uploadshield.json
RemainAfterExit=yes RemainAfterExit=yes
[Install] [Install]

View File

@@ -1,5 +1,5 @@
<?php <?php
// Simple endpoint that accepts file uploads; auto_prepend_file will run upload-logger.php // Simple endpoint that accepts file uploads; auto_prepend_file will run uploadshield.php
header('Content-Type: text/plain'); header('Content-Type: text/plain');
if (empty($_FILES)) { if (empty($_FILES)) {
echo "no files\n"; echo "no files\n";

View File

@@ -1,20 +1,11 @@
<?php <?php
/** /**
* Global Upload Logger (Hardened v3) part of UploadShield * UploadShield runtime
* Project: TheWallpapers
*
* Purpose: * Purpose:
* - Log ALL normal uploads via $_FILES (single + multi) * - Log uploads and detect suspicious uploads before application code runs
* - Detect common evasion (double extensions, fake images, path tricks, PHP payload in non-php files) * - Configured via `uploadshield.json` (env `UPLOADSHIELD_CONFIG` supported)
* - Log suspicious "raw body" uploads (php://input / octet-stream) that bypass $_FILES
* - Optional blocking mode
* *
* Install: * Install as PHP-FPM pool `auto_prepend_file=/path/to/uploadshield.php`
* - Use as PHP-FPM pool `auto_prepend_file=.../upload_logger.php`
*
* Notes:
* - This cannot *guarantee* interception of every file-write exploit (file_put_contents, ZipArchive extract, etc.)
* but it catches most real-world upload vectors and provides strong forensic logging.
*/ */
// Ignore CLI // Ignore CLI
@@ -74,11 +65,11 @@ $PEEK_RAW_INPUT = false;
$TRUSTED_PROXY_IPS = ['127.0.0.1', '::1']; $TRUSTED_PROXY_IPS = ['127.0.0.1', '::1'];
// Environment variable name or marker file to explicitly allow peeking // Environment variable name or marker file to explicitly allow peeking
$ALLOW_PEEK_ENV = 'UPLOAD_LOGGER_ALLOW_PEEK'; $ALLOW_PEEK_ENV = 'UPLOADSHIELD_ALLOW_PEEK';
$PEEK_ALLOW_FILE = __DIR__ . '/.upload_logger_allow_peek'; $PEEK_ALLOW_FILE = __DIR__ . '/.uploadshield_allow_peek';
// Auto-enable peek only when explicitly allowed by environment/file or when a // Auto-enable peek only when explicitly allowed by environment/file or when a
// trusted frontend indicates the body was buffered via header `X-Upload-Logger-Peek: 1`. // trusted frontend indicates the body was buffered via header `X-UploadShield-Peek: 1`.
// This avoids consuming request bodies unexpectedly. // This avoids consuming request bodies unexpectedly.
try { try {
$envAllow = getenv($ALLOW_PEEK_ENV) === '1'; $envAllow = getenv($ALLOW_PEEK_ENV) === '1';
@@ -115,7 +106,7 @@ $BASE64_ALLOWLIST_CTYPE = [];
// Allowlist file location and environment override // Allowlist file location and environment override
$ALLOWLIST_FILE_DEFAULT = __DIR__ . '/allowlist.json'; $ALLOWLIST_FILE_DEFAULT = __DIR__ . '/allowlist.json';
$ALLOWLIST_FILE = getenv('UPLOAD_LOGGER_ALLOWLIST') ?: $ALLOWLIST_FILE_DEFAULT; $ALLOWLIST_FILE = getenv('UPLOADSHIELD_ALLOWLIST') ?: $ALLOWLIST_FILE_DEFAULT;
if (is_file($ALLOWLIST_FILE)) { if (is_file($ALLOWLIST_FILE)) {
$raw = @file_get_contents($ALLOWLIST_FILE); $raw = @file_get_contents($ALLOWLIST_FILE);
@@ -131,9 +122,9 @@ if (is_file($ALLOWLIST_FILE)) {
} }
// Load config (JSON) or fall back to inline defaults. // Load config (JSON) or fall back to inline defaults.
// Config file path may be overridden with env `UPLOAD_LOGGER_CONFIG`. // Config file path may be overridden with env `UPLOADSHIELD_CONFIG`.
$CONFIG_FILE_DEFAULT = __DIR__ . '/upload-logger.json'; $CONFIG_FILE_DEFAULT = __DIR__ . '/uploadshield.json';
$CONFIG_FILE = getenv('UPLOAD_LOGGER_CONFIG') ?: $CONFIG_FILE_DEFAULT; $CONFIG_FILE = getenv('UPLOADSHIELD_CONFIG') ?: $CONFIG_FILE_DEFAULT;
// Default modules and settings // Default modules and settings
$DEFAULT_CONFIG = [ $DEFAULT_CONFIG = [
@@ -168,7 +159,7 @@ if (is_file($CONFIG_FILE)) {
$cfgLogFile = $CONFIG_DATA['paths']['log_file'] ?? null; $cfgLogFile = $CONFIG_DATA['paths']['log_file'] ?? null;
if (is_string($cfgLogFile) && $cfgLogFile !== '') { if (is_string($cfgLogFile) && $cfgLogFile !== '') {
$isAbs = preg_match('#^[A-Za-z]:[\\/]#', $cfgLogFile) === 1 $isAbs = preg_match('#^[A-Za-z]:[\/]#', $cfgLogFile) === 1
|| (strlen($cfgLogFile) > 0 && ($cfgLogFile[0] === '/' || $cfgLogFile[0] === '\\')); || (strlen($cfgLogFile) > 0 && ($cfgLogFile[0] === '/' || $cfgLogFile[0] === '\\'));
if ($isAbs) { if ($isAbs) {
$logFile = $cfgLogFile; $logFile = $cfgLogFile;
@@ -183,7 +174,7 @@ $BOOT_LOGGER = new \UploadLogger\Core\Services\LogService($logFile, []);
$fileAllow = is_file($PEEK_ALLOW_FILE); $fileAllow = is_file($PEEK_ALLOW_FILE);
$headerAllow = false; $headerAllow = false;
if (isset($_SERVER['HTTP_X_UPLOAD_LOGGER_PEEK']) && $_SERVER['HTTP_X_UPLOAD_LOGGER_PEEK'] === '1') { if (isset($_SERVER['HTTP_X_UPLOADSHIELD_PEEK']) && $_SERVER['HTTP_X_UPLOADSHIELD_PEEK'] === '1') {
$clientIp = $REQ->getClientIp(); $clientIp = $REQ->getClientIp();
if (in_array($clientIp, $TRUSTED_PROXY_IPS, true)) { if (in_array($clientIp, $TRUSTED_PROXY_IPS, true)) {
$headerAllow = true; $headerAllow = true;
@@ -380,7 +371,7 @@ $LOGGER = new \UploadLogger\Core\Logger($logFile, $REQUEST_CTX, $CONFIG);
/* /*
* Map frequently-used legacy globals to values from `Config` so the rest of * Map frequently-used legacy globals to values from `Config` so the rest of
* the procedural helpers can continue to reference globals but operators * the procedural helpers can continue to reference globals but operators
* may control behavior via `upload-logger.json`. * may control behavior via `uploadshield.json`.
*/ */
$BLOCK_SUSPICIOUS = $CONFIG->get('ops.block_suspicious', $BLOCK_SUSPICIOUS ?? false); $BLOCK_SUSPICIOUS = $CONFIG->get('ops.block_suspicious', $BLOCK_SUSPICIOUS ?? false);
$MAX_SIZE = (int)$CONFIG->get('limits.max_size', $MAX_SIZE ?? (50 * 1024 * 1024)); $MAX_SIZE = (int)$CONFIG->get('limits.max_size', $MAX_SIZE ?? (50 * 1024 * 1024));