1

I have a simple "favorite post" button that works with AJAX. I understand that using a WordPress nonce improves security, but am not quite sure why or how it does this. This also makes me unable to check if I've implemented the nonce correctly and securely.

jQuery Script

function favorites_javascript() {       
    $ajax_nonce = wp_create_nonce( "ajax-nonce-favorites" );

    ?>

    <script>
        ( function( $ ) {               
            var ajaxurl = "<?php echo admin_url('admin-ajax.php'); ?>";

            // ...

            // Data to be sent to function
            var data = {
                'action': 'favorites_callback',
                'security': '<?php echo $ajax_nonce; ?>',

                'post_id': post_id,
                'button_action' : button_action
            };

            // Send Data
            jQuery.post(ajaxurl, data, function(response) {     

                // ...

            });

            // ...

        } )( jQuery );
    </script> 

    <?php
} add_action( 'wp_footer', 'favorites_javascript' );

PHP Callback

function favorites_callback() {
    check_ajax_referer( 'ajax-nonce-favorites', 'security' );

    if ( !is_user_logged_in() ) {
        wp_send_json_error( 'Error!' );
    }

    // Process Data

    wp_die();
} add_action( 'wp_ajax_favorites_callback', 'favorites_callback' );

Is this the correct implementation of WordPress nonce for AJAX? And how do I "test" this code to see if the nonce works and is secure?

  • 1
    golden question, one that not enough plugin authors ask themselves – Mark Kaplun Mar 30 '17 at 13:22
1

Your code is incorrect in some subtle ways but to get why we first need to get back to what is a nonce in wordpress and what does it protect against.

A Nonce by the common definition is a number used only once and it is meant to fight replay attacks. In a replay attack you just replay a transaction (http request) without understanding its component to redo a user action. simple example where it can lead to bad situation is if someone could replay your shopping orders and cause you to order more of the same thing without you being involved. Typical situation you use someone else's network (not sure if SSL helps or not here).

It was also discovered that nonce are a good way to avoid CSRF attacks as it is hard to predict the URL/parameters the attacker should use.

In wordpress the order of importance is reversed, the first priority is to fight CSRF attacks and fighting replay attacks have just a secondary importance. This leads into wordpress nonces being a "unique per day" number instead of the classical "one time" number.

so how does wordpress uses nonces to protect agaunst CSRF? it generates a nonce based on the context. You can see in the wp_create_nonce code

function wp_create_nonce($action = -1) {
    $user = wp_get_current_user();
    $uid = (int) $user->ID;
    if ( ! $uid ) {
        /** This filter is documented in wp-includes/pluggable.php */
        $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 );
}

That the context is the action name, the user id, and the current login session and this ensures that a different nonce will be generated for different actions, different users and even different sessions of the same user, therefor even if someone knows the nonce used for to do an action (lets say, delete a post) when doing it with his own user, he will not be able to trick you into deleting a post by constructing special URL with the same nonce.

And here is your fist mistake (which many do), which is trying to apply nonces to non logged in users. Without a user context whole of the nonce is derived just from the action, and an "attacker" can just share the same url to be used by non logged in users (or by a bot).

How to test that your code works correctly in preventing CSRF? if it is a simple URL, try to resend it when logged in as another user. It is easier to do with requests in which the url itself contains the nonce, but probably can be done with browser developer tools (change authentication cookies and re-send) or curl/wget.

The other part in which you code is lacking is a protection against a sort of a replay attack. Assuming I know that you liked post X, can I re transmit the request and cause it to be seen like you liked also post Y? Right now all I need to do is just change th post_id parameter of the request.

The way to protect against this kind of attacks is by having the action part of the nonce be dynamic and include in some way the important parameters, in your case the post id. Core usually uses actions of the kind delete-post-{post_id} which ensures that there will be a different nonce generated for different posts.

How to test? Using the browser developer tools, change the post id parameter to ensure the same nonce can not be used to favorite a different posts.

For phpunit type of testing you will probably want to write your own nonce generator that wraps the wordpress core nonce functions and check they do not give the same nonce for different users, different sessions, different actions, and different key request parameters.

  • This in an excellent answer! The only thing it's missing I think is a clear example of how to implement an action-oriented nonce. After looking at the Codex I found the example I needed to fully understand how my original approach was lacking: wp_nonce_field( 'delete-comment_'.$comment_id ); – Swen Mar 31 '17 at 11:01
2

Reading about it in the WP Codex seems that the implementation is right.

A simple test would be to make a "forged" ajax request with no security parameter in $.ajax(), or to make the same request with a nonce taken from a different time and page.

(what is a nonce, for reference: wiki)

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.