How WordPress Uses Salts and Why You Should Not Rotate Them: A Technical Deep-Dive

| in


It’s difficult to find a topic in WordPress security with more published misinformation than WordPress salts.

Nobody seems to have an idea how WordPress core actually uses them & unfortunately, even the core documentation is partially misleading
(EDIT: we’ll probably open a PR to clarify them).

Below are some vastly incorrect statements about WordPress salts that we found with a quick Google search.

Salts make it much harder for hackers to steal user passwords.

WordPress Salts are random pieces of data used to enhance the security of passwords stored in the website’s database.

Salts need to be periodically rotated for better security from Brute Force attacks.

WordPress Salts or security keys are strings of random characters, used by WordPress to encrypt your username and password.

If you come across similar statements, you should consider finding alternative sources for your security information as you’re either reading SEO-optimized crap articles, or the quality assurance is severely lacking.

To prove our claims here, we’ll use the WordPress core codebase as the single (and only) source of truth.

So, What are WordPress Salts?

Definition

The opening line of the documentation for the wp_salt function hits the nail on the head.

Returns a salt to add to hashes.

It’s that simple, but unfortunately the docs then derail and there’s a significant drift between the actual code and the docs.

Our definition is the following:

WordPress Salts are random strings, stored in the wp-config.php file, that can be used as keys for cryptographic operations such as keyed hashing (HMAC) or encryption.

They can be accessed via the wp_salt function.

How does the wp_salt function work?

The wp_salt function is one of the most convoluted functions in WordPress Core, as a result of keeping backwards compatibility with ancient WordPress versions (we’re talking v2.7, released on December 10th, 2008).

It’s best to understand its behavior by looking at the two main use cases:

1.) Calling wp_salt with one of the inbuilt “schemes”

TL;DR Calling wp_salt with one of auth/secure_auth/logged_in/nonce as the argument returns a random string that is derived from constants in the wp-config.php file.

You’ve probably come across some variations of these constants that are defined in your wp-config.php file during the WordPress setup.

define('AUTH_KEY', 'long-random-string-1');
define('AUTH_SALT', 'long-random-string-2');

define('SECURE_AUTH_KEY', 'long-random-string-3');
define('SECURE_AUTH_SALT', 'long-random-string-4');

define('LOGGED_IN_KEY', 'long-random-string-5');
define('LOGGED_IN_SALT', 'long-random-string-6');

define('NONCE_KEY', 'long-random-string-7');
define('NONCE_SALT', 'long-random-string-8');

These eight constants are called the “WordPress salts” or sometimes “WordPress security keys”.

They can be accessed by calling wp_salt.

function wp_salt( $scheme = 'auth' ) {}

There are four pairs of “keys” and “salts”, each making up one of the “official” schemes (we explain their usages here):

  1. auth: AUTH_KEY and AUTH_SALT
  2. secure_auth: SECURE_AUTH_KEY and SECURE_AUTH_SALT
  3. logged_in: LOGGED_IN_KEY and LOGGED_IN_SALT
  4. nonce: NONCE_KEY and NONCE_SALT

Depending on the value of the scheme argument, different values will be returned by the wp_salt function. However, the returned value is always the combination of the “key” and the “salt”.

For example, the below code:

<?php

echo wp_salt('auth');
echo wp_salt('secure_auth');
echo wp_salt('nonce');
echo wp_salt('logged_in');

would output:

long-random-string-1long-random-string-2
long-random-string-3long-random-string-4
long-random-string-5long-random-string-6
long-random-string-7long-random-string-8

It’s not entirely clear to us why there’s always a “key” and a “salt” since they’re just combined.

Without giving it too much thought, there might just as well be one “salt” (random string) for each of the different schemes.

This could be a situation similar to Chesterton’s Fence, where nobody knows why it was done this way initially, so nobody dares to change it.

2.) Calling wp_salt with a custom “schemes”

TL;DR Calling wp_salt with a custom “scheme” as an argument returns a random string that’s derived from either the SECRET_KEY constant (if defined in the wp-config.php), or the secret_key option in the database.

If no secret_key option exists, it will be generated.

It’s possible to call wp_salt with any argument. In that case, the following code branch is run:

// CUT FOR BREVITY

if (defined('SECRET_KEY') && SECRET_KEY && empty($duplicated_keys[SECRET_KEY])) {
    $values['key'] = SECRET_KEY;
}

// CUT FOR BREVITY

if ( ! $values['key']) {
    $values['key'] = get_site_option('secret_key');
    if ( ! $values['key']) {
        $values['key'] = wp_generate_password(64, true, true);
        update_site_option('secret_key', $values['key']);
    }
}

$values['salt'] = hash_hmac('md5', $scheme, $values['key']);

$cached_salts[ $scheme ] = $values['key'] . $values['salt'];

