HowTo: Securing WordPress wp-content and wp-includes Directories

After some banging around in mod_rewrite, I sorted out how to prevent prying fingers from directly executing PHP modules that are contained within the wp-content and wp-includes directories.  With few exceptions, PHP modules in these directories should not be directly access by a browser client.

My quest started when I observed errors like the following in the website access log:

::1 - - [05/May/2011:11:45:58 -0500] "GET /wp-content/plugins/commentluv/commentluv.php HTTP/1.1" 500 -

This isn’t a script that should be getting executed directly and actually causes PHP errors if it is attempted, hence the “500” status.  There is potential for other scripts to be directly executed that would have more serious side effects, including the potential for exposing the site to exploits due to a poorly coded plugin. The WordPress Codex instructions for plugins includes a statement that plugins should include something like the following at the top of PHP modules to prevent such direct access to any internal functions:

// Protect from direct execution
if (!defined('WP_PLUGIN_DIR')) {
      header('Status: 403 Forbidden');
      header('HTTP/1.1 403 Forbidden');
exit();
}

The error I mentioned above in my server access log indicates that the script is not properly protecting itself.  Instead of relying on collective abilities of all plugin authors to protect their code, I decided to protect my site from this general form of attack.  My sites are now better secured even if the plugin author neglected to implement this security measure.

Credit where credit is due, a thread in the  WordPress Forums started me in the right direction to get these portions of WordPress locked down.  The rules I added to my .htaccess file are:

# Protect wp-content directory -- php modules should not be directly accessed
RewriteCond %{REQUEST_FILENAME} wp-content/.+\.(php|txt|pl)$ [NC]
# Cookies for Comments uses a PHP formatted CSS file
RewriteCond %{REQUEST_FILENAME} !cookies-for-comments/css\.php$ [NC]
RewriteRule .* - [F,NS,L]

# Protect wp-includes directory - don't allow access to php files
RewriteCond %{REQUEST_FILENAME} wp-includes/.+\.php$ [NC]
# Allow the multi-site uploaded files handling
RewriteCond %{REQUEST_FILENAME} !wp-includes/ms-files\.php$ [NC]
# js/tinymce has some php modules, following rule allows them
RewriteCond %{REQUEST_FILENAME} !wp-includes/js/tinymce/.+ [NC]
RewriteRule .* - [F,NS,L]

Placement of these rules in the .htaccess file is also very important!  If you are running multi-site, the rules must come before these lines or they will never be run:

RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule . index.php [L]

A few notes on my research and testing:

  • Forbidding access to .html files is not a good idea.  There are too many exceptions in plugins where .html files are directly accessed by the client browser.
  • The forum posting referenced above used %{THE_REQUEST} which is not appropriate for filtering based on the URI.  The value retrieved is not normalized and will contain any superfluous ‘/’ characters as well as ‘..’ path references so it is easy subverted as written.
  • The forum posting also specifically allowed any files under wp-includes/js to be delivered.  My preference is to be more precise in exception handling and after a bit of exploring I found that the only place in wp-includes/js that actually does use PHP modules requiring direct access is tinymce.  That rule was made more specific.
  • I added an exclusion for the ‘.pl’ type to wp-content as a precaution.  Just in case some plugin author out there happens to include some Perl files in their plugin.  This concept could be further expanded on, and perhaps even better would be to flip the tests from being exclusion based to inclusion based.  In other words, the only files in the directories that are allowed to be directly accessed are javascript, jpeg, gif, etc.  This structure would probably work well as a ModSecurity rule driven off a data file that includes the allowed files.
  • If you are running plugins that have PHP modules requiring direct access, additional rules like the one referencing the plugin cookies-for-comments would need to be created for those modules.
  • If you run multi-site, the rule for ms-files.php allows uploaded media to be accessed by the client browser.  I’d prefer to see ms-files.php in the parent directory as this is not an included file.

Posted by Ken

Toastmaster, Woodworker, Craft Beer Enthusiast and dutiful supporter of three demanding house cats. I'm also an experienced Software Engineering Manager with a demonstrated history of working in the Information Technology and Services industry. Skilled in Operating System Development, Unix, SPARC Servers, Wordpress and PHP.

Leave a Reply

Your email address will not be published. Required fields are marked *