Security

How to Block PHP Execution in Upload Folders on WordPress and Drupal

A clay 3D render of a small red toy race car resting on two wooden blocks on a storeroom shelf, its four wheels removed and stacked neatly beside it, so the car is kept safe but cannot drive, a visual for how to block PHP execution in WordPress and Drupal upload folders so stored files can sit on the server without ever running

To block PHP execution in an upload folder, tell the web server to refuse PHP files requested from folders your CMS can write to. It is one of the cheapest server rules you can add. It turns many upload flaws from code execution into a stored file that does nothing.

Most of the upload and form exploits we have covered end the same way. A PHP file lands in a writable folder, the attacker requests it by URL, and the server runs it. This piece covers which folders to lock on WordPress and Drupal and the Apache and nginx rules to use. It also covers the Drupal setup that ships with core, how to test the result, and what the rule does not fix.

The short version: lock wp-content/uploads on WordPress and sites/default/files on Drupal. On Apache, put the rule in the main server config if you can. Apache ignores .htaccess files when AllowOverride is off. On nginx, .htaccess does nothing, so you need a location rule placed before the PHP handler. Then test with a harmless file.

Why upload folders need a rule to block PHP execution

Upload folders hold images, PDFs and form attachments. Nothing in them should ever run as code. But the web server does not know that unless you say so.

That gap is why our Gravity Forms file upload flaw post said the impact depended on the server. The same pattern ran through the September WordPress form upload exploits and the Ghostscript PDF upload flaw. Patching the plugin closes the door the attacker used. A rule to block PHP execution limits what the next flaw can do.

WordPress 7.1.4 shipped on October 28. The WordPress release post lists a fix for "A weakness in the WordPress upgrade process which can lead to remote code execution during the upgrade window" among others. The post gives no technical detail, so we cannot say whether a folder rule would have stopped it. We covered the release in our WordPress 7.1.4 post. It is still a reminder that writable folders are where trouble starts.

WordPress’s own hardening guide, last updated October 14, 2026, covers file permissions and DISALLOW_FILE_EDIT. It says that setting "will not prevent an attacker from uploading malicious files to your site, but might stop some attacks" and stops there. The guide does not cover PHP in the uploads folder. That is the gap this post fills.

Which folders to lock

Start with the folders that take user uploads. Add other writable folders only after testing on staging, because some plugins serve their own files from them.

CMS Folder Notes
WordPress wp-content/uploads Media library and most form uploads. Multisite uploads sit inside it too.
WordPress Other writable folders such as wp-content/upgrade or plugin cache folders Candidates only. Test first, since some plugins expect to serve files there.
Drupal sites/default/files (public files) Drupal expects a protective .htaccess here on Apache
Drupal Private files and temp directories Keep these outside the web root so they cannot be requested at all

First, find out how your server runs PHP

The right rule depends on the server. There are three common setups:

  • Apache with mod_php. PHP runs inside Apache. The php_flag engine off setting works here.
  • Apache with PHP-FPM. Apache hands .php requests to a separate PHP process. php_flag does nothing.
  • nginx with PHP-FPM. nginx never reads .htaccess files. Only server config rules count.

Two Apache facts trip people up. Apache’s .htaccess tutorial says the default value of AllowOverride is None. With that setting, ".htaccess files are completely ignored" by the server. The same page prefers the main config for performance and security, when you have access to it.

PHP’s own Apache configuration page says the engine setting "is really only useful in the Apache module version of PHP" (mod_php). So an .htaccess that only says php_flag engine off gives no protection on a PHP-FPM server.

If you are on a managed host, ask which setup it uses before you change anything. Our guide to choosing a managed WordPress host lists other questions worth asking at the same time.

How to block PHP execution on Apache

If you can edit the server or virtual host config, use a <Directory> block. Adjust the path to your site.

<Directory "/var/www/example.org/wp-content/uploads">
    <FilesMatch "(?i)\.(php[0-9]?|phar|phtml)(\.|$)">
        Require all denied
    </FilesMatch>
</Directory>

Apache’s mod_authz_core documentation says Require all denied means "Access is denied unconditionally" for that section. The pattern is ours. It matches .php, numbered variants like .php7, .phar and .phtml, in any letter case.

Why match a PHP extension in the middle of a name? Apache’s mod_mime documentation says filenames "may have multiple extensions" and each one is checked. On a server mapped with AddHandler, a file named photo.php.jpg could still reach PHP. The (\.|$) part catches that case.

If you can only use .htaccess, put the inner FilesMatch block in an .htaccess file in the uploads folder. Two conditions apply. Require needs AllowOverride AuthConfig on that folder, per the same Apache page. And the web server should not be able to write the file. Otherwise an attacker who can upload could replace it.

On mod_php servers, you can add a second layer inside that file:

<IfModule mod_php.c>
    php_flag engine off
</IfModule>

Drupal’s files directory protection

Drupal ships this protection for Apache. Drupal’s guide on preventing execution of untrusted PHP was last updated November 16, 2025. It says the files directory relies on a .htaccess file. The web server must not be able to write to it. The Drupal 11 version contains these directives.

Options -Indexes -ExecCGI -Includes -MultiViews
SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006
<Files *>
  SetHandler Drupal_Security_Do_Not_Remove_See_SA_2013_003
</Files>
<IfModule mod_php.c>
  php_flag engine off
</IfModule>