return apply_filters( 'salt', $cached_salts[ $scheme ], $scheme );

First, the code determines a hash key, either using a SECRET_KEY constant defined (usually) in the wp-config.php file (Line 3-5), or by saving (and using) a random string in the wp_options table (Line 9-15).

This hash key is used to calculate the md5 HMAC of the passed custom scheme (Line 17).

Finally, the return value is the combination of the hash key and the md5 HMAC (Line 19-21).

For example, the following code:

<?php

add_action('plugins_loaded', function () {
    define('SECRET_KEY', 'calvin');
    
    echo wp_salt('snicco_scheme');
});

outputs:

calvin9b0337d74263ba450b3b6727440040b6
  • hash key: calvin
  • HMAC: 9b0337d74263ba450b3b6727440040b6

This behavior is largely undocumented. We’re listing it here for completeness.

Where does WordPress Use Salts

We now established that broadly speaking, calling the wp_salt function returns a random string that can be used for further cryptographic procedures.

Surprisingly, wp_salt is only used in one place – inside the wp_hash function, which serves as a wrapper function to create md5-HMAC hashes.

function wp_hash( $data, $scheme = 'auth' ) {
	$salt = wp_salt( $scheme );

	return hash_hmac( 'md5', $data, $salt );
}

wp_hash is used in a couple of places inside the WordPress core codebase, and we’ll only focus on the most security-relevant usage in this deep-dive.

For completeness’s sake, the function is also used in relation to:

However, by far, the most important usage of the wp_hash function is with WordPress authentication cookies & WordPress nonces.

Authentication cookies

We have written an entire deep-dive on how WordPress uses Authentication cookies and sessions.

TL;DR The wp_hash function is used to create, and validate, the cryptographic signature of the WordPress auth cookies.

The scheme that’s used is “auth” or “secure_auth” , depending on whether the site is using HTTPS.

Nonces

In addition, the wp_hash function is used to create cryptographically secure nonces in the wp_create_nonce and wp_verify_nonce functions.

We won’t explain the concept of nonces in this deep-dive, please refer to the docs on WordPress Nonces.

Both functions call wp_hash with the “nonce” scheme to create signatures during the nonce creation and validation.

We’ve highlighted the relevant linens in the functions below:

function wp_create_nonce( $action = -1 ) {
    $user = wp_get_current_user();
    $uid  = (int) $user->ID;
    if ( ! $uid ) {
        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
    }
    
    $token = wp_get_session_token();
    $i     = wp_nonce_tick( $action );
    
    return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
}
function wp_verify_nonce( $nonce, $action = -1 ) {
    $nonce = (string) $nonce;
    $user  = wp_get_current_user();
    $uid   = (int) $user->ID;
    if ( ! $uid ) {
        $uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
    }
    
    if ( empty( $nonce ) ) {
        return false;
    }
    
    $token = wp_get_session_token();
    $i     = wp_nonce_tick( $action );
    
    // Nonce generated 0-12 hours ago.
    $expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 1;
    }
    
    // Nonce generated 12-24 hours ago.
    $expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }
    
    do_action( 'wp_verify_nonce_failed', $nonce, $action, $user, $token );
    
    // Invalid nonce.
    return false;
}

That’s it. There are no further usages of WordPress Salts in the WordPress Core codebase. Which brings us to…

Busting WordPress Salts Myths

Myth: WordPress salts are used for password hashing

Let’s revisit some statements from our introduction.

Salts make it much harder for hackers to steal user passwords.

WordPress Salts are random pieces of data used to enhance the security of passwords stored in the website’s database.

WordPress Salts or security keys are strings of random characters, used by WordPress to encrypt your username and password. The strings are used to hash your login credentials.

There’s zero proof of that anywhere in WordPress core that would support these statements.

A simple check of the WordPress hashing code will reveal this.

For password hashing, WordPress uses the wp_hash_password function, which, by default, delegates to the open-source phpass library that is bundled with core.

function wp_hash_password( $password ) {
	global $wp_hasher;

	if ( empty( $wp_hasher ) ) {
		require_once ABSPATH . WPINC . '/class-phpass.php';
		// By default, use the portable hash from phpass.
		$wp_hasher = new PasswordHash( 8, true );
	}

	return $wp_hasher->HashPassword( trim( $password ) );
}

phpass is a third-party dependency of WordPress, and as such, it obviously does not use WordPress-specific code (WordPress Salts) anywhere.

It’s true that passwords are salted. But not with “WordPress salts”. Salting is a cryptographic concept that’s not unique to WordPress.

If you don’t believe us, perform this simple test for yourself:

  1. Create a new user on a WordPress test site.
  2. Log in as that user.
  3. Log out.
  4. Change all of your “salts” in the wp-config.php file.
  5. Log in again with the same password.

