Block access to PHP files on your WordPress site with Nginx

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.

22 Comments

  1. Perfect, thanks Bjorn. This was helpful to me!

    I’ve been blocking requests to /xmlrpc.php for years but my error logs were showing some fatal errors because of bots accessing certain plugin files directly. Not anymore.

    You’re probably aware of this but I just stumbled upon this GitHub repository full of WordPress related NGINX rules: https://github.com/pothi/WordPress-Nginx/ .

    PS. Your name just KEEPS popping-up when searching for devops stuff. Your upgrade guide to PHP7 was much appreciated & I was happy to see you use 2 of my plugins on this site. :)

    1. Thank you, Danny – for both your comment and plugins!
      I did not know about that repo, but will certainly look through it :-)

  2. I wonder if it is worth adding a bit to this article showing where you would put these rules if you: A. Wanted it to have effect over all sites non your server or B. Just have effect for one site. For newbies!

  3. I think there are there is a “\” missing in front of the “.php” ending in the location rules.

  4. Thanks for the helpful tips! On the restriction to wp-includes, we’ve found that this can break the content editor within wp-admin (TinyMCE). Our solution was to add another location statement within the wp-includes (deny) statement that specifically allowed the wp-tinymce.php file. Hope that helps.

    1. Hi Paul,

      this is my fix for tinymce nginx:

      location ~* /wp-includes/js/tinymce/wp-tinymce.php {
      allow all;
      # arrange for PHP to kick in here, otherwhise you will get a download of the PHP source file ..
      }

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

  5. Doesn’t work for me. I am putting all these in the server block {} but i can still manually execute a php file as a logged out user if I know its file address.

  6. Another important file to protect is nginx.conf, in case you have it.
    It is generated by TotalCache plugin (and maybe others), and it might have information a hacker can be interested on.

    1. Thank you for notifying me, Dan!
      I’m pretty sure I didn’t want them there too. There was an issue with the syntax highlighting plugin. Fixed it now.

  7. Thanks for this article!

    I put the blocks in my nginx configuration.
    How can I test if these blocks actually work?

  8. I am trying to allow ips but this not work for me. Can any one check what is wrong with my nginx code.

    location = /xmlrpc.php {
    allow 13.32.0.0/15;
    allow 13.52.0.0/16;
    allow 13.54.0.0/15;
    deny all;
    access_log off;
    log_not_found off;
    }

    Thanks

  9. Thank you Bjørn for this still useful post in 2018!

    I’ve implemented this and made some changes for the rules to be able to setup in one line instead of having multiple of the denial location blocks (cleaning up a bit of the conf file.

    If anyone is interested:


    location ~* (/wp-content/.*\.php|/wp-includes/.*\.php|/xmlrpc\.php$|/(?:uploads|files)/.*\.php$) {
    deny all;
    access_log off;
    log_not_found off;
    }

  10. Hello Jeroen,

    your last Code from February is small and minify so bether :)

    i have add the variable return 444; for close the Connection.

    thanks
    Manuel

  11. Hey Bjorn,
    For somebody who has no idea how to mess with the command line and uses Nginx (on a Linode VPS + Managed GridPane panel) – is there any one, single lean plugin that you recommend that can do both – brute force login limit and XMLRPC block+pingback removal protection? I’m trying to limit the use of bloat and extra plugins. If you don’t know of one plugin that can do both – what two separate plugins do you recommend?

    Thanks!

Comments are closed.