WordPress Plugins
Free Tools
Pricing Blog Case Studies Switch to Royal Plugin Graveyard Support My Account Cart
Support / GuardPress / Uploads PHP Execution Block Setup

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.

What the panel looks like

Open GuardPress → Settings → Hardening → Uploads PHP Execution Block to see the status card, pre-enable scan, and Enable button.

Uploads PHP Execution Block panel showing DISABLED status, detected server type, .htaccess path, and a collapsible list of safe-to-block stub files with plugin attribution (SiteVault, FormForge Pro, Royal Affiliate Pro, Royal Forms Pro)

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.

Why it’s off by default

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.

Force-enable option

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.

What the classifier can’t catch

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).

If the canary says the block is not being honored

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.

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

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
Related GuardPress hardening topics