WordPress Developer Blog

Understand and use WordPress nonces properly

What is a nonce?

In cryptography, a nonce is an arbitrary number that can be used just once in a cryptographic communication. It is often a random or pseudo-random number issued in an authentication protocol to ensure that old communications cannot be reused in replay attacks.

Wikipedia

What if I tell you that WordPress nonce is not a nonce?

Like some other things (I’m looking at you, cron jobs), nonces in WordPress are not the real nonces. 

Unlike traditional nonces, WordPress nonces can be used multiple times within their limited lifetime, and their default lifetime is anywhere between 12 hours plus 1 second and 24 hours. To measure the nonce lifespan, WordPress uses 12-hour periods since the Unix epoch (1 January 1970). Each period is “a tick”, and each nonce has two ticks to rock the world. 

The function that validates the nonce, wp_verify_nonce(), will return this tick number:

  • 1 for the first 12h of nonce’s life
  • 2 for the second 12h of nonce’s life
  • false for the nonce that’s not valid any more.   

The nonce will live a total of 24 hours only if it’s created at the very beginning of the tick. However, if it’s created at the very end of the tick, it can live just a second longer than 12 hours.

The actual function for determining the nonce’s lifespan is wp_nonce_tick(). It holds the filter nonce_life, which allows extenders to modify the length of the nonce’s life. As with any other powerful tool, this filter can and will break your site if used inappropriately

<?php
/**
 * Change the lifespan of a nonce to 4 hours.
 * 
 * @param int $lifespan Lifespan of nonces in seconds. Default 86,400 seconds, or one day.
 * 
 * @return float Float value rounded up to the next highest integer.
 */
add_filter( 'nonce_life', 'wporg_nonce_life' );

function wporg_nonce_life( $lifespan ) {
	return 4 * HOUR_IN_SECONDS;
}

Now, if you’re thinking that 24 hours is way too long for something that should be used only once, you’re right. Twenty-four hours is long enough for someone to steal your nonces. But fear not; there’s enough protection built into WordPress’ nonces system. 

Official WordPress documentation on Nonces slightly brushes against this idea: “the same nonce will be generated for a given user in a given context”. A given user is a keyword here. Nonces are unique to the session of the currently active user. Meaning they are valid only for the currently active user. Even if someone has your credentials AND your nonce, they need your session as well to be able to use it successfully.

When creating nonce, the wp_create_nonce() function takes the user ID and session token values. These are unique to the current user. Furthermore, it adds two more values to the mix and hash them at least twice. There are two rabbit holes you can follow from this function:

The point is it is secure, but you should never forget to run the current_user_can() check because the nonce is unaware of the user’s permissions.

<?php
/**
 * Protect nonce with current_user_can() check.
 */

// Build URL for deleting the user.
$url = add_query_arg(
	array(
		'action' => 'delete',
		'user'   => $user_id,
	),
	admin_url( 'users.php' )
);
// Add nonce to the URL.
$delete_user_url = wp_nonce_url( $url, 'delete-user', 'my_custom_nonce_name' );

// $delete_user_url URL could be sent to admin via email. When clicked it can lead to a template/page where checks 
// make sure current user can delete other users, verify nonce and then perform action. 
if ( current_user_can( 'delete_users' ) && 
    isset( $_GET[ 'my_custom_nonce_name' ] ) && 
    wp_verify_nonce( $_GET[ 'my_custom_nonce_name' ], 'delete-user' ) 
   ) {
	// delete user code here
}

When to use nonces?

If WordPress nonce is not an actual nonce and has so many things to keep in mind, what is it good for anyway?

Nonces are crucial for authorising HTTP requests to your site. The nonces’ purpose is to prevent malicious HTTP requests.

The most common malicious HTTP requests that can be prevented by using nonces are Cross-Site Request Forgery (CSRF) Attacks, including form submission attacks, unauthorised AJAX requests and various plugin and theme exploits. A few years ago, it was reported that some popular WordPress plugins have CSRF vulnerabilities. Take a look at some of them to understand how they can be performed if the code is not secure enough.

