Uploads hardening guide

Disable PHP execution in WordPress uploads, safely.

Uploads should contain media, not executable code. On a suitable Apache single-site install, a narrow rule in the uploads directory can limit the impact of a malicious PHP upload.

01Scope the rule to uploads.

Place the hardening file in wp-content/uploads/, not in the WordPress root.

02It is an Apache control.

Nginx and other servers need an equivalent server-level configuration, not this .htaccess file.

03Check multisite first.

Some multisite setups route files through ms-files.php; WordPress warns not to apply this technique there.

What this protects — and what it does not

The rule denies requests for PHP-like files inside the uploads directory. That matters because an attacker who can upload a script needs the web server to execute it before it becomes a working backdoor. Preventing execution in a media-only directory removes one useful path from upload to server-side code execution.

It is defense in depth, not a substitute for updates, least-privilege accounts, a secure upload workflow, and a web application firewall. It also does not scan a file, remove a compromised plugin, or stop PHP elsewhere in WordPress.

1 A file reaches uploads2 A request targets a PHP-like file3 Apache applies the directory rule4 Execution is denied

Decide whether this approach fits your site

EnvironmentUse this .htaccess rule?Why
Apache single siteUsually, after a backup and test.The file can be isolated to wp-content/uploads/, keeping the change narrow.
Nginx or managed platformNo — ask for the server equivalent.Nginx does not read .htaccess files.
Multisite using ms-files.phpDo not add it blindly.WordPress documents an exception because that routing can require PHP handling for uploaded files.
Do not block PHP in the WordPress root.

WordPress itself needs PHP to run. The protection belongs only in a media-only uploads directory, and only after you have confirmed the server and network setup support it.

On Nginx, the equivalent lives in the server block rather than a directory file — Nginx never reads .htaccess:

nginx.conf or site config, inside the server block location ~* /wp-content/uploads/.*\.php$ { deny all; }

Apply and test the narrow rule

The rule goes in its own file — wp-content/uploads/.htaccess — not in the root file that holds the permalink rules.

wp-content/uploads/.htaccess
# Never execute PHP that was uploaded as media.
<FilesMatch "\.(php|phtml|phar|php[0-9])$">
  <IfModule mod_authz_core.c>
    Require all denied
  </IfModule>
  <IfModule !mod_authz_core.c>
    Order allow,deny
    Deny from all
  </IfModule>
</FilesMatch>

Both access syntaxes are there on purpose: Apache 2.4 reads the mod_authz_core branch, and Apache 2.2 reads the other one. Emitting only the 2.4 form returns a 500 error on a 2.2 server instead of silently doing nothing, which is a worse failure than the one the rule is meant to prevent.

nginx — the same protection, in the server block
# nginx: inside the site's server block, before the PHP location.
location ~* /wp-content/uploads/.*\.(php|phtml|phar|php[0-9])$ {
    deny all;
}

nginx never reads .htaccess, so an uploads file there protects nothing; the location block above has to go into the server configuration, above whatever block passes .php to PHP-FPM. LiteSpeed running in Apache-compatibility mode honours the .htaccess version, but OpenLiteSpeed does not — check which one your host runs before assuming the file is doing anything.

  1. 01
    Confirm Apache and the uploads path.

    Verify that the server reads .htaccess and that this WordPress installation stores media in its normal wp-content/uploads/ directory.

  2. 02
    Check the multisite exception.

    If this is a network, confirm whether it uses ms-files.php before adding the rule. Use the host or WordPress documentation when uncertain.

  3. 03
    Generate the uploads-only block.

    In the local .htaccess generator, enable “Block PHP execution in uploads” and copy the separate uploads output.

  4. 04
    Create wp-content/uploads/.htaccess.

    Place the copied output in that directory, not in the root file that contains WordPress permalink rules.

  5. 05
    Verify normal media still works.

    Upload an image, open its public URL, and check that legitimate front-end and admin media requests still load. Roll back the small file if they do not.

Quick answers

Does blocking PHP in uploads break normal WordPress images?

It should not: images, PDFs, and other media are not PHP. Still, test a normal upload and its public URL after making the change because hosts and plugins can customize media handling.

Should I put this rule in the root .htaccess file?

No. The rule should be contained in wp-content/uploads/.htaccess so it applies only to a media directory. Blocking PHP at the root would break WordPress.

Can I use this on WordPress multisite?

Not without checking its upload routing. WordPress specifically cautions against this method for multisite configurations that use ms-files.php.

Where does this rule sit relative to the main WordPress rewrite block?

Separately — it lives in its own wp-content/uploads/.htaccess file, not in the root file covered in the default .htaccess guide. The two never need to interact.

Primary reference: WordPress hardening documentation.