13 KiB
Integration
Example upload-logger.json (commented for easy copy/paste into your environment):
// {
// "modules": {
// "flood": true,
// "filename": true,
// "mime_sniff": true,
// "hashing": true,
// "base64_detection": true,
// "raw_peek": false,
// "archive_inspect": true,
// "quarantine": true
// },
// "paths": {
// "log_file": "logs/uploads.log",
// "quarantine_dir": "quarantine",
// "state_dir": "state",
// "allowlist_file": "allowlist.json"
// },
// "limits": {
// "max_size": 52428800,
// "raw_body_min": 512000,
// "sniff_max_bytes": 8192,
// "sniff_max_filesize": 2097152,
// "hash_max_filesize": 10485760,
// "archive_max_inspect_size": 52428800,
// "archive_max_entries": 200
// },
// "ops": {
// "quarantine_owner": "root",
// "quarantine_group": "www-data",
// "quarantine_dir_perms": "0700",
// "log_rotate": {
// "enabled": true,
// "size": 10485760,
// "keep": 7
// }
// },
// "allowlists": {
// "base64_uris": [
// "/api/uploads/avatars",
// "/api/v1/avatars",
// "/user/avatar",
// "/media/upload",
// "/api/media",
// "/api/uploads",
// "/api/v1/uploads",
// "/attachments/upload",
// "/upload",
// "#^/internal/webhook#",
// "#/hooks/(github|gitlab|stripe|slack)#",
// "/services/avatars",
// "/api/profile/photo"
// ],
// "ctypes": ["image/svg+xml","application/xml","text/xml"]
// }
// }
Notes:
- Remove the leading
//when copying this into a realupload-logger.jsonfile. - Adjust paths, owners, and limits to match your environment and PHP-FPM worker permissions.
ContentDetector tuning and false-positive guidance
-
The repository includes a
ContentDetectorthat performs a fast head-scan of uploaded files to detect PHP open-tags and common webshell indicators (for examplepassthru(),system(),exec(),shell_exec(),proc_open(),popen(),base64_decode(),eval(),assert()). It intentionally limits the scan to a small number of bytes to reduce CPU/IO overhead. -
Tuning options (place these in
upload-logger.json):limits.sniff_max_bytes(integer): number of bytes to read from the file head for scanning. Default:8192.limits.sniff_max_filesize(integer): only perform head-scan on files with size <= this value. Default:2097152(2 MB).allowlists.ctypes(array): content-types that should be considered trusted for base64/raw payloads (for exampleimage/svg+xml,application/xml,text/xml) and may relax some detections.allowlists.base64_uris(array): URI patterns that should be ignored for large base64 payloads (webhooks, avatar uploads, etc.).
-
False positives:
eval(and other tokens commonly appear in client-side JS inside SVG files or in benign templating contexts. If you observe false positives:- Add trusted URIs to
allowlists.base64_urisfor endpoints that legitimately accept encoded content. - Add trusted content-types to
allowlists.ctypesto relax detection for XML/SVG uploads. - Tune
limits.sniff_max_bytesandlimits.sniff_max_filesizeto increase or decrease sensitivity.
- Add trusted URIs to
-
Suggested (example) detector tuning block (commented):
// "detectors": {
// "content": {
// "enabled": true,
// "sniff_max_bytes": 8192,
// "sniff_max_filesize": 2097152,
// "allow_xml_eval": false
// }
// }
Remove the leading // when copying these example snippets into a real upload-logger.json file.
🔐 Per-Site PHP Upload Guard Integration Guide
This guide explains how to integrate a global PHP upload monitoring script
using auto_prepend_file, on a per-site basis, with isolated security
folders.
📁 1. Recommended Folder Structure
Each website should contain its own hidden security directory:
/var/www/sites/example-site/
├── public/
├── app/
├── uploads/
├── .security/
│ ├── upload_guard.php
│ └── logs/
│ └── uploads.log
Benefits:
- Per-site isolation
- Easier debugging
- Independent log files
- Reduced attack surface
🔧 2. Create the Security Directory
From the site root:
cd /var/www/sites/example-site
mkdir .security
mkdir .security/logs
Set secure permissions:
- Set secure permissions:
chown -R root:www-data .security
chmod 750 .security
chmod 750 .security/logs
Quarantine hardening (important):
-
Ensure the quarantine directory is owner
root, groupwww-data, and mode0700so quarantined files are not accessible to other system users. Example provisioning scriptscripts/provision_dirs.shnow enforces these permissions and tightens existing files to0600. -
If using Ansible, the playbook
scripts/ansible/upload-logger-provision.ymlincludes a task that sets any existing files in the quarantine directory to0600and enforces owner/group. -
Verify SELinux/AppArmor contexts after provisioning; the script attempts to register fcontext entries and calls
restoreconwhen available.
📄 3. Install the Upload Guard Script
Create the script file:
nano .security/upload_guard.php
Paste your hardened upload monitoring script.
Inside the script, configure logging:
$logFile = __DIR__ . '/logs/uploads.log';
Lock the script:
chown root:root .security/upload_guard.php
chmod 644 .security/upload_guard.php
⚙️ 4. Enable auto_prepend_file (Per Site)
Option A — PHP-FPM Pool (Recommended)
Edit the site’s PHP-FPM pool configuration:
nano /etc/php/8.x/fpm/pool.d/example-site.conf
Add:
php_admin_value[auto_prepend_file] = /var/www/sites/example-site/.security/upload_guard.php
Reload PHP-FPM:
systemctl reload php8.x-fpm
🔐 Per-Site PHP Upload Guard Integration Guide
This guide explains how to integrate a global PHP upload monitoring script
using auto_prepend_file, on a per-site basis, with isolated security
folders.
📁 1. Recommended Folder Structure
Each website should contain its own hidden security directory:
/var/www/sites/example-site/
├── public/
├── app/
├── uploads/
├── .security/
│ ├── upload-logger.php
│ └── logs/
│ └── uploads.log
Benefits:
- Per-site isolation
- Easier debugging
- Independent log files
- Reduced attack surface
🔧 2. Create the Security Directory
From the site root:
cd /var/www/sites/example-site
mkdir .security
mkdir .security/logs
Set secure permissions:
chown -R root:www-data .security
chmod 750 .security
chmod 750 .security/logs
📄 3. Install the Upload Guard Script
Create the script file:
nano .security/upload-logger.php
Paste your hardened upload monitoring script.
Inside the script, configure logging:
$logFile = __DIR__ . '/logs/uploads.log';
Lock the script:
chown root:root .security/upload-logger.php
chmod 644 .security/upload-logger.php
⚙️ 4. Enable auto_prepend_file (Per Site)
Option A — PHP-FPM Pool (Recommended)
Edit the site’s PHP-FPM pool configuration:
nano /etc/php/8.x/fpm/pool.d/example-site.conf
Add:
php_admin_value[auto_prepend_file] = /var/www/sites/example-site/.security/upload-logger.php
Reload PHP-FPM (adjust service name to match your PHP version):
systemctl reload php8.x-fpm
Option B — Apache Virtual Host
If using a shared PHP-FPM pool, configure in the vHost:
<Directory /var/www/sites/example-site>
php_admin_value auto_prepend_file /var/www/sites/example-site/.security/upload-logger.php
</Directory>
Reload Apache:
systemctl reload apache2
🚫 5. Block Web Access to .security
Prevent direct HTTP access to the security folder.
In the vHost:
<Directory /var/www/sites/example-site/.security>
Require all denied
</Directory>
Or in .htaccess (if allowed):
Require all denied
✅ 6. Verify Installation
Create a temporary file:
<?php phpinfo();
Open it in browser and search for:
auto_prepend_file
Expected output:
/var/www/sites/example-site/.security/upload_guard.php
Remove the test file after verification.
🧪 7. Test Upload Logging
Create a simple upload test:
<form method="post" enctype="multipart/form-data">
<input type="file" name="testfile">
<button>Upload</button>
</form>
Upload any file and check logs:
cat .security/logs/uploads.log
You should see a new entry.
🔒 8. Disable PHP Execution in Uploads
Always block PHP execution in upload directories.
Example (Apache):
<Directory /var/www/sites/example-site/uploads>
php_admin_flag engine off
AllowOverride None
</Directory>
Reload Apache after changes.
🛡️ 9. Enable Blocking Mode (Optional)
After monitoring for some time, enable blocking.
Edit:
$BLOCK_SUSPICIOUS = true;
Then reload PHP-FPM.
📊 10. (Optional) Fail2Ban Integration (JSON logs)
Create a JSON-aware filter that matches event: "suspicious" and extracts the IP address.
nano /etc/fail2ban/filter.d/php-upload.conf
[Definition]
# Match JSON lines where event == "suspicious" and capture the IPv4 address as <HOST>
failregex = ^.*"event"\s*:\s*"suspicious".*"ip"\s*:\s*"(?P<host>\d{1,3}(?:\.\d{1,3}){3})".*$
ignoreregex =
Create a jail that points to the per-site logs (or a central aggregated log):
[php-upload]
enabled = true
filter = php-upload
logpath = /var/www/sites/*/.security/logs/uploads.log
maxretry = 3
findtime = 600
bantime = 86400
action = iptables-multiport[name=php-upload, port="http,https", protocol=tcp]
Restart Fail2Ban:
systemctl restart fail2ban
Fail2Ban action: nftables example
If your host uses nftables, prefer the nftables action so bans use the system firewall:
[php-upload]
enabled = true
filter = php-upload
logpath = /var/www/sites/*/.security/logs/uploads.log
maxretry = 3
findtime = 600
bantime = 86400
action = nftables[name=php-upload, port="http,https", protocol=tcp]
This uses Fail2Ban's nftables action (available on modern distributions). Adjust port/protocol to match your services.
Central log aggregation (Filebeat / rsyslog)
Forwarding per-site JSON logs to a central collector simplifies alerts and Fail2Ban at scale. Two lightweight options:
- Filebeat prospector (send to Logstash/Elasticsearch):
filebeat.inputs:
- type: log
paths:
- /var/www/sites/*/.security/logs/uploads.log
json.keys_under_root: true
json.add_error_key: true
fields:
source: php-upload-logger
output.logstash:
hosts: ["logserver:5044"]
- rsyslog
imfileforwarding to remote syslog (central rsyslog/logstash):
Add to /etc/rsyslog.d/10-upload-logger.conf:
module(load="imfile" PollingInterval="10")
input(type="imfile" File="/var/www/sites/*/.security/logs/uploads.log" Tag="uploadlogger" Severity="info" Facility="local7")
*.* @@logserver:514
Both options keep JSON intact for downstream parsing and reduce per-host Fail2Ban complexity.
Testing your Fail2Ban filter
Create a temporary file containing a representative JSON log line emitted by upload-logger.php and run fail2ban-regex against your filter to validate detection.
# create test file with a suspicious event
cat > /tmp/test_upload.log <<'JSON'
{"ts":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","event":"suspicious","ip":"1.2.3.4","user":"guest","name":"evil.php.jpg","real_mime":"application/x-php","reasons":["bad_name","php_payload"]}
JSON
# test the filter (adjust path to filter if different)
fail2ban-regex /tmp/test_upload.log /etc/fail2ban/filter.d/php-upload.conf
fail2ban-regex will report how many matches were found and display sample matched groups (including the captured <HOST>). Use this to iterate on the failregex if it doesn't extract the IP as expected.
🏁 Final Architecture
Client → Web Server → PHP (auto_prepend) → Application → Disk
↓
Log / Alert / Ban
This provides multi-layer upload monitoring and protection.
🗂️ Log rotation & SELinux/AppArmor notes
- Example
logrotatesnippet to rotate per-site logs weekly and keep 8 rotations:
/var/www/sites/*/.security/logs/uploads.log {
weekly
rotate 8
compress
missingok
notifempty
create 0640 root adm
}
- If your host enforces SELinux or AppArmor, ensure the
.securitydirectory and log files have the correct context so PHP-FPM can read the script and write logs. For SELinux (RHEL/CentOS) you may need:
chcon -R -t httpd_sys_rw_content_t /var/www/sites/example-site/.security/logs
restorecon -R /var/www/sites/example-site/.security
Adjust commands to match your platform and policy. AppArmor profiles may require adding paths to the PHP-FPM profile.
⚠️ Security Notes
- Never use
777permissions - Keep
.securityowned byroot - Regularly review logs
- Update PHP and extensions
- Combine with OS-level auditing for best results
📌 Recommended Maintenance
Weekly:
grep ALERT .security/logs/uploads.log