Using nonce in the backend application

Several functions with various purposes are available with the Nonces API.

Creating a nonce

A nonce can be created in several ways depending on the use case.

For a form

When you need a nonce for the form, wp_nonce_field() will create a ready-to-use hidden field for you and more. If you need it, you can have a referer field as well.

If you are building a custom form in the WordPress dashboard, you are likely building it as a part of your plugin settings. In this case, using the settings_fields() function is recommended, which will not only take care of the nonce field but will include additional useful hidden fields.

For an URL

When you need a nonce for the URL, you’re covered with wp_nonce_url(). If you have more arguments for the URL, and you most likely will, it is always advised to use add_query_arg(), as we did in the example for using the current_user_can() check with nonces. 

However, sometimes your second argument for add_query_arg() is an URL that already has arguments, and it may have been escaped with esc_url(). This way, you might end up with a useless URL due to the fact that wp_nonce_url() is escaping output with esc_html() (take a look at this example for a better understanding). This behaviour is known and reported but also not fixed because every attempt to fix it would break something else.

If your URL, besides escaping, needs to go through sprintf() as well, do take a look at this example of how to do it properly.

For whatever else

In any other context, creating a nonce is best done with the wp_create_nonce() function.

Looking through the WordPress core, there are various ways of using this function.

<?php
/**
 * Found in wp-login.php
 */
?>

<div class="admin-email__actions-secondary">
	<?php
		$remind_me_link = wp_login_url( $redirect_to );
		$remind_me_link = add_query_arg(
			array(
				'action'          => 'confirm_admin_email',
				'remind_me_later' => wp_create_nonce( 'remind_me_later_nonce' ),
			),
			$remind_me_link
		);
	?>
	<a href="<?php echo esc_url( $remind_me_link ); ?>"><?php _e( 'Remind me later' ); ?></a>
</div>
<?php
/**
 * Found in wp-admin/theme-install.php
 * https://github.com/WordPress/wordpress-develop/blob/6.2/src/wp-admin/theme-install.php#L231
 */
?>

<label for="wporg-username-input"><?php _e( 'Your WordPress.org username:' ); ?></label>
<input type="hidden" id="wporg-username-nonce" name="_wpnonce" value="<?php echo esc_attr( wp_create_nonce( $action ) ); ?>" />
<input type="search" id="wporg-username-input" value="<?php echo esc_attr( $user ); ?>" />
<input type="button" class="button favorites-form-submit" value="<?php esc_attr_e( 'Get Favorites' ); ?>" />

Set a JavaScript constant for theme activation is marked private (still) but you can find it here.

<?php
/**
 * Set a JavaScript constant for theme activation.
 *
 * Sets the JavaScript global WP_BLOCK_THEME_ACTIVATE_NONCE containing the nonce
 * required to activate a theme. For use within the site editor.
 *
 * @see https://github.com/WordPress/gutenberg/pull/41836.
 *
 * @since 6.3.0
 * @private
 */
function wp_block_theme_activate_nonce() {
	$nonce_handle = 'switch-theme_' . wp_get_theme_preview_path();
	?>
	<script type="text/javascript">
		window.WP_BLOCK_THEME_ACTIVATE_NONCE = <?php echo wp_json_encode( wp_create_nonce( $nonce_handle ) ); ?>;
	</script>
	<?php
}
<?php
/**
 * As found in rest_cookie_check_errors()
 * https://developer.wordpress.org/reference/functions/rest_cookie_check_errors/
 */

// Send a refreshed nonce in header.
rest_get_server()->send_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) );
<?php
/**
 * As found in _wp_dashboard_recent_comments_row()
 * https://developer.wordpress.org/reference/functions/_wp_dashboard_recent_comments_row/
 */

