Improving WordPress Password Security

partially transparent lock overlayed a world map with 1s and 0s
CC0 source: https://pixabay.com/en/cyber-security-hacker-security-3194286/

For those of you who know me, when I create a web application I usually do not even support passwords at all. It is my opinion that passwords are a prime example where a technological solution to a problem does not match well with the constraints of the human mind.

A good password is both unique and difficult to guess. For humans, that also means difficult to remember, and difficult to remember means that we either use the same password many places (so it is not unique), use something easy to remember like password123 or qwerty or letmein or fuckpasswords (so it is easy to guess), or we write it down somewhere, where a hacker can potentially find it.

I even once did some work for a company that used employee phone numbers for the passwords. Yeah, that got fixed.

With password generators and password managers it is a little less of a problem but on more than one occasion password managers have had vulnerabilities that allowed them to be compromised.

So my preference is to just not use passwords. The user enters their login name and a nonce is generated and stored in association with their session data. The user is then sent an e-mail with the nonce as a GET variable. They click the link, and are authenticated. Sure, it means anyone with access to the user’s e-mail account can authenticate, but the same is true with passwords because anyone with access to the user’s e-mail account can reset a password.

WordPress and Passwords

Unfortunately WordPress does not work that way, and while it probably is possible to write a plugin to give it that capability, it still has passwords so the security of passwords within WordPress needs to be done well.

This is from the WordPress pluggable.php file:

// If the hash is still md5...
if ( strlen($hash) <= 32 ) {
  $check = hash_equals( $hash, md5( $password ) );
  if ( $check && $user_id ) {
    // Rehash using new hash.
    wp_set_password($password, $user_id);
    $hash = wp_hash_password($password);
  }
  //[redacted comments]
  return apply_filters( 'check_password', $check, $password, $hash, $user_id );
}

We can see from that that at one point, WordPress used unsalted md5() for password hashes. That is really really really bad to do. And since WordPress was first released well after it was known to be insecure, it is an indication that someone who knows security was not involved in the initial releases.

To their credit, what the above code does is update the password hash to something better when it encounters a password hash created using md5().

So what they are they doing now?

From the file class-phpass.php it looks like they are implementing the Portable PHP password hashing framework. The home page of that site is not even accessible via a secure connection, which gives me some concern but it does not mean the code is bad. The code is obviously old but it is not necessarily bad.

That framework is kind of outdated, WordPress should at least be using the PHP native password_hash() and password_verify() functions on systems running PHP 5.5.0 or newer and only use that framework on older systems. Really though, versions of PHP older than PHP 5.6.x should not be used as they no longer receive even security updates, and PHP older than 7.1 should be highly discouraged.

Anyway, that framework uses either blowfish or extended DES if blowfish is not available, which in practice means they are using blowfish because PHP has supported blowfish for a very long time. All versions of PHP since 5.3.0 has blowfish and PHP prior to 5.3.0 often has blowfish. You would have to be using a really ancient insecure version of PHP to have the WordPress native password hashing fallback to Extended DES.

To be honest I can not criticize WordPress too much. What they are doing was probably one of the better solutions when they started doing it, but a lot of time has passed and now there is a much better way.

NaCL, Sodium, and Argon2id

Cryptography is hard. A lot of mistakes happen when implementing it, and those mistakes result in exploitable applications. This is why NaCL was created. With NaCL it is still possible to make implementation mistakes, but it is more difficult to make implementation mistakes, it exists to make quality cryptography easier for the non-cryptographer.

Sodium is a fork of NaCL intended to extend the usability even further. A wrapper to Sodium is readily available for PHP 7+ and is included by default with PHP 7.2+.

The Sodium library includes the Argon2id variant of Argon2 password hashing algorithm, which is currently considered by most to be best well-tested password hashing algorithm that currently exists.

Rather than wait for WordPress to implement Argon2 which likely will not happen for years, my Pluggable-Unplugged plugin gives the ability to start using it now.

It is not enabled by default when that plugin is installed because once you switch to it, turning it off means that any password created after it was enabled or users that have logged in after it was enabled will have to reset their passwords.

There are some implementation notes I would like to address.

Initial Enabling

When Argon2id password hashing is enabled, any new passwords created will use Argon2id password hashing from the start. However existing password hashes in the database will not be upgraded to Argon2id password hashing until the user logs in again. That is the only way to do it, the user’s password needs to be available to create the Argon2id hash, and the user’s password is only available when the user logs in with their username and password.

So if you look at the password hashes in the database after enabling Argon2id, you will likely see existing password hashes that are not Argon2id hashes. Don’t be alarmed or think it did not work, it just means those users have not logged in with their password since it was enabled. The very first time they do, their hash will be upgraded to an Argon2id hash.

Password Rehashing

When a user with an existing Argon2id hash logs in with their password, there is a 20% chance their password will be rehashed. Like all modern password algorithms, the number of rounds of hashing that is performed can be tuned. The more rounds of hashing required, the more difficult it is for a cracker who managed to get a database dump to brute force crack a password. However more rounds also means that it takes longer to verify a password.

The Sodium library sets some constants for how many rounds of hashing to do based upon the capabilities of your hardware. As your server hardware improves, the number of rounds needed increases.

By recreating the hash one out of every five times a user logs in with their password, existing hashes with fewer rounds of hashing will be updated after moving to better hardware. It does mean every now and then the login process will be a little slower for a user, but only 20% of time, and it is not likely to be noticed.

