Uploads PHP Execution Block: Setup, Nginx Config, and Common Pitfalls
The Uploads PHP Execution Block (new in GuardPress 1.6.41) writes an .htaccess rule into wp-content/uploads/ that denies execution of any file with a PHP extension. It shuts down the “upload a .php via a bad file-type check in another plugin, then hit the URL to run a shell” attack class regardless of what other plugins do. This article covers what it protects against, the safe way to enable it, the AllowOverride canary that verifies your server is actually honoring the rule, the Nginx equivalent server-block config for non-Apache hosts, and what to do if a legitimate plugin depends on PHP-in-uploads.
The panel loads the current state via AJAX — server type, pre-enable scan results, and buttons update in place without a page reload.
What the Block Actually Protects Against
The most common way an attacker turns a “my site got hacked” incident into a full site takeover is by uploading a .php file into wp-content/uploads/ through a plugin with a weak file-type check, then hitting the URL directly. The uploaded file runs as PHP, and the attacker now has arbitrary code execution.
The chain requires two things to work: a plugin that lets the wrong file type through (many have), and the web server willing to execute anything under uploads/. Denying the second half breaks the chain regardless of which plugin has the weak validator this month. That’s what this feature does. It writes an .htaccess rule that denies execution of any file with a PHP extension (.php, .phtml, .php3, .php4, .php5, .php7, .php8, .phar) inside wp-content/uploads/. A file with those extensions can still land on disk if another plugin has a broken validator, but hitting its URL now returns 403 instead of executing.
Some plugins legitimately execute PHP from uploads/ as part of normal operation — a few older backup and LMS-caching plugins, some backup restore flows that stage code in the uploads dir mid-restore. Enabling the block on those installs would break the plugin. The pre-enable scan lists any existing PHP files first so you can decide before the write happens.
Enabling the Block
Open the panel
Go to GuardPress → Settings → Hardening → Uploads PHP Execution Block. The status card loads via AJAX and shows the current state, your detected server type, and the .htaccess path the block will be written to.
Review the pre-enable scan
The panel walks wp-content/uploads/ recursively and lists every file with a PHP extension it finds. Files are split into two groups: real code (in a yellow warning box) and safe-to-block stubs (in a green collapsible — see below for what qualifies). If there are files in the yellow box, expand each one and confirm you know why they’re there before continuing. Each file is annotated with the plugin it likely belongs to.
Click Enable
The button lives top-right of the status card on Apache / LiteSpeed hosts. Clicking it writes the managed .htaccess block into wp-content/uploads/.htaccess, preserving any pre-existing content in that file (the block is bracketed with sentinel comments so future updates can find and rewrite it cleanly). On Nginx, the button is hidden and a config snippet is shown instead — see the Nginx section below.
Review the canary result
Immediately after the write, GuardPress runs an AllowOverride canary probe (details in the AllowOverride section). If the probe reports the block is not being honored, the panel surfaces a warning. The .htaccess is still on disk, but your server config is ignoring it — you’ll need to fix the server config or use the Nginx equivalent.
If the pre-enable scan flags real code and you’ve confirmed you want to block it anyway, click Enable anyway (force) in the warning box. The block is written and any flagged files become 403 on next access. Same effect via WP-CLI: wp guardpress hardening uploads-php-block enable --force.
Understanding the Pre-Enable Scan
Almost every plugin drops an empty stub file into each of its uploads subdirectories to prevent Apache’s directory listing from exposing the folder contents. Two common variants exist:
<?php // Silence is golden.
and the stricter variant:
<?php http_response_code(403); exit;
Both patterns exist to have no runtime behavior. Denying their execution has no functional downside — you swap a PHP-generated 403 for an Apache-generated 403 with the same net effect. The pre-enable scan classifies files 500 bytes or smaller that contain no risky-behavior tokens (no echo, print, include, eval, $_GET/$_POST/$_FILES, no filesystem writes, no code execution primitives, no database access) as safe-to-block stubs. These are collected in the green collapsible in the panel and don’t gate enabling — the block goes on without a warning.
Files that fail the classifier — larger than 500 bytes, or containing any of the tokens above — land in the yellow warning box as “real code” and require your explicit review before enabling. That’s the class of file where enabling the block could break something you care about.
The classifier is deliberately conservative — it flags anything that could do anything. But because the check is text-based, a small file that avoids the trigger tokens could still be malicious. If any of the flagged safe-to-block stubs are in a directory you didn’t create and don’t recognize, treat that as a signal to investigate before enabling the block. The block will 403 the file, but it won’t explain how it got there.
Nginx Server-Block Equivalent
Nginx doesn’t honor .htaccess at all. If you’re on Nginx, the panel detects your server type and hides the Enable button (a hidden button would write a file the server was going to ignore, which is worse than doing nothing). Instead, the panel shows a copy-paste config snippet you add to your Nginx server block:
# GuardPress Managed — deny PHP execution inside wp-content/uploads/
location ~* ^/wp-content/uploads/.*\.(php|phtml|php3|php4|php5|php7|php8|phar)$ {
deny all;
return 403;
}Copy the snippet
Click Copy in the panel’s Nginx section, or paste from the block above.
Add it inside your server block
Edit your site’s Nginx config — usually in /etc/nginx/sites-available/ or your hosting-provider’s equivalent. Paste the snippet inside the server { … } block for the site (not at the top level of the file). Order doesn’t matter as long as it’s within the correct server block.
Test the config, then reload
Run nginx -t to validate the config for syntax errors — do not reload if this fails, the current config is still live. Once nginx -t reports OK, reload with nginx -s reload or systemctl reload nginx. No downtime — reload is graceful.
Verify the block is active
Drop a test PHP file into uploads and try to access it. From the shell: echo ‘<?php echo "pwned";’ > wp-content/uploads/test.php and then curl -sI https://your-site.com/wp-content/uploads/test.php. Response should be HTTP/1.1 403. Delete the test file after.
The Apache and Nginx blocks are semantically identical. The Apache version uses <FilesMatch> inside <IfModule> guards so it works on both mod_authz_core (Apache 2.4+) and the older Order/Deny syntax (Apache 2.2). The Nginx version uses a location block matching the same file-extension list with case-insensitive matching (~*).
The AllowOverride Canary Probe
Writing an .htaccess file is only half the protection — the server has to actually honor it. On Apache, that requires AllowOverride to be set to All or FileInfo in your vhost config (or somewhere higher in the config tree). Hosts that set AllowOverride None for performance ignore every .htaccess on the site, silently.
After the block is written, GuardPress performs a canary probe:
Write a probe file
A tiny PHP file with a random 32-character token is written to wp-content/uploads/.guardpress-htaccess-test.php. Its only job is to echo the token.
Fetch the URL via wp_remote_get
GuardPress requests the file over HTTPS from the same server. If .htaccess is honored, the response is 403 (or 404, some hosts serve the alternate). If .htaccess is ignored, PHP-FPM runs the file and the response body contains the token.
Delete the probe and report the result
The probe file is always deleted (in a finally-style pattern — it’s removed even if the request errors). The panel then shows the probe result: honored (green) or not-honored with the specific reason (red banner).
The .htaccess file is on disk but your server config is ignoring it. Options: contact your host and ask them to set AllowOverride All (or at minimum FileInfo) for the site’s docroot; move to a host that honors .htaccess; or switch to Nginx and use the snippet above. Do not treat “the file exists” as protection when the canary has told you otherwise.
Disabling or Rescanning
The panel keeps two buttons live while the block is enabled: Rescan and Disable.
- Rescan re-runs the pre-enable scan against the current state of
uploads/. Useful if you just installed a plugin and want to see if it dropped anything new. It doesn’t change the block state. - Disable strips the managed block out of
.htaccessand leaves any other content in the file intact. If the file becomes empty after the strip, GuardPress deletes it entirely rather than leaving a zero-byte artefact. Same effect via WP-CLI:wp guardpress hardening uploads-php-block disable.
The block is also removed automatically when you deactivate GuardPress or uninstall it — no orphaned .htaccess rules left behind.
WP-CLI and REST Reference
Everything the panel does is available via WP-CLI and REST for scripted deployments across a fleet of sites.
WP-CLI
wp guardpress hardening uploads-php-block status wp guardpress hardening uploads-php-block scan wp guardpress hardening uploads-php-block enable [--force] [--exclude=<path>[,<path>…]] wp guardpress hardening uploads-php-block disable
status returns a human-readable summary or JSON via --format=json. scan lists files that would be flagged during enable (without actually enabling). enable --force skips the pre-enable-scan gate — use for scripted rollout on sites you’ve already vetted. --exclude takes a comma-separated list of absolute paths inside uploads/ to whitelist so a specific plugin’s working directory keeps executable.
REST
GET /guardpress/v1/hardening/uploads-php-block/statusPOST /guardpress/v1/hardening/uploads-php-block/enable— body acceptsexcluded_paths(array) andforce(boolean)POST /guardpress/v1/hardening/uploads-php-block/disable
All REST endpoints require the standard WordPress admin cookie + nonce — permission callback is manage_options on single-site and manage_network_options on multisite.
Still Stuck? Email Priority Support
If the panel doesn’t behave the way this article describes, or you’re on Nginx and want a second pair of eyes on the config before you reload, email support@royalplugins.com. Priority email support is included with your GuardPress Pro license; typical response time is within 24 hours.
Information to include in your email
- GuardPress version from WP Admin → Plugins (must be 1.6.41 or later for this feature)
- WordPress version from WP Admin → Updates
- Web server and version if you know it (Apache 2.4, Nginx, LiteSpeed, etc.). The status panel reports what GuardPress detected; if that doesn’t match your host’s actual server, mention it.
- What the pre-enable scan flagged — screenshot of the yellow warning box, or the list of files if there are only a few
- Canary probe result — “honored” or the specific reason string it reported
- Any admin-notice message that appeared after clicking Enable
- Security Hardening bundle — the other hardening toggles (disable file editing, hide WP version, disable directory browsing, XML-RPC block).
- Audit log completeness — role changes, sensitive-option writes, and wp-config.php touches now appear in the audit log.
- Attacker-behavior email alerts — new-admin-created, admin-role-granted, plugin-activated, plugin-deactivated, and admin-email-changed events.