$del_nonce     = esc_html( '_wpnonce=' . wp_create_nonce( "delete-comment_$comment->comment_ID" ) );
$approve_nonce = esc_html( '_wpnonce=' . wp_create_nonce( "approve-comment_$comment->comment_ID" ) );

$approve_url   = esc_url( "comment.php?action=approvecomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$approve_nonce" );
$unapprove_url = esc_url( "comment.php?action=unapprovecomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$approve_nonce" );
$spam_url      = esc_url( "comment.php?action=spamcomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$del_nonce" );
$trash_url     = esc_url( "comment.php?action=trashcomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$del_nonce" );
$delete_url    = esc_url( "comment.php?action=deletecomment&p=$comment->comment_post_ID&c=$comment->comment_ID&$del_nonce" );

This is an interesting example in AJAX functionality for comments, where creating nonce is done simultaneously with checking its value.

<?php
/**
 * As found in wp_ajax_replyto_comment()
 * https://developer.wordpress.org/reference/functions/wp_ajax_replyto_comment/
 */

if ( current_user_can( 'unfiltered_html' ) ) {
	if ( ! isset( $_POST['_wp_unfiltered_html_comment'] ) ) {
		$_POST['_wp_unfiltered_html_comment'] = '';
	}

	if ( wp_create_nonce( 'unfiltered-html-comment' ) != $_POST['_wp_unfiltered_html_comment'] ) {
		kses_remove_filters(); // Start with a clean slate.
		kses_init_filters();   // Set up the filters.
		remove_filter( 'pre_comment_content', 'wp_filter_post_kses' );
		add_filter( 'pre_comment_content', 'wp_filter_kses' );
	}
}

Another interesting example of creating nonce for the URL to be used in Javascript code.

<?php
/**
 * As found in wp-admin/edit-form-blocks.php
 */

// Get admin url for handling meta boxes.
$meta_box_url = admin_url( 'post.php' );
$meta_box_url = add_query_arg(
	array(
		'post'                  => $post->ID,
		'action'                => 'edit',
		'meta-box-loader'       => true,
		'meta-box-loader-nonce' => wp_create_nonce( 'meta-box-loader' ),
	),
	$meta_box_url
);
wp_add_inline_script(
	'wp-editor',
	sprintf( 'var _wpMetaBoxUrl = %s;', wp_json_encode( $meta_box_url ) ),
	'before'
);

Verifying the nonce

Do you sanitize your nonce when verifying? You really should. 

The function for verifying nonce, wp_verify_nonce(), has two hooks: filter nonce_user_logged_out and action wp_verify_nonce_failed. This means the function is pluggable, and extenders should not trust its input values. If you’re applying WordPress Coding Standards (and use the code sniffer), you probably know there is a whole sniff dedicated to verifying nonces.

The proper way for verifying nonce with applying WPCS is as in this example:

<?php
/**
 * Verifying nonce with sanitizing as per WPCS.
 */
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ 'my_custom_nonce_name' ] ) ), 'delete-user' ) ) {
  return;
}

And this approach applies to other functions verifying nonces as well, check_admin_referer() and check_ajax_referer(), as they are both pluggable and WPCS is sniffing them as well.

There is a discussion about the need for sanitizing nonces, and potentially, in the future, WordPress core might do it for you automatically.

If you’re unsure

The function wp_nonce_ays(), a replacement for wp_explain_nonce(), displays a message for the action (if such a message exists) alongside the well-known “Are you sure?”. If you are not sure how this function works, you should be careful with it. It won’t prevent action from happening, and it has been exploited in the past. When in doubt, take a look at how it’s been used in the core

Refreshing the nonce

Sometimes it’s needed to refresh the nonce. For example, you start creating a post, and your session, for whatever reason, expires before you hit the Publish button. Or you change the WordPress directory while some users are still logged in. Or you’re logged in with https but navigate to http URLs, and then you return to https in the dashboard. All these situations can cause your nonce to be invalid and WordPress to try to refresh it. 

