Imagine the following scenario:
- John has an account on my website, but is currently not logged in.
- He clicks the “upvote” button on an article, but is redirected to the login page because only logged-in users can upvote.
- In the referral URL (to which John is redirected after login) the following parameters are passed:
action=upvote
article=1234
nonce=gibberish
.
- After login, John is redirected to the referral URL which contains the “upvote” action.
- The nonce is now invalid because it was generated while not logged in.
The main problem here is that nonces are, from my understanding, generated with the user session. But after login, the session changes, thus making the nonce invalid.
Does WordPress have any way to handle cases like this?
I went for the solution as suggested by @Rup in the comments. Replacing the two nonce functions, wp_create_nonce()
and wp_verify_nonce()
to prefix a nonce with a 1 or a 0 for logged-in and logged-out users respectively.
The logged out nonces work by IP rather than User ID and session Token. Thus, allowing for nonces to carry over after a login. As far as I can tell, this doesn’t have any significant impact on security.
if ( ! function_exists( 'wp_create_nonce' ) ) {
/**
* Creates a cryptographic token tied to a specific action, user, user session,
* and window of time.
*
* @since 2.0.3
* @since 4.0.0 Session tokens were integrated with nonce creation
*
* @param string|int $action Scalar value to add context to the nonce.
* @return string The token.
*/
function wp_create_nonce( $action = -1 ) {
$user = wp_get_current_user();
$uid = (int) $user->ID;
$logged_in = '1-';
$token = wp_get_session_token();
$i = wp_nonce_tick();
if ( ! $uid ) {
// Prefix when logged-out nonce
$logged_in = '0-';
/** This filter is documented in wp-includes/pluggable.php */
$uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
// Use IP instead of user_id
$uid = $_SERVER['REMOTE_ADDR'];
$token = $_SERVER['REMOTE_ADDR'];
}
return $logged_in . substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
}
}
if ( ! function_exists( 'wp_verify_nonce' ) ) {
/**
* Verify that correct nonce was used with time limit.
*
* The user is given an amount of time to use the token, so therefore, since the
* UID and $action remain the same, the independent variable is the time.
*
* @since 2.0.3
*
* @param string $nonce Nonce that was used in the form to verify
* @param string|int $action Should give context to what is taking place and be the same when nonce was created.
* @return false|int False if the nonce is invalid, 1 if the nonce is valid and generated between
* 0-12 hours ago, 2 if the nonce is valid and generated between 12-24 hours ago.
*/
function wp_verify_nonce( $nonce, $action = -1 ) {
$nonce = (string) $nonce;
$user = wp_get_current_user();
$uid = (int) $user->ID;
if ( ! $uid ) {
/**
* Filters whether the user who generated the nonce is logged out.
*
* @since 3.5.0
*
* @param int $uid ID of the nonce-owning user.
* @param string $action The nonce action.
*/
$uid = apply_filters( 'nonce_user_logged_out', $uid, $action );
}
if ( empty( $nonce ) ) {
return false;
}
$token = wp_get_session_token();
$i = wp_nonce_tick();
// Check if nonce is for logged_in or logged_out ('1-' and '0-' respectively)
if ( substr( $nonce, 0, 2 ) == '0-' ) {
// Use IP instead of user_id and session token
$uid = $_SERVER[ 'REMOTE_ADDR' ];
$token = $_SERVER['REMOTE_ADDR'];
}
// Remove nonce prefix
$nonce = substr( $nonce, 2 );
// 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;
}
/**
* Fires when nonce verification fails.
*
* @since 4.4.0
*
* @param string $nonce The invalid nonce.
* @param string|int $action The nonce action.
* @param WP_User $user The current user object.
* @param string $token The user's session token.
*/
do_action( 'wp_verify_nonce_failed', $nonce, $action, $user, $token );
// Invalid nonce
return false;
}
}