The misunderstood WordPress nonces

Nonces are probably one of the most misunderstood security features in WordPress. They create more problems than they solve when misused.

Cross-site request forgery (CSRF)

CSRF attacks (or sea-surf for those in the know) exploit the privileges of an innocent end-user to submit forged requests.

Indeed, all requests by authenticated users for a given domain will likely include cookies used by this domain. That’s why attackers impersonate those users to make their malicious crafted requests appear genuine and legitimate.

For example, if I know someone’s email and I’ve good reasons to believe he’s connected to a website powered by WordPress and Woocommerce (e.g., subscription). I could send this person a fake email with a link to a discount that is supposed to expire in three hours.

The user would click on the malicious link and go to a fake website with a tricked web form (hence the word “cross-site”). This way, I could add some scripts that send POST requests to the targeted website (the real one) on submit and do many damages on the user’s behalf (hence the word “forgery”).

What are nonces?

Nonces are numbers used once. More pragmatically, they are one-time tokens stored on the server and passed to the client and then sent back to the server to verify requests.

The goal is to validate the user’s requests and ensure nobody’s trying to act on the user’s behalf.

Nonces are generated with salt keys and other parameters such as timestamps, so hackers can neither replicate nor copy them. Once the request has been validated, the nonce is invalidated, and you need a new one to send the same request.

Without nonces, there’s not much you can do to stop CSRF attacks. However, it should be noted that “SameSite” cookie flags can help mitigate them (as an additional layer), as browser support is now excellent.

How WordPress implements nonces

In WordPress, the code looks like this:

<form method="post">
   <!-- some inputs here ... -->
   <?php wp_nonce_field( 'name_of_my_action', 'name_of_nonce_field' ); ?>
</form>

It adds hidden inputs with specific names and values, and the validation part can be:

if (
    ! isset( $_POST['name_of_nonce_field'] )
    || ! wp_verify_nonce( $_POST['name_of_nonce_field'], 'name_of_my_action' )
) {
    check_admin_referer( 'name_of_my_action', 'name_of_nonce_field' );
    // process form data
} 

Source: WordPress theme handbook - Using Nonces

The big problem is WordPress nonces are not used once! Nonces remain valid 12 hours by default and more in particular conditions. It’s called the tick cycle:

// 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;
}

Source: WordPress core files - /wp-includes/pluggable.php

The only thing that can invalidate the nonce during that valid period is when the user logs out, as a new session token is generated when the user logs in again.

It’s a critical point that many developers skip because they just don’t know it.

WordPress nonces are tied to a user ID

WordPress nonces are unique to each user’s session:

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();

    return substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
}

Otherwise, any logged-in user could easily run actions for another user.

Besides, it would be much easier for a hacker to replicate them.

Secure AJAX calls

AJAX is great for improving user’s experience and prevent useless page reloading. I’m not a big fan of WordPress AJAX but it makes the implementation easier for developers.

However, they often forget to add nonces, while it can prevent AJAX attacks. By using wp_localize_script(), you can pass a nonce to the Javascript and sent it back to the server for validation.

WordPress has a specific function to validate nonce in AJAX calls. It’s called check_ajax_referer().

Don’t misuse WordPress nonces

I’ve seen this statement more than once before:

use nonces everywhere, fight against hackers

🚨 While it’s undoubtedly helpful in the back-end, be careful with the front-end. For example, it’s a common cause of cache issues when nonces are directly used in front-end forms for non-logged-in users.

Never forget the user’s permissions

Nonces are not the ultimate protection. The official documentation is unequivocal about it:

always assume that nonces can be compromised

Serious problems can happen when you neglect the user’s permission.

WordPress manages users with roles and capabilities. You should use specific functions to check those permissions in addition, as nonces do not check user’s permissions at all.

Use current_user_can():

// if the user can manage options then
if ( current_user_can( 'manage_options' ) ) {
    // do some action
}

Source: WordPress core reference ) - current_user_can()

Wrap up

WordPress nonces provide an additional layer of security but on no account should you rely exclusively on them to prevent CSRF attacks and secure your forms.

WordPress implementation can be tricky for developers, leading to misleading impressions of safety and, thus, security concerns.

Photo by Morgane Perraud on Unsplash