If you ever need to do it, it is recommended to use the wp_refresh_nonces filter. Usage examples of this filter are very scarce, and it takes some serious digging, but again, when in doubt, take a look at the core. The gist of the process is as follows:

  1. Check if your nonce exists in the received data.
  2. Check if you’re working with the correct entity (post ID, screen ID etc.)
  3. Check if the current user can perform the action (edit post, delete user etc.) 
  4. Create a new nonce with wp_create_nonce().
  5. Return response.

Core examples of this filter in action (see what I did there?) can be seen in wp_refresh_post_nonces(), wp_refresh_metabox_loader_nonces(), and wp_refresh_heartbeat_nonces(), to name a few.

The function where this filter was introduced, wp_ajax_heartbeat(), also uses wp_send_json() to send success and error messages.

The function meant for refreshing REST API nonce is wp_ajax_rest_nonce(), but I’m yet to see an example of its usage. However, what’s evident at first glance is that this function doesn’t send any success or error messages, so be aware of that when working with it.
If you need refreshed nonces for the Customizer, you can find them in the customize_refresh_nonces filter.

Using nonce in the frontend application

The frontend here can be understood in various ways. The first thing that comes to mind is sending the nonce to Javascript code. It might be local Javascript files, or the nonce should be sent outside of the WordPress install. 

In the first case, this is possible with wp_localize_script() or wp_add_inline_script(), as we saw in this example from the core. Or, if you would like to use REST API, you can add your nonce to the wp_rest action.

A good example can be found in REST API documentation.

<?php
/**
 * https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/
 */

wp_localize_script( 'wp-api', 'wpApiSettings', array(
    'root'  => esc_url_raw( rest_url() ),
    'nonce' => wp_create_nonce( 'wp_rest' )
) );

Nonce created this way can be used in your AJAX calls as a data parameter for requests (_wpnonce URL arg) or via the X-WP-Nonce header.

/**
 * https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/
 */
 
$.ajax( {
    url: wpApiSettings.root + 'wp/v2/posts/1',
    method: 'POST',
    beforeSend: function ( xhr ) {
        xhr.setRequestHeader( 'X-WP-Nonce', wpApiSettings.nonce );
    },
    data:{
        'title' : 'Hello Moon'
    }
} ).done( function ( response ) {
    console.log( response );
} );

In the second case, next to adding nonce to your requests, you need to set authentication for your external application, for which you can use Application Passwords in combination with other authentication methods.

It is worth mentioning here @wordpress/api-fetch package, which has a built-in middleware for creating a nonce.

This is an example of how core Gutenberg is using it:

/**
 * https://github.com/WordPress/gutenberg/blob/trunk/packages/api-fetch/src/index.js#L168-L186
 *
 */
   
return enhancedHandler( options ).catch( ( error ) => {
	if ( error.code !== 'rest_cookie_invalid_nonce' ) {
		return Promise.reject( error );
	}

	// If the nonce is invalid, refresh it and try again.
	return (
		window
			// @ts-ignore
			.fetch( apiFetch.nonceEndpoint )
			.then( checkStatus )
			.then( ( data ) => data.text() )
			.then( ( text ) => {
				// @ts-ignore
				apiFetch.nonceMiddleware.nonce = text;
				return apiFetch( options );
			} )
	);
} );

What to avoid when using nonces?

There are a few more things to keep in mind:

  • Don’t call wp_create_nonce() before init is fired – read more about it
  • Use a proper function for creating nonce. Otherwise, you might miss something important or try to reinvent the wheel (such as a referrer field, for example).
  • Never forget user role or capability checks. Permission and access are not controlled by nonce and should be checked separately.

In the end, there is a fun fact for you. Three years ago, the term “nonce” was proposed to be changed into something else. The reason was a derogatory meaning in British English.