Step 5 would fail if the “WordPress salts” had anything to do with password hashing – But they don’t have anything to do with password hashing or storage, which is why you can still log in.

All of this confusion stems from not reading the wp_salt docs correctly, or bothering to cross-check them against the code.

Salting passwords helps against tools which has stored hashed values of common dictionary strings. The added values makes it harder to crack.

Source

The above statement is (generally) correct if referring to the general concept of salting. However, it doesn’t say that “WordPress Salts” are used for password hashing.

To avoid further confusion, the above should be removed from the docs.

But that’s not an excuse for security vendor after security vendor spreading this much misinformation at scale.

Myth: WordPress salts protect against brute-force attacks

Another perpetrated myth is that WordPress Salts protect against Brute-Force attacks.

Salts need to be periodically rotated for better security from Brute Force attacks.

This has no basis in reality and probably comes from misunderstanding or blindly copying outdated documented from this page which says that:

A secret key makes your site harder to successfully attack by adding random elements to the password.

In simple terms, a secret key is a password with elements that make it harder to generate enough options to break through your security barriers.

As we already proved above, there is no relation at all between WordPress Salts and the hashing of user passwords.

Myth: WordPress salts encrypt user information in cookies

WordPress Salts are used to secure [encrypt] usernames and passwords that are stored in browser cookies

We’ve explained in great detail how WordPress creates authentication cookies and proved that passwords are never stored in browser cookies (why would they? We don’t even have access to plaintext passwords).

Instead, WordPress uses characters 8-12 of the password hash (which was not created using WordPress salts) as parts of the auth cookie generation.

The username is always in the authentication cookies as plaintext and is never encrypted.

Should You Ever Rotate Your WordPress Salts?

Rotate WordPress Salts

TL;DR No, you should never (!) rotate your salts unless you’re 110% sure that you have been hacked.

We already established that, calling the wp_salt function returns a random string that can be used for further cryptographic procedures.

This generally means creating signatures (keyed hashing) or encryption.

If you’re only using stock WordPress, without plugins, you could rotate your salts without permanent damage because WordPress core does not use them for persistent data.

As we have already shown, they’re used for auth cookie signatures & nonces. Both are of a temporary nature.

Rotating the WordPress salts would invalidate all session cookies (thus, logging all users out the next time they visit the site) and nonces (meaning that cached forms likely can’t be submitted anymore).

However, If you throw plugins into the mix, everything will eventually explode.

Many plugins use the WordPress salts for encrypting their data. If you change your salts, decryption of the data will be impossible and it’s irrecoverably lost.

It’s debatable whether plugins should use the core salts, but since there have been no clear guidelines, a huge amount of them do, and this won’t change anytime soon.

Image 1

A quick search on WPDirectory reveals hundreds of plugins that use the wp_salt function for all sorts of different purposes.

It’s impossible for a non-developer to track and determine if they can safely rotate their salts.

We can give you two concrete examples from our own experience.

2FA plugins:

After our research on vulnerabilities in popular 2FA plugins, many plugins started encrypting TOTP secrets with one of the WordPress salts.

Suppose you have 500 users with 2FA enabled before you rotate your salts.

Now you rotate them (to achieve what again?).

Congrats, you have now DOS’d your own site because none of your users can log in anymore.

Their 2FA secrets can’t be decrypted anymore and all of them have to call you for support.

API Keys:

More and more plugins are starting to encrypt API keys they need to function in the database.

This is good.

However, since there’s no “standard way” for plugins to derive an encryption key, they have to use WordPress’s salts (or nudge their users to edit the wp-config.php file to add custom constants).

One example is the free FluentSMTP plugin (which we use on this site).

All sensitive settings are encrypted in the database using one of the WordPress salts by default.

If you rotate your salts, your site will not be able to send emails anymore.

These are just two concrete examples from our experience. There are undoubtably many more ways rotating salts will break your plugins that use them.

“But it’s an easy way to log out all users”

Sure, you could also change all your user’s password hashes in PhpMyAdmin – That would also log them out.

Depending on your plugin setup, the consequences will be comparable.

If you need to log out all users, you can run the following query (via PhpMyAdmin or other DB management tools)

DELETE FROM wp_usermeta WHERE meta_key = 'session_tokens'

Alternatively, if you’re using Fortress, you can log out all users via WP-CLI.

wp snicco/fortress session:destroy-all

“But it helps against session hijacking”

No, rotating your WordPress Salts does not help you against session hijacking (We’ll prove this in a future article in much greater detail).

At least not unless you plan to rotate your WordPress every couple minutes (which will log out everyone every couple minutes).

If you’re so inclined, create a frequent Cron job for the wp config shuffle-salts command on a site with frequent user interactions and see how long it takes for them to revolt.

It’s worth repeating:

You should never (!) rotate your salts unless you’re 110% sure that you have been hacked.