Drupal describes the SetHandler lines as a catch-all handler that keeps scripts in the folder from running. Check that the file exists in sites/default/files on every Drupal site you manage, and that it matches.

The same guide warns about one PHP-FPM setup. On Apache 2.4, some servers send .php requests to PHP-FPM with a ProxyPassMatch line. Drupal says that line overrides most other settings and largely cancels the .htaccess protection. Drupal’s guide says "Avoid doing this" about that line. Its alternative works the same way but lets .htaccess files override it.

<FilesMatch \.php$>
   SetHandler "proxy:fcgi://127.0.0.1:9000"
</FilesMatch>

If your server config uses ProxyPassMatch for PHP, folder rules on Apache will not hold. Fix that first.

Drupal 10 reaches end of life on December 9, 2026. Drupal’s release schedule confirms the date. After it, Drupal 10 gets no security fixes. Server rules like these will not make up for an unpatched core. Plan the move to Drupal 11 now.

Our Drupal 10 end-of-life guide covers the upgrade path. Our plain-language look at the Drupal Webform advisories shows why upload handling deserves this attention.

How to block PHP execution on nginx

nginx ignores .htaccess, so Drupal’s file and any WordPress uploads .htaccess do nothing there. WordPress’s nginx guide has a rule for this. It sits in the guide’s global restrictions file.

# Deny access to any files with a .php extension in the uploads directory
location ~* /(?:uploads|files)/.*\.php$ {
    deny all;
}

Order matters. The nginx location documentation says regex locations are checked in the order they appear. The search stops at the first match. If location ~ \.php$ comes first, PHP files in uploads go straight to PHP-FPM. Put the deny rule above it.

For Drupal on nginx, the same idea applies to the public files folder. The pattern below is ours, built from the nginx docs.

location ~* ^/sites/[^/]+/files/.*\.(php[0-9]?|phar|phtml)$ {
    deny all;
}

Two PHP-FPM settings add a backstop on any server. They help whether you block PHP execution on Apache or nginx. WordPress’s nginx guide notes you should have cgi.fix_pathinfo = 0 in php.ini. PHP’s FPM configuration page says security.limit_extensions defaults to .php .phar. It adds that you "should only limit FPM to .php extensions" to stop other extensions running code. Set it to .php unless something on the server needs more.

How to test that you block PHP execution

Test on staging first, then on production with a file that does nothing harmful.

  1. Over SFTP or SSH, create dm-php-test.php in the uploads folder.
  2. Put this single line in it: <?php echo 'php-' . 'ran';
  3. Request it in a browser or with curl -i https://example.org/wp-content/uploads/dm-php-test.php.
  4. Read the result. If the page shows php-ran, PHP executed and the rule is not working. A 403 error, a 404 error, or the raw source code means PHP did not run.
  5. Repeat with a copy named dm-php-test.php.jpg to check the double-extension case on Apache.
  6. Delete both files.

Then check normal uploads still work after you block PHP execution. Upload an image and a PDF, and confirm both display. If your forms plugin or page builder breaks, look for PHP files it serves from inside uploads.

What this rule does not fix

A rule to block PHP execution limits damage. It does not stop the upload itself, and it does not clean a site that is already compromised.

  • Attackers can still upload HTML or SVG files that run JavaScript in a visitor’s browser.
  • PHP already placed in plugin, theme or core folders still runs.
  • A flaw that lets an attacker include a file from PHP code can still load it.
  • Vulnerable plugins still need updates, and dead plugins still need removing.

Treat it as one layer. Keep core and plugins current. Keep upload folders free of anything executable. Review which plugins accept files from the public.

Frequently Asked Questions

What does it mean to block PHP execution in a folder?

It means the web server refuses to run PHP files requested from that folder. The files can still exist on disk, but a request for them returns an error or plain text instead of running code.

Will WordPress break if I block PHP execution in uploads?

Core WordPress does not need PHP to run from `wp-content/uploads`. Some plugins may serve files from there, so test on staging and check uploads, forms and page builders afterward.

Does Drupal already block PHP execution in its files folder?

On Apache, yes, through a `.htaccess` file in the public files directory. On nginx that file is ignored, so you need a server rule. Drupal also warns that a `ProxyPassMatch` setup for PHP-FPM can cancel the protection.

Why does my .htaccess rule have no effect?

Either the server is nginx, which ignores `.htaccess`, or Apache has `AllowOverride None` for that folder, which Apache says makes it skip `.htaccess` files entirely. Move the rule into the server config or ask your host.

Is php_flag engine off enough?

Only on Apache with mod_php. PHP’s documentation says the engine setting is really only useful in the Apache module version of PHP. Most current servers use PHP-FPM, where it does nothing.

Which file extensions should the rule cover?

At least `.php`, `.phar`, `.phtml` and numbered forms like `.php7`. On Apache, also catch PHP extensions in the middle of a name, such as `photo.php.jpg`, because Apache compares each extension in a filename.

Can I do this on a managed host?

Often the host already sets it at the platform level, and you may not be able to edit server config. Ask the host whether PHP is blocked in upload folders and test with the harmless file described above.

Does this replace updating plugins?

No. It limits what an upload flaw can do, but attackers can still upload other harmful file types or use flaws in code that already runs. Keep core and plugins updated.

What should I see when I test it?

A 403 or 404 error, or the PHP source shown as plain text. If the test file prints its message, PHP ran and the rule is not working.

Digital Matters

Security Desk