In your WordPress site, there are directories that include PHP files that visitors should never be able to access directly. They are only there for WordPress to function as an application that runs on your server. But because of WordPress’ directory and file structure, they are kind of accessible to the public. All of them are meant to be part of a larger application – WordPress, that is – and should not cause any harm if called directly – that we know. Some of the files execute some code even when ran standalone. An attacker might know of a clever way to make that code run in an unexpected manner, causing harm. To be on the safe side, we should deny access to all these PHP files from the outside world. Since we block access to them in our Nginx configuration, PHP will still run them as usual and WordPress will work just fine.

The directories we need to protect

The wp-includes directory will always be named that. The directories for uploads, themes and plugins are by default subfolders within wp-content ( media, wp-content/themes and wp-content/plugins respectively), but may be moved elsewhere. The same goes for the wp-content directory itself.

Oh, and the access_log and log_not_found statements in the examples on this page are here just to not fill up our logs with crap requests. If you want to log the requests, remove the statements accordingly.

Block PHP files in the includes directory

This location should always be the same.

location ~* /wp-includes/.*.php$ {
	deny all;
	access_log off;
	log_not_found off;
}

Block PHP files in the content directory

This directory is by default /wp-content, but you can easily define it to be elsewhere, e.g. by simply setting the WP_CONTENT_DIR/ WP_CONTENT_URL constants, so adjust the config accordingly.

location ~* /wp-content/.*.php$ {
	deny all;
	access_log off;
	log_not_found off;
}

Block PHP files in the uploads directory

The uploads directory may or may not be a subdirectory of wp-content and may or may not have been renamed to something entirely different. Adjust the config accordingly.

location ~* /(?:uploads|files)/.*.php$ {
	deny all;
	access_log off;
	log_not_found off;
}

The files part is for the default multisite/network path. You can remove it if you want to, but it doesn’t cause any harm to stay in there.

Plugin and theme directories

I think most people leave the themes and plugins directories as subdirectories within the content directory, but they can also easily be moved to somewhere else. You define the constant pair WP_PLUGIN_DIR/ WP_PLUGIN_URL for plugins, and use the function register_theme_directory() for themes to do so. Add similar location blocks for plugins and themes if you have moved them out of the content directory as well.

If you haven’t tampered with the plugin or theme locations, skip this part.

If you moved the plugins dir to e.g. /modules:

location ~* /modules/.*.php$ {
	deny all;
	access_log off;
	log_not_found off;
}

If you moved the themes dir to e.g. /skins:

location ~* /skins/.*.php$ {
	deny all;
	access_log off;
	log_not_found off;
}

If you use both, you should be able to combine them in the same way as we did with the upload folders above.

Block access to xmlrpc.php

If you don’t need XML-RPC (you most likely don’t – you only do if you use Jetpack or the WordPress phone app), you can block requests to it. Even though some people claim XML-RPC isn’t the culprit to the well-known attacks using it (notably people involved in services that use XML-RPC), it is beyond any doubt that you simply can not be attacked through XML-RPC if you block it entirely. All XML-RPC requests are routed through the file xmlrpc.php:

location = /xmlrpc.php {
	deny all;
	access_log off;
	log_not_found off;
}

You have now reduced the public surface of your application similar to standing sideways in a gunfight: Your vulnerable surface that an attacker can hit is now much smaller.