WordPress must be able to write media files to `wp-content/uploads`, but files stored there should never need to execute as PHP. This article explains how to block PHP-like files using a central Apache configuration, with `.htaccess` as an additional safety net. It also compares the advantages and limitations of both methods, explains `Options -ExecCGI`, and shows how to test the protection. This hardening measure does not replace patching, but it can disrupt the common attack chain in which an upload vulnerability is turned into an executable web shell.
I currently maintain eight WordPress blogs, handling almost everything myself—from server configuration and system upgrades to routine backups and security hardening. Over the past few years, I have encountered several relatively minor security incidents. Fortunately, they were detected and handled promptly, and none caused any serious damage.
Investigating, reviewing and documenting each incident has been a valuable opportunity to learn and improve. These experiences have also taught me that server security is never truly a one-off task, and years of stable operation should never become a reason for complacency. Promptly updating WordPress and its plugins, managing secrets carefully, tightening file permissions, reviewing logs and backups, and disabling PHP execution in upload directories may seem like small measures, but they can interrupt an attack chain at a critical moment. Security is ultimately an ongoing discipline that requires constant attention and improvement.
Hardening WordPress: Disable PHP Execution in `wp-content/uploads`
A normal WordPress installation needs to write media files into wp-content/uploads. That convenience creates an important security boundary: the directory must be writable, but files stored there should never need to execute as PHP.
If a vulnerable plugin, theme or custom upload endpoint allows an attacker to place a PHP file in the uploads directory, the attacker may try to request that file through the browser. If Apache passes it to PHP, a file-upload vulnerability can become remote code execution—a web shell running with the web server’s permissions.
Blocking PHP execution in uploads does not fix the original vulnerability, but it can break this common attack chain. It is a small, low-risk defence-in-depth measure that every WordPress server should consider alongside prompt updates, least-privilege file permissions, backups and monitoring. WordPress’s own hardening guidance likewise treats security as a layered process rather than a single setting.
Method 1: A central Apache configuration
On my server, multiple WordPress sites live below /var/www, so I created the following configuration file:
/etc/apache2/conf-available/wordpress-uploads-no-php.conf
I placed this configuration inside it:
<DirectoryMatch "^/var/www/(?:[^/]+/)*wp-content/uploads(?:/|$)">
<FilesMatch "(?i)\.(?:php[0-9]*|phtml|phar)$">
Require all denied
</FilesMatch>
</DirectoryMatch>
The outer DirectoryMatch limits the rule to WordPress upload directories under /var/www. It also covers the usual year-and-month subdirectories below uploads.
The inner FilesMatch checks filenames case-insensitively and denies direct HTTP access to extensions such as:
.php.php5,.php7,.php8and other numeric PHP suffixes.phtml.phar
Require all denied is the decisive instruction: matching files cannot be retrieved through Apache. Apache documents the use of DirectoryMatch, FilesMatch and Require all denied for filesystem-scoped access control.
Enable and validate the configuration:
a2enconf wordpress-uploads-no-php
apache2ctl configtest
systemctl reload apache2
Do not skip configtest. Apache should print:
Syntax OK
Should I add Options -ExecCGI?
It is reasonable as an additional precaution:
<DirectoryMatch "^/var/www/(?:[^/]+/)*wp-content/uploads(?:/|$)">
Options -ExecCGI
<FilesMatch "(?i)\.(?:php[0-9]*|phtml|phar)$">
Require all denied
</FilesMatch>
</DirectoryMatch>
Options -ExecCGI prevents execution through Apache’s CGI mechanism. However, it does not by itself disable PHP-FPM, mod_php or every other possible handler. It is therefore extra hardening, not a replacement for the FilesMatch denial.
Apache’s CGI documentation shows that ExecCGI is the option that permits CGI execution.
Method 2: An .htaccess safety net
I also placed the following in each site’s wp-content/uploads/.htaccess:
<FilesMatch "(?i)\.(?:php[0-9]*|phtml|phar)(?:\.|$)">
Require all denied
</FilesMatch>
There is no DirectoryMatch here because an .htaccess file already applies to its own directory and its descendants.
Apache reads .htaccess files during requests, and the server’s AllowOverride settings determine which directives they are allowed to contain. This behaviour is described in Apache’s configuration-file documentation.
This slightly stricter expression also rejects suspicious double-extension names such as:
shell.php.jpg
backdoor.phtml.png
payload.phar.gif
A correctly configured PHP handler should not execute .php.jpg, but an uploads directory has no legitimate reason to contain filenames with embedded executable extensions. The trade-off is that such a file will be blocked even if it is actually an image.
If adding this .htaccess file causes a 500 Internal Server Error, inspect Apache’s error log:
tail -n 50 /var/log/apache2/error.log
A 500 response usually means that the directive is not permitted by the applicable AllowOverride or AllowOverrideList setting.
Do not enable AllowOverride All merely to make this safety net work. If you control the server, use the central Apache configuration instead.
Central configuration versus .htaccess
| Method | Advantages | Disadvantages |
|---|---|---|
| Central Apache configuration | One rule can protect every site; controlled by root; independent of AllowOverride; harder for a compromised WordPress process to modify; generally cleaner and faster |
Requires server administration access; changes require validation and reload; the path expression must match the actual site layout |
.htaccess inside uploads |
Easy per-site deployment; useful on shared hosting; takes effect without an Apache reload; provides a second copy of the rule | Works only when overrides are allowed; must be maintained for every site; adds per-request filesystem checks; may be deleted or altered if the application can write the directory |
Apache notes that when overrides are enabled it may look for .htaccess files along the requested filesystem path, so the central configuration is preferable for performance when you control the server.
For that reason, I treat the server-level rule as the real security boundary and .htaccess only as a secondary safety net.
Test the protection instead of assuming it works
Create a temporary test file in one site’s uploads directory:
printf '%s\n' '<?php echo "SHOULD-NOT-EXECUTE";' \
> /var/www/example.com/wp-content/uploads/security-test.php
Request it:
curl -i https://example.com/wp-content/uploads/security-test.php
The expected response is:
403 Forbidden
Most importantly, the text SHOULD-NOT-EXECUTE must never appear.
Also check that a normal existing JPEG or PNG in the same directory is still accessible:
curl -I https://example.com/wp-content/uploads/path/to/existing-image.jpg
Remove the test file immediately:
rm /var/www/example.com/wp-content/uploads/security-test.php
Repeat the test after meaningful Apache, PHP-FPM or virtual-host configuration changes. Security controls that have not been tested are assumptions, not controls.
What this protection does not do
This measure deliberately has a narrow purpose. It does not:
- patch WordPress, themes or plugins;
- prevent a vulnerable application from writing a malicious file;
- stop malicious code that is already executing through another PHP file;
- prevent a plugin from loading a file locally with PHP’s
includeorrequire; - replace malware scanning, log review, backups or proper filesystem permissions.
It blocks direct web access to dangerous filenames in a directory intended only for static media. That may be enough to stop a basic uploaded web shell, but it is not proof that the site is clean after a compromise.
Final recommendation
If you administer Apache yourself, use the central configuration as the primary control. Add Options -ExecCGI as harmless defence-in-depth where no CGI execution is legitimately required.
Keep the .htaccess rule only as an additional layer when overrides are already enabled—never weaken the server configuration just to support it.
Most importantly, continue updating WordPress core, themes and plugins promptly. Disabling execution in uploads reduces the blast radius of one attack path; it does not remove the vulnerability that allowed the upload in the first place.
Wordpress is King!
- Hardening Wordpress: Disable PHP Execution in wp-content/uploads/
- Wordpress Short Code Function Showing Relevant Posts
- Wordpress: How to Output Full Text in the Feed?
- How to Add Adsense Ads to bbPress Forum?
- Adding Two Short Code Functions to Wordpress: Top Posts By Number of Comments and Top Posts by Ratings
- Adding a Short Code Function to Include Any PHP or HTML Files in the WordPress Posts or Pages
- Free Wordpress T-Shirt by Submitting a Bug Report
- 5 Things to Look For in a WordPress Hosting Provider
- Using the Regular Expression to Replace External Links in Wordpress for SEO purposes
- How to Add a ShortCode to Include a PHP File in Wordpress?
- Hardening WordPress: Disable PHP Execution in `wp-content/uploads`
WordPress must be able to write media files to `wp-content/uploads`, but files stored there should never need to execute as… - How to Use WordPress Search to Display Relevant Posts?
How to Create a Lightweight Related Posts Shortcode in WordPress Display Relevant Posts in WordPress Without a Plugin A Simple… - WordPress: How to Output Full Text in the Feed?
How to Make WordPress RSS /feed Output Full Text WordPress RSS Full Content Output Guide How to Show Full Text… - PHP7.x is deprecated by WordPress: Why You Should Upgrade to PHP8.x?
Time to Say Goodbye to PHP 7.4 — My Blogs Are Finally Getting an Upgrade After Years on PHP 7.4,… - Adding Two Short Code Functions to WordPress: Top Posts By Number of Comments and Top Posts by Ratings
The wordpress shortcode is a great feature that we can easily add via the add_shortcode function. This post introduces two…
–EOF (The Ultimate Computing & Technology Blog) —
Last Post: Turning a Dusty Mini PC into a Home Server: My NUC-8’s Second Life
