Learn to secure your WordPress site with this simple, yet effective, tip!
Securing the WordPress uploads folder is important. In many hacked WordPress sites, a PHP backdoor is found within the WP_CONTENT_DIR/uploads
directory. Often because this is the location where uploads are placed automatically. From the backdoor within wp-content/uploads
other backdoors are uploaded to various locations, and scripts are injected with malware.
Please note that the techniques shown below will only partially secure your WordPress website, but not fully! You must take other security measures as well. For example, you can use a .htaccess as a web application firewall (WAF), to block out some vulnerabilities based on their request characteristics. Or you can use a HTTP blacklist to block out IP addresses known to abuse websites.
Block PHP execution in Windows Server IIS and Linux Apache
As said, it is recommended to deny PHP execution in folders like WordPress wp-content/uploads. A lot of malware is uploaded to that folder and used as and entry point. Additional malware and PHP backdoors are uploaded from there, so disable PHP execution in wp-content/uploads! Let's start.
Windows Server IIS web.config
The easiest method in IIS to disable PHP execution in a folder is to create a web.config file with an accessPolicy for handlers. Such an accessPolicy tells IIS what a PHP hander is allowed to do: execute, read, or read/write. Using this technique you disable the execution of PHP completely for that particular folder.
You'll find more information about IIS Handlers in IIS' documentation.
In IIS Manager:
- In IIS Manager, click through to your web site
- double click "Handler Mappings"
- search for PHP and double click
- click Request Restrictions
- click the tab Access
- Set Script to Read
Did you know you can use Windows Server File Server Resource Manager File Screens to block the upload of vulnerable WordPress plugins? Read all about it in Deny vulnerable WordPress plugins using Windows Server File Server Resource Manager's File Screens.
Using a web.config file directly
Copy and paste the following XML into a new file and save it as web.config:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<!-- deny PHP execution per IIS accessPolicy -->
<handlers accessPolicy="Read"/>
</system.webServer>
</configuration>
You can use the following PowerShell or appcmd.exe commands to configure an accessPolicy for handlers. Substitute example.com with your website name:
# PowerShell, WebAdministration
Set-WebConfiguration "/system.webServer/handlers/@accessPolicy" `
-value "Read" `
-PSPath "MACHINE/WEBROOT/APPHOST/example.com/wp-content/uploads"
# AppCmd.exe
appcmd.exe set config "example.com/wp-content/uploads" /section:handlers /accessPolicy:Read
By setting an accessPolicy to "Read", you basically disable all configured handlers, not just PHP.
PHP function to disable PHP execution in wp-content/uploads in Windows Server
As always, the PHP code is provided AS-IS. The PHP code to write a web.config file comes partially from the WordPress functions iis7_add_rewrite_rule() and saveDomDocument(), both found in wp-admin/includes/misc.php
.
It shouldn't be too hard for you to wrap this in a WordPress plugin:
<?php
function disable_script_execution() {
$is_iis7 = true;
$path = WP_CONTENT_DIR . '/uploads';
$filename = 'web.config';
if ( $is_iis7 ) {
if ( ! file_exists( $path . '/' . $filename ) ) {
$fp = fopen( $path . '/' . $filename, 'w' );
fwrite( $fp, '<configuration/>' );
fclose( $fp );
}
$formatxml = PHP_EOL;
$formatxml = " <handlers accessPolicy=\"Read\" />";
$formatxml .= PHP_EOL;
$doc = new DOMDocument();
$doc->preserveWhiteSpace = true;
if ( $doc->load( $path . '/' . $filename) === false ) {
return false;
}
$xpath = new DOMXPath( $doc );
$read_accesspolicy = $xpath->query( '/configuration/system.webServer/handlers[starts-with(@accessPolicy,\'Read\')]' );
if ( $read_accesspolicy->length > 0 ) {
return true;
}
$xmlnodes = $xpath->query( '/configuration/system.webServer/handlers' );
if ( $xmlnodes->length > 0 ) {
$handlers_node = $xmlnodes->item(0);
}
else {
$handlers_node = $doc->createElement( 'handlers' );
$xmlnodes = $xpath->query( '/configuration/system.webServer' );
if ( $xmlnodes->length > 0 ) {
$system_webServer_node = $xmlnodes->item(0);
$handler_fragment = $doc->createDocumentFragment();
$handler_fragment->appendXML( $formatxml );
$system_webServer_node->appendChild( $handler_fragment );
}
else {
$system_webServer_node = $doc->createElement( 'system.webServer' );
$handler_fragment = $doc->createDocumentFragment();
$handler_fragment->appendXML( $formatxml );
$system_webServer_node->appendChild( $handler_fragment );
$xmlnodes = $xpath->query( '/configuration' );
if ( $xmlnodes->length > 0 ) {
$config_node = $xmlnodes->item(0);
$config_node->appendChild( $system_webServer_node );
}
else {
$config_node = $doc->createElement( 'configuration' );
$doc->appendChild( $config_node );
$config_node->appendChild( $system_webServer_node );
}
}
}
$rule_fragment = $doc->createDocumentFragment();
$rule_fragment->appendXML( $formatxml );
$handlers_node->appendChild( $rule_fragment );
$doc->encoding = "UTF-8";
$doc->formatOutput = true;
saveDomDocument( $doc, $path .'/'. $filename );
return true;
}
}
Re-enable PHP script execution
If you want to re-enable PHP script execution, you can simply delete the web.config file from the wp-content/uploads directory. Or you can programmatic enable PHP by deleting the <handlers accessPolicy="Read"/>
line:
<?php
function enable_script_execution() {
$is_iis7 = true;
$path = WP_CONTENT_DIR . '/uploads';
$filename = 'web.config';
if ( $is_iis7 ) {
if ( ! file_exists( $path . '/' . $filename ) ) {
return true;
}
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false;
if ( $doc->load( $path . '/' . $filename ) === false ) {
return false;
}
$xpath = new DOMXPath($doc);
$handlers = $xpath->query( '/configuration/system.webServer/handlers[contains(@accessPolicy,\'Read\')]' );
if ( $handlers->length > 0 ) {
$child = $handlers->item(0);
$parent = $child->parentNode;
$parent->removeChild( $child );
$doc->formatOutput = true;
saveDomDocument( $doc, $path .'/'. $filename );
}
}
return true;
}
?>
Disable PHP execution in a selected directory on Linux Apache (3 methods)
There are several methods to disable PHP execution in Apache using a .htaccess file. I'll mention 3 of them here in short:
SetHandler: use SetHandler default-handler
to send the file using the default_handler()
, which is the handler used by default to handle static content. It just renders the PHP as text. So be careful with this.
In your wp-content/uploads/.htaccess
file, add:
# use <Files *> if appropriate
<Files *.php>
SetHandler default-handler
</Files>
Mod_authz_core .htaccess access control: you can deny access to PHP files using Apache access control in mod_authz_core module. It'll send a 403 response for requests to *.php files. Add to your .htaccess file:
<Files *.php>
# Apache 2.2
<IfModule !mod_authz_core.c>
Order Deny,Allow
Deny from all
</IfModule>
# Apache 2.4.6+
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
</Files>
Please note the IfModule
condition whether the module mod_authz_core.c is available. This is important as explained in the above linked article.
Mod_Rewrite: rewrite and forbid requests to *.php files. It's easy to use mod_rewrite to deny access to *.php files by rewriting those requests and sending a F|forbidden flag:
RewriteEngine On
RewriteRule ^.*.php$ - [F,L]
Why did I start this article with
Preferably without the use of a security plugin
Because most security plugins are easy to defeat, especially once you have access to the file system the website resides on (e.g WordPress admin, FTP access, and so on). The post Defeating WordPress Security Plugins (Revisited) by @TheXC3LL has some nice red teaming thoughts about this.
By disabling PHP in the uploads directory, your WordPress site is a little bit more secured. The, somewhat older, Smashing Magazine post Common WordPress Malware Infections gives great insight in common WordPress (core, theme, plugin) vulnerabilities and how they're abused.