This blog is running WordPress. I use the packages from Fedora. I also have a cronjob that auto-updates the packages as soon as they are available. This reduces the potential window of opportunity a hacker could access a vulnerability on my blog. You should also ready my postings on my PHP, Apache, and IPTables configurations.
Since the package can be auto-updated, I have a script that runs every hour to re-run the security modifications I’ve made to wordpress. If/when WordPress is auto-patched, my script will redo the security fixes. Here are the commands I have scripted.
- By default, /usr/share/wordpress is owned by the wordpress user account. This is required if you want to add new plugins or use it as a shared account to update static content. I don’t need either of those, so I chown’d it to root. I wouldn’t be able to download plugins through the WordPress dashboard anyways since my IPTables firewall prohibits the Apache webserver from connecting to anything. See my posting on my IPTables configuration for more details.
chown -R root /usr/share/wordpress
- By default, /etc/wordpress is world-readable. It contains a file called wp-config.php, which contains the database username/password along with the auth keys and salts. That is horrible default ownership/permissions. I chgrp’d to apache and removed world-readable permissions on the directory.
chgrp apache /etc/wordpress
chmod 750 /etc/wordpress
- I am running the “twentytwelve” theme, which displays information about WordPress on the blog pages. I removed that by adding a site-info tag to the CSS.
grep site-info /usr/share/wordpress/wp-content/themes/twentytwelve/style.css >/dev/null 2>&1 || echo “
.site-info {
display: none;
}” >> /usr/share/wordpress/wp-content/themes/twentytwelve/style.css
I restricted access to the admin area and the login page. If I allowed for users to sign-up to my site, I would allow the login page, but restrict the admin area.
<Location /mkblog/wp-admin>
Order deny,allow
Allow from 192.168.1.0/24
Deny from all
require all granted
</Location>
<Location /mkblog/wp-login.php>
Order deny,allow
Allow from 192.168.1.0/24
Deny from all
require all granted
</Location>
I installed the Better WP Security plugin and followed some of what it recommended.
First, I allowed it to remove the default admin user, which also removed user id 1.
In the “tweaks” section, I enabled the following:
Protect Files – Prevent public access to readme.html, readme.txt, wp-config.php, install.php, wp-includes, and .htaccess. These files can give away important information on your site and serve no purpose to the public once WordPress has been successfully installed.
Disable Directory Browsing – Prevents users from seeing a list of files in a directory when no index file is present.
Filter Request Methods – Filter out hits with the trace, delete, or track request methods.
Filter Suspicious Query Strings – Filter out suspicious query strings in the URL. These are very often signs of someone trying to gain access to your site but some plugins and themes can also be blocked.
Remove WordPress Generator Meta Tag – Removes the meta tag from your sites header. This process hides version information from a potential attacker making it more difficult to determine vulnerabilities.
Remove wlwmanifest header – Removes the Windows Live Writer header. This is not needed if you do not use Windows Live Writer or other blogging clients that rely on this file.
Remove EditURI header – Removes the RSD (Really Simple Discovery) header. If you don’t integrate your blog with external XML-RPC services such as Flickr then the “RSD” function is pretty much useless to you.
Hide Theme Update Notifications – Hides theme update notifications from users who cannot update themes. Please note that this only makes a difference in multi-site installations.
Hide Plugin Update Notifications – Hides plugin update notifications from users who cannot update themes. Please note that this only makes a difference in multi-site installations.
Hide Core Update Notifications – Hides core update notifications from users who cannot update themes. Please note that this only makes a difference in multi-site installations.
Enable strong password enforcement – Enforce strong passwords for all users with at least the role specified below.
Remove WordPress Login Error Messages – Prevents error messages from being displayed to a user upon a failed login attempt.
Display random version number to all non-administrative users – Displays a random version number to visitors who are not logged in at all points where version number must be used and removes the version completely from where it can.
I setup a rewrite rule to redirect 404 pages back to the browser. When an attacker scans my site for vulnerable software, every page that does not exist is redirected back to them. This doesn’t necessarily stop the attack, but it’s fun to mess with the hackers:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteCond %{REQUEST_FILENAME} !/wp-admin
RewriteRule .* http://127.0.0.1%{REQUEST_URI}?GIVE_IT_UP=LOL [R,L,QSA]
The !-f matches if the requested file does not exist.
The !-d matches if the requested directory does not exist.
The !-l matches if the requested file could be a link and does not exist.
When all conditions are met, the rewrite rule sends a 302 redirect with a Location header that tells the browser that the requested file was moved to their own computer.
The Better WP Security plugin tries to make a few changes to the .htaccess file, which I disabled through chown’ing the directory to root. Instead, I took what it wanted to put in the .htaccess file and put it directly into the directory stanza for wordpress in my http config file in /etc/. Below are the additions. I changed the recommended rejected rule to a redirect to localhost.
RewriteRule ^wp-admin/includes/ – [F,L]
RewriteRule !^wp-includes/ – [S=3]
RewriteCond %{SCRIPT_FILENAME} !^(.*)wp-includes/ms-files.php
RewriteRule ^wp-includes/[^/]+\.php$ – [F,L]
RewriteRule ^wp-includes/js/tinymce/langs/.+\.php – [F,L]
RewriteRule ^wp-includes/theme-compat/ – [F,L]
RewriteCond %{REQUEST_METHOD} ^(TRACE|DELETE|TRACK) [NC]
#RewriteRule ^(.*)$ – [F,L]
RewriteRule .* http://127.0.0.1%{REQUEST_URI}?GIVE_IT_UP=LOL [R,L,QSA]
RewriteCond %{REQUEST_METHOD} POST
RewriteCond %{REQUEST_URI} ^(.*)wp-comments-post\.php*
RewriteCond %{HTTP_REFERER} !^(.*).*
RewriteCond %{HTTP_REFERER} !^http://jetpack\.wordpress\.com/jetpack-comment/ [OR]
RewriteCond %{HTTP_USER_AGENT} ^$
#RewriteRule ^(.*)$ – [F,L]
RewriteRule .* http://127.0.0.1%{REQUEST_URI}?GIVE_IT_UP=LOL [R,L,QSA]
RewriteCond %{QUERY_STRING} \.\.\/ [NC,OR]
RewriteCond %{QUERY_STRING} ^.*\.(bash|git|hg|log|svn|swp|cvs) [NC,OR]
RewriteCond %{QUERY_STRING} etc/passwd [NC,OR]
RewriteCond %{QUERY_STRING} boot\.ini [NC,OR]
RewriteCond %{QUERY_STRING} ftp\: [NC,OR]
RewriteCond %{QUERY_STRING} http\: [NC,OR]
RewriteCond %{QUERY_STRING} https\: [NC,OR]
RewriteCond %{QUERY_STRING} (\<|%3C).*script.*(\>|%3E) [NC,OR]
RewriteCond %{QUERY_STRING} mosConfig_[a-zA-Z_]{1,21}(=|%3D) [NC,OR]
RewriteCond %{QUERY_STRING} base64_encode.*\(.*\) [NC,OR]
RewriteCond %{QUERY_STRING} ^.*(\[|\]|\(|\)|<|>|ê|”|;|\?|\*|=$).* [NC,OR]
RewriteCond %{QUERY_STRING} ^.*("|'|<|>|\|{||).* [NC,OR]
RewriteCond %{QUERY_STRING} ^.*(%24&x).* [NC,OR]
RewriteCond %{QUERY_STRING} ^.*(127\.0).* [NC,OR]
RewriteCond %{QUERY_STRING} ^.*(globals|encode|localhost|loopback).* [NC,OR]
RewriteCond %{QUERY_STRING} ^.*(request|select|concat|insert|union|declare).* [NC]
RewriteCond %{QUERY_STRING} !^loggedout=true
RewriteCond %{QUERY_STRING} !^action=rp
RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in_.*$
RewriteCond %{HTTP_REFERER} !^http://maps\.googleapis\.com(.*)$
#RewriteRule ^(.*)$ – [F,L]
RewriteRule .* http://127.0.0.1%{REQUEST_URI}?GIVE_IT_UP=LOL [R,L,QSA]
<files readme.html>
Order allow,deny
Deny from all
</files>
<files readme.txt>
Order allow,deny
Deny from all
</files>
<files install.php>
Order allow,deny
Deny from all
</files>
<files wp-config.php>
Order allow,deny
Deny from all
</files>