If you think this article is too long, I say: “Nonsense! We don’t talk about nonces enough.” But on a serious side, if you have a good code example for any part of a nonce life and use case, please share it in a comment below or submit it to the code reference pages. Thank you!

Props to @bph@greenshady, @sboisvert, and @ipstenu for feedback and peer review.

Categories:

8 responses to “Understand and use WordPress nonces properly”

  1. Carsten Bach Avatar

    Hi Milana and thank you for this nice *long article*. I really enjoyed it and learned a lot.

    But I mentioned a little typo in the example for wpcs compliant nonce checks. I think the last closing bracket should come a little earlier.

    “`php
    if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ ‘my_custom_nonce_name’ ] ) ), ‘delete-user’ ) ) {
    “`

    1. Milana Cap Avatar

      Hello Carsten,

      Thank you so much for catching and reporting that one. It is fixed now.

  2. Steven Lin Avatar

    “The next one is marked private, and you won’t find it in the code reference. You can find it here.”

    Code Ref. does show private functions/classes.

    In this case, wp_block_theme_activate_nonce() is listed here
    https://developer.wordpress.org/reference/functions/wp_block_theme_activate_nonce/

    I’m guessing the reason could be wp_block_theme_activate_nonce() is introduced since v6.3, released on Aug. 8th, 2023 (this blog post is published on August 1, 2023, before v6.3 is released), so the page didn’t show up in DevHub until the software is released.

    1. Milana Cap Avatar

      Updated the post, thank you, Steven!

  3. Luc Avatar
    Luc

    Great article. I developed a custom WordPress plugin, a price calculator for ordering custom print work. With AJAX calls settings are send to the server and the server does calculations and returns the price. Of course these AJAX calls are protected with wp_nonces. This works perfect!

    But now we want to force a login before the user can go to next (ordering) step. So we created an AJAX login to do the login. After login the nonce changes, so check_ajax_referer( ‘my_custom_action’,’security’) fails. Exactly what we expected.

    To solve this, we create a new nonce with wp_create_nonce( ‘my_custom_action’ ) on the server after a successful login. And this new nonce we send back to the client. In further AJAX calls, we now use this new nonce. But still we get a fail on check_ajax_referer( ‘my_custom_action’,’security’).

    What are we doing wrong and why? How can we achieve that we can login trough AJAX and still use the nonce check system.

    1. Milana Cap Avatar

      Hi Luc,

      That seems like a more complex problem, which consists of carrying data from logged-out to logged-in state. E-commerce is not my field of expertise, and I’d probably have to go through a series of trial and error to get to the bottom of the problem.

      A quick look into what WooCommerce is doing (is saying nothing because it’s too complicated for a quick look 😅): I see they are using sessions and tokens in addition to nonces. There’s also a function called maybe_update_nonce_user_logged_out(), which suggests they are considering the workflow of updating nonces depending on the user’s logged-in state.

      This comment is probably not helping in solving your problem and I really hope someone else knows a quicker solution (and post it here). The only thing I can advise at this point is to take a look at how other similar plugins handled it – that’s the beauty of open source.

  4. Alex Avatar

    Hi Milana! Thanks for this post, really nice to have some extra details in addition to the existing documentation!
    Would be great to see a write-up on the common problems that arise in connection with using nonces. For example, how to deal with caching plugins causing Ajax requests to fail with 403.

    1. Milana Cap Avatar

      Hello Alex,

      Thank you for the comment. You open up a rather broad discussion here, but many things are left unknown, e.g., how much control you have over caching plugins and AJAX calls. In the most simple scenario, with a 403 error, I’m assuming that the AJAX nonce has been cached and, therefore, invalid.

      When dealing with cache and nonces, the first rule of thumb is to make the cache shorter than the nonce lifespan. This also requires understanding how the cache is cleared in that specific case. Is it using a cron job? Then the question is how many visits the website has and can you trust that cron. As I said, there are too many unknowns here, but a good starting point would be setting caching shorter than the nonce lifespan.

Leave a Reply