From 7c8bccc911deecf06a2ca1abd116bb5e1f07f241 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Thu, 12 Feb 2026 14:48:07 +0100 Subject: [PATCH] Rename project to UploadShield: update runtime, configs, docs, and provisioning; run lint/tests --- .gitignore | 2 +- CONFIG_REFERENCE.md | 6 +-- CONTRIBUTING.md | 2 +- INTEGRATION.md | 10 ++--- README.md | 18 ++++---- ...ocking.json => uploadshield.blocking.json} | 6 +-- ...ogger.prod.json => uploadshield.prod.json} | 6 +-- core/Config.php | 2 +- docs/INSTALLATION.md | 42 +++++++++---------- docs/release-checklist.md | 10 ++--- .../{upload-logger.json => uploadshield.json} | 0 phpunit.xml | 2 +- scripts/ansible/provision-full.yml | 26 ++++++------ ...ovision.yml => uploadshield-provision.yml} | 8 ++-- scripts/provision_dirs.sh | 8 ++-- scripts/rollout_enable_blocking.sh | 10 ++--- ...service => uploadshield-provision.service} | 2 +- tests/smoke/public/upload.php | 2 +- upload-logger.json => uploadshield.json | 0 upload-logger.php => uploadshield.php | 37 +++++++--------- 20 files changed, 95 insertions(+), 104 deletions(-) rename config/{upload-logger.blocking.json => uploadshield.blocking.json} (83%) rename config/{upload-logger.prod.json => uploadshield.prod.json} (83%) rename examples/{upload-logger.json => uploadshield.json} (100%) rename scripts/ansible/{upload-logger-provision.yml => uploadshield-provision.yml} (87%) rename scripts/systemd/{upload-logger-provision.service => uploadshield-provision.service} (63%) rename upload-logger.json => uploadshield.json (100%) rename upload-logger.php => uploadshield.php (92%) diff --git a/.gitignore b/.gitignore index c54437a..763a0b0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ uploads.log # Peek allow marker (local only) -/.upload_logger_allow_peek +/.uploadshield_allow_peek # Local environment files .env diff --git a/CONFIG_REFERENCE.md b/CONFIG_REFERENCE.md index 9746323..4679c3b 100644 --- a/CONFIG_REFERENCE.md +++ b/CONFIG_REFERENCE.md @@ -1,6 +1,6 @@ # 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 @@ -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. - `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 { @@ -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. - 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. -- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83e9751..4b069ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ Add CONFIG_REFERENCE.md mapping configuration options - A basic smoke harness exists under `tests/smoke/`. To run locally: ```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 ``` diff --git a/INTEGRATION.md b/INTEGRATION.md index db40681..1ce6875 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -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.). -Example `upload-logger.json` (simplified) for UploadShield: +Example `uploadshield.json` (simplified) for UploadShield: ```json { @@ -50,7 +50,7 @@ Notes: ### 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`). -- 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_filesize` (default 2097152) — only scan files up to this size. - `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.add_error_key: true fields: - source: php-upload-logger + source: php-uploadshield output.logstash: hosts: ["logserver:5044"] ``` ## 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. @@ -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. - 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`. diff --git a/README.md b/README.md index 29d9035..55d2bc7 100644 --- a/README.md +++ b/README.md @@ -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. -Primary file: [upload-logger.php](upload-logger.php) +Primary file: [uploadshield.php](uploadshield.php) Summary - 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. -Key configuration (top of `upload-logger.php`) +Key configuration (top of `uploadshield.php`) - `$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. - `$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. 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. - 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 - `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_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`). -- 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 - 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. - 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 - 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. 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. 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. 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 diff --git a/config/upload-logger.blocking.json b/config/uploadshield.blocking.json similarity index 83% rename from config/upload-logger.blocking.json rename to config/uploadshield.blocking.json index 6c84e01..8465564 100644 --- a/config/upload-logger.blocking.json +++ b/config/uploadshield.blocking.json @@ -7,9 +7,9 @@ "archive_inspect": true }, "paths": { - "quarantine_dir": "/var/lib/upload-logger/quarantine", - "state_dir": "/var/lib/upload-logger/state", - "allowlist_file": "/etc/upload-logger/allowlist.json" + "quarantine_dir": "/var/lib/uploadshield/quarantine", + "state_dir": "/var/lib/uploadshield/state", + "allowlist_file": "/etc/uploadshield/allowlist.json" }, "limits": { "max_size": 52428800, diff --git a/config/upload-logger.prod.json b/config/uploadshield.prod.json similarity index 83% rename from config/upload-logger.prod.json rename to config/uploadshield.prod.json index 884b4d0..99887dd 100644 --- a/config/upload-logger.prod.json +++ b/config/uploadshield.prod.json @@ -7,9 +7,9 @@ "archive_inspect": true }, "paths": { - "quarantine_dir": "/var/lib/upload-logger/quarantine", - "state_dir": "/var/lib/upload-logger/state", - "allowlist_file": "/etc/upload-logger/allowlist.json" + "quarantine_dir": "/var/lib/uploadshield/quarantine", + "state_dir": "/var/lib/uploadshield/state", + "allowlist_file": "/etc/uploadshield/allowlist.json" }, "limits": { "max_size": 52428800, diff --git a/core/Config.php b/core/Config.php index d50eb2a..a009406 100644 --- a/core/Config.php +++ b/core/Config.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace UploadLogger\Core; /** - * Simple immutable configuration holder for UploadShield (upload-logger.json). + * Simple immutable configuration holder for UploadShield (uploadshield.json). */ final class Config { diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 0455a8a..d6cec4a 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -1,6 +1,6 @@ # 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. **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. **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. -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. 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)** -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: @@ -41,16 +41,16 @@ Example layout: ├── app/ ├── uploads/ ├── .security/ -│ ├── upload-logger.php +│ ├── uploadshield.php │ └── logs/ │ └── uploads.log ├── quarantine/ └── state/ -``` + Place `uploadshield.php` into `.security/uploadshield.php`. **3. Copy files & configure** -- Place `upload-logger.php` into `.security/upload-logger.php`. -- Copy `upload-logger.json` from the repository to the same directory and edit paths to absolute values, e.g.: +- Place `uploadshield.php` into `.security/uploadshield.php`. +- 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.quarantine_dir` → `/var/www/sites/example-site/quarantine` - `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: - `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. PHP‑FPM configuration (per-site pool)** 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 -; Ensure upload-logger runs before application code -php_admin_value[auto_prepend_file] = /var/www/sites/example-site/.security/upload-logger.php +; Ensure uploadshield runs before application code +php_admin_value[auto_prepend_file] = /var/www/sites/example-site/.security/uploadshield.php ``` Then reload PHP-FPM: @@ -98,11 +98,11 @@ sudo systemctl reload php8.1-fpm ``` 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. **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 { @@ -129,10 +129,10 @@ Create `/etc/logrotate.d/upload-logger` with content adapted to your paths: - Upload a file containing ` - + tests diff --git a/scripts/ansible/provision-full.yml b/scripts/ansible/provision-full.yml index 01a3b2f..d58c14a 100644 --- a/scripts/ansible/provision-full.yml +++ b/scripts/ansible/provision-full.yml @@ -5,11 +5,11 @@ - hosts: web become: true vars: - upload_logger_root: "{{ playbook_dir | default('.') | dirname | realpath }}" - logs_dir: "{{ upload_logger_root }}/logs" - quarantine_dir: "{{ upload_logger_root }}/quarantine" - state_dir: "{{ upload_logger_root }}/state" - examples_dir: "{{ upload_logger_root }}/examples" + uploadshield_root: "{{ playbook_dir | default('.') | dirname | realpath }}" + logs_dir: "{{ uploadshield_root }}/logs" + quarantine_dir: "{{ uploadshield_root }}/quarantine" + state_dir: "{{ uploadshield_root }}/state" + examples_dir: "{{ uploadshield_root }}/examples" quarantine_owner: "root" quarantine_group: "www-data" quarantine_perms: "0700" @@ -17,8 +17,8 @@ logs_perms: "0750" log_file_mode: "0640" selinux_fcontext: "httpd_sys_rw_content_t" - tmpfiles_conf: "/etc/tmpfiles.d/upload-logger.conf" - logrotate_dest: "/etc/logrotate.d/upload-logger" + tmpfiles_conf: "/etc/tmpfiles.d/uploadshield.conf" + logrotate_dest: "/etc/logrotate.d/uploadshield" tasks: - name: Ensure logs directory exists @@ -45,14 +45,14 @@ group: "{{ quarantine_group }}" 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: - src: "{{ examples_dir }}/upload-logger.json" - dest: "{{ upload_logger_root }}/upload-logger.json" + src: "{{ examples_dir }}/uploadshield.json" + dest: "{{ uploadshield_root }}/uploadshield.json" owner: "{{ quarantine_owner }}" group: "{{ quarantine_group }}" 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 copy: @@ -66,12 +66,12 @@ - name: Install logrotate snippet if example exists copy: - src: "{{ examples_dir }}/logrotate.d/upload-logger" + src: "{{ examples_dir }}/logrotate.d/uploadshield" dest: "{{ logrotate_dest }}" owner: root group: root 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 when: ansible_selinux.status == 'enabled' diff --git a/scripts/ansible/upload-logger-provision.yml b/scripts/ansible/uploadshield-provision.yml similarity index 87% rename from scripts/ansible/upload-logger-provision.yml rename to scripts/ansible/uploadshield-provision.yml index d04df3b..a164869 100644 --- a/scripts/ansible/upload-logger-provision.yml +++ b/scripts/ansible/uploadshield-provision.yml @@ -1,13 +1,13 @@ --- # 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 become: true vars: - upload_logger_root: "{{ playbook_dir | default('.') | dirname | realpath }}" - quarantine_dir: "{{ upload_logger_root }}/quarantine" - state_dir: "{{ upload_logger_root }}/state" + uploadshield_root: "{{ playbook_dir | default('.') | dirname | realpath }}" + quarantine_dir: "{{ uploadshield_root }}/quarantine" + state_dir: "{{ uploadshield_root }}/state" quarantine_owner: "root" quarantine_group: "www-data" quarantine_perms: "0700" diff --git a/scripts/provision_dirs.sh b/scripts/provision_dirs.sh index 35e4004..29a921f 100644 --- a/scripts/provision_dirs.sh +++ b/scripts/provision_dirs.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -# Provision quarantine and state directories for UploadShield (upload-logger.php) -# Usage: sudo ./provision_dirs.sh [--config path/to/upload-logger.json] +# Provision quarantine and state directories for UploadShield (uploadshield.php) +# Usage: sudo ./provision_dirs.sh [--config path/to/uploadshield.json] ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" -CFG="${1:-$ROOT_DIR/upload-logger.json}" +CFG="${1:-$ROOT_DIR/uploadshield.json}" QUIET=0 if [[ "${2:-}" == "--quiet" ]]; then QUIET=1; fi @@ -85,7 +85,7 @@ else fi # 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 info "Writing tmpfiles.d entry to ${TMPFILE}" cat > "$TMPFILE" < 0 && ($cfgLogFile[0] === '/' || $cfgLogFile[0] === '\\')); if ($isAbs) { $logFile = $cfgLogFile; @@ -183,7 +174,7 @@ $BOOT_LOGGER = new \UploadLogger\Core\Services\LogService($logFile, []); $fileAllow = is_file($PEEK_ALLOW_FILE); $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(); if (in_array($clientIp, $TRUSTED_PROXY_IPS, 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 * 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); $MAX_SIZE = (int)$CONFIG->get('limits.max_size', $MAX_SIZE ?? (50 * 1024 * 1024));