0

Imagine the following scenario:

  1. John has an account on my website, but is currently not logged in.
  2. He clicks the "upvote" button on an article, but is redirected to the login page because only logged-in users can upvote.
  3. In the referral URL (to which John is redirected after login) the following parameters are passed: action=upvote article=1234 nonce=gibberish.
  4. After login, John is redirected to the referral URL which contains the "upvote" action.
  5. 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?

  • So you must trigger the upvote AFTER the user got logged in. Take the URL param you already have to accomplish this. – leymannx Apr 3 at 14:02
  • 1
    wp_verify_nonce and wp_create_nonce are 'pluggable', meaning you can declare your own versions in plugins to replace the core versions. You could e.g. separate logged in and logged out nonces, e.g. by prefixing them with 1 and 0 respectively, and have separate schemes to construct and validate them: use the current scheme for logged in users, but for logged out users e.g. use the IP address and a finer-grained timestamp in the hash so that logged out nonces have a much shorter timespan. – Rup Apr 3 at 22:37
  • 1
    Then wp_verify_nonce can approve a not-logged-in nonce even after the user had logged in. But by far the simplest thing to do would be to redirect the user back to the page with the vote button, and have them press to vote again now that they're logged in. – Rup Apr 3 at 22:39
1

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.