Optional Prehash

Some users will create really crappy passwords that are commonly used elsewhere. While I do plan to add a dictionary checker at some point to try and catch these very poor passwords, the bottom line is they will always happen. Users can get very creative in figuring out how to have a crappy password that passes automated tests.

When a crappy password is used, if an attacker gets a dump of the password hashes, the crappy passwords will be brute forced unless a prehash technique is used.

What a prehash does, it takes the user’s password and uses a salt in combination with the password to create a hash that is actually used as the password internally.

As long as the attacker does not have access to the salt, the attacker will not be able to use a stolen database dump to brute force weak passwords. The salt should therefore NOT be stored in the database, but should be a constant defined in the wp-config.php file.

The plugin looks to see if the constant PASSWORD_SALT is defined. If (and only if) it is defined:

if (defined('PASSWORD_SALT')) {
    // prehash the password
    $key = hash('sha256', PASSWORD_SALT, true);
    $raw = sodium_crypto_generichash($password, $key, 64);
    $password = base64_encode($raw);
    sodium_memzero($key);
    sodium_memzero($raw);
}

What that does, regardless of what the password actually is, it creates a 64 byte (512 bit) hash of the password that is unique to the key and the password, and base64 encodes the hash (just to prevent any possible bugs with null bytes). That 64 byte hash is then used as the password that is fed to Argon2id to be hashed with its algorithm.

Essentially what it does, it adds ‘artificial entropy’ to the password assuming the attacker does not know the salt so that simple passwords can not realistically be brute forced if the attacker gets a database dump. If the attacker knows the salt, then it does not prevent a brute force of weak passwords.

A common issue with WordPress plugins, they often are vulnerable to remote SQLi attacks that allow attackers to get a dump of the database, and due to the bad design of WordPress where everything is in one database, that means the password hashes are vulnerable to remote theft when a plugin has an SQLi vulnerability. The constants defined in wp-config.php however can not be stolen with a SQLi attack.

It is important to note that if you ever change the salt, which you should do if you have reason to believe it has been compromised, every user will have to do a password recovery before they can log in again. So you may not want to set that constant. That being said, I do, because I know users will create bad passwords.

Passwords that get an Argon2id hash before you have defined a salt will still work, but each time the user logs in, there will be a 20% chance their password is rehashed and the rehashed version will then have the added protection.

Salt Generation

I recommend generating the salt before activating Argon2id password hashing. That way every password hash generated benefits from the extra security from the start. Since the salt is turned into a 256 bit key, the salt should have at least 256 bits (16 bytes) of entropy. Might as well just use 64 bytes though. You can generate a 64 byte salt with the following code:

#!/usr/bin/env php
<?php
$raw = random_bytes(64);
$salt = str_shuffle(base64_encode($raw));

print sprintf("define(%s '%s');\n", str_pad("'PASSWORD_SALT',", 19, " "), $salt);
?>

The output of that command line PHP shell script can just be added to your wp-config.php file. It will do nothing when the Argon2id hashing from the pluggable-unplugged plugin is not enabled, but when you do enable Argon2id hashing, the passwords will be pre-hashed adding some protection against brute force of weak passwords.

Geeky Tech Addendum

Just to clarify, the optional salt that triggers a prehash is only for the prehash. Like any secure password hashing algorithm, Argon2id generates a unique nonce salt that is kept with the hash string when it generates a password hash. Salts for the actual password hash itself are unique, and that is important as it defends against so-called rainbow tables.

Earlier I used the term, in quotes, ‘artificial entropy’. That is a term I made up, though it is quite possible others have made up the same term and even used it the same way I do.

Artificial Entropy is a method of masking the fact that real entropy is not there. Masking lack of entropy is a topic that fascinates me. Really it is a form of obscurity and security by obscurity is dangerous, but obscurity can give resistance to attackers when proper security protocol such as strong passwords are not properly adhered to or when random number generators are not really as high quality as they should be.

Artificial Entropy is not a substitute for real entropy, it only sometimes obfuscates the fact that there is not as much real entropy as their should be. If the attacker does not know the salt, the attacker has to figure out the salt before they can use a brute force attack against stolen password hashes. With a quality salt, it will be very difficult though not impossible. If WordPress or a plugin has a vulnerability that allows the attacker to execute arbitrary code, the salt used is trivial to steal. That is why it is only ‘artificial’ entropy.

Educate your users and encourage them to use quality passwords, but use the prehash technique to help prevent account compromise when the users do not listen to you and create weak passwords anyway. It will happen, and if WordPress or a plugin you use has an SQLi vulnerability, the password hashes are very vulnerable to theft.

As long as the salt is not known, it will add at most 32 bytes of entropy to the entropy the password already has, as the salt is used to create a 32 byte key for a cryptographic hash function.

If the salt has less than 32 bytes of entropy (a poor salt) or the password already has 32 bytes of entropy (a really high quality password) then it will add less than 32 bytes of entropy.

The only time the prehash reduces entropy is when the password already has 64 bytes of entropy, which is extremely rare. The prehash is a 64 byte prehash so passwords that already have 64 bytes or more will not benefit, but it is extremely rare for such passwords to be used.

From the perspective of an attacker who has stolen the password hashes but does not have the salt, all passwords will have 64 bytes of entropy, which makes them virtually impossible to brute force.

Leave a Reply

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

Anonymity protected with AWM Pluggable Unplugged