WordPress Front End Security: CSRF and Nonces

Avatar of Andy Adams
Andy Adams on (Updated on )

In our last article, we covered Cross-Site Scripting (XSS) and the functions WordPress provides to prevent XSS attacks. Today, we’ll look at another security concern for front end developers: Cross-Site Request Forgery (CSRF).

Lest you think this security stuff isn’t important, a major vulnerability was recently found in the WP SEO plugin, which is installed on 1,000,000+ WordPress sites and which allowed hackers to manipulate the WordPress database using CSRF. (The plugin was fixed quickly, but you can see how scary this stuff can be.)

Nonces and Cross-Site Request Forgery

Put simply, CSRF is when bad guys try to trick users (usually someone with access to the WordPress dashboard) into doing something they didn’t intend to do. A simple example can help illustrate:

Suppose you were building a WordPress plugin to allow logged-in users to submit their pictures. On the front end of the site, your plugin might generate a form like so:

<?php if ( is_user_logged_in() ) : ?>
  <form action="/submit-picture/" method="get">
    <input type="text" name="picture_url">
    <input type="submit">
  </form>
<?php endif; ?>

On the back end, you handle the picture form submissions:

if ( is_user_logged_in() && isset( $_REQUEST['picture_url'] ) ) {
  // Save the picture here
}

Now, suppose a hacker wants to submit a fake picture. The hacker doesn’t have a username or password – so they can’t do anything, right?

Not quite. To hijack one of your users’ accounts to submit a fake picture, the hacker could try to get a logged-in user to click a link that looks like this:

http://your-site.com/submit-picture/?picture_url=fake-picture.jpg

If a logged-in user clicks that link, what would stop the picture from being submitted? You guessed it: Nothing. Hacker wins.

That’s because we didn’t do anything to verify the user’s intention to submit a picture. The “F” in CSRF stands for forgery: The hacker has forged (faked) a request on behalf of the user, kind of like how you forged your mom’s signature on sick notes in elementary school.

How to Prevent CSRF

We can stop CSRF attacks by using some handy functionality built into WordPress. To prevent a request from being successfully “forged”, WordPress uses nonces (numbers used once) to validate the request was actually made by the current user.

The basic process looks like this:

  1. A nonce is generated.
  2. That nonce is submitted with the form.
  3. On the back end, the nonce is checked for validity. If valid, the action continues. If invalid, everything halts – the request was probably forged!

Let’s Add a Nonce

On the front end, suppose we wanted to add a nonce to a form submission. We’ll do this with the wp_nonce_field convenience function:

<form action="/submit-picture/" method="get">
  <input type="text" name="picture_url">
  <input type="submit">
  <?php wp_nonce_field( 'submit_picture' ); ?>
</form>

Easy, right? Now, when we’re processing this form on the back end, we just need to use some built-in WordPress functions to verify that the nonce was valid:

// By default, we can find the nonce in the "_wpnonce" request parameter.
$nonce = $_REQUEST['_wpnonce'];
if ( ! wp_verify_nonce( $nonce, 'submit_picture' ) ) {
  exit; // Get out of here, the nonce is rotten!
}

// Now we can process the submission
if ( is_user_logged_in() && isset( $_REQUEST['picture_url'] ) ) {
  // Save the picture here
}

Because the nonce is unknown to the hacker, he can’t craft a fake link that will do harm. Any malicious attempts will be squashed at the wp_verify_nonce check. We’ve stopped the hacker, right?! For many cases, yes. We’ve made it much more difficult for a hacker to forge a request.

Nonces are User-Specific

But what if a bad logged-in user of your site wanted to submit a fake picture for another user? Couldn’t the bad user could just look at the HTML source of the site, find the nonce field, and add it to their “evil” URL like so?

http://your-site.com/submit-picture/?picture_url=fake-picture.jpg&_wpnonce=NONCEVALUE

Fortunately, it won’t work. WordPress nonces are unique to a user’s session, so a hacker couldn’t substitute his own nonce for another user.

Hat tip to Mark Allen in the comments for pointing out session-unique nonces.

Try These Nonce Functions

WordPress is full of convenience functions to make generating nonces (and preventing CSRF) easier. Here are a few you might find useful for different situations:

Function: wp_verify_nonce

What it does: Validates that the nonce value was legitimately generated by WordPress.

No matter how you generate your nonce, it should be checked on the back end with wp_verify_nonce

Just like the examples above, use wp_verify_nonce to verify a user’s intent:

$submitted_value = $_REQUEST['_wpnonce'];

if ( wp_verify_nonce( $submitted_value, 'your_action_name' ) ) {
  // nonce was valid...
}

Codex entry for wp_verify_nonce

Function: wp_nonce_field

What it does: Prints a hidden <input> tag containing a nonce value.

Used when you’re building a <form> that needs to be protected from CSRF (which is pretty much every <form>).

Just like in the examples above, wp_nonce_field is a convenience function to make our life easier building HTML forms:

<form>
<!-- Other form fields go here -->
<?php wp_nonce_field( 'your_action_name' ); ?>
</form>

Codex entry for wp_nonce_field

Function: wp_nonce_url

What it does: Returns a URL with a nonce value attached.

Used when you’re building a URL that needs a nonce field appended as a query string parameter. Most useful when doing GET requests that need CSRF validation.

Here’s an example of wp_nonce_url where we need to validate that a user intended to click a link:

<?php
  $action_url = wp_nonce_url( '/change-color/?color=blue', 'change_color' );
  // $action_url is now "/change-color/?color=blue&_wpnonce=GENERATED_VALUE"
?>
<a href="<?php echo esc_url( $action_url ); ?>">Change to blue</a>

Codex entry for wp_nonce_url

Function: wp_create_nonce

What it does: Generates a nonce value.

Used when you need a nonce value that doesn’t fit one of the above convenience functions.

Here’s wp_create_nonce used to generate a nonce value for returning in JSON format (useful for returning in AJAX requests):

<?php
  $nonce_value = wp_create_nonce( 'your_action_name' );

  return json_encode( array( 'nonce' => $nonce_value ) );
?>

Codex entry for wp_create_nonce

Function: check_ajax_referer

What it does: Checks for a submitted nonce using wp_verify_nonce, and if the validation fails, stops execution.

check_ajax_referer is another convenience function mainly for use in AJAX requests. You can use it to skip doing the wp_verify_nonce check yourself:

<?php
  // Forged requests won't get past this line:
  check_ajax_referer( 'your_action_name' );

  // Do your action here
?>

Codex entry for check_ajax_referer

Nonces for Everything!

CSRF vulnerabilities range from harmless (e.g. poll submissions) to extremely dangerous (e.g. modifying passwords or permissions).

Regardless of severity, you should be using nonces to prevent CSRF any time a user does an action in WordPress. Because…why wouldn’t you? You don’t want hackers forging requests on your site, and WordPress gives you the tools to stop them.

Use nonces, stop forgery, and foil hackers!