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:
- A nonce is generated.
- That nonce is submitted with the form.
- 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>
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!
I have a question – let’s say you want to generate a nounce as a unique hash value. How would you verify that randomly generated hash in the backend and how would you generate it in the first place ? This seems like a more secure way of protecting form submissions.
This is essentially what WordPress does with
wp_create_nonce
and the like – generates a hash value and checks it on the back end. Are you alluding to something specifically about how WP does it?This is one of the most clear articles on what nonces are and what they do in WordPress. I often time think that front end developers consider it to be the back end developers responsibility to take care of all of the security issues. This couldn’t be farther from the truth.
One question I have is that over the years Google CAPTHA has been less and less powerful at stopping spam. In the same regard do you think that nonces in WP run the same risk as the years progress? If so, how do you think we can become less reactionary on more proactive at preventing these security risks?
FYI I understand that spam is different then hackers :).
WP nonces are not perfect, but I don’t think they suffer the same problem as CAPTCHA. CAPTCHA becomes less effective as image-reading scripts get better at picking numbers out of images. Nonces can’t be “figured out” in the same way if they’re done correctly.
I do wish WordPress had automated nonce generation. For example, in Ruby on Rails you never have to write a nonce generation or check – it’s all done automatically. That’d be a major step in preventing CSRF.
in britain, the word ‘nonce’ means ‘child abuser’.. so the internationalisation of this concept may need a rethink!
Thanks for this article, it’s nice to explain how to protect against
CSRF
attacks.However, I’m shocked there is no mention of
POST
requests instead ofGET
ones. UsingPOST
forms makeCSRF
attacks harder because you can’t forge requests simply by altering parameters in the URL (withPOST
there is no parameter in the URL).To me, using
POST
request in the basics and should be the first step. UsingCSRF
token should be done too, but it’s only the second step.Also, you’d better rely on variables
$_POST
or$_GET
to get exactly the value you want, because$_REQUEST
is configuration dependant, and it’s content may be altered with cookies values (cf. http://php.net/manual/en/reserved.variables.request.php) so I would not recommend using it in this case.In a general case, forms should use
POST
instead ofGET
(at least) when:– database content is modified (this is the case here)
– user’s session state is changed (like connection AND deconnection pages for example)
Cheers,
Romain
POST requests do make it harder (you can’t just trick someone into clicking a link), but they’re not that much harder. If a hacker can get someone to visit their site, they can fire off a POST via JavaScript.
I used GET to keep the examples simple, but you’re correct – POST should be used for most form submissions.
I’d never considered CSRF. I don’t use WP, but I can see how one might implement unique identifiers on all user actions. Must admit, security is where I am weakest (Chris, you ought to do a poll on that), but there are obviously some easy wins that we should all be implementing – just like we (try to) keep a short checklist for performance wins.
Is that user-specific nonce just appending the user’s ID to the nonce? If so, can’t an attacker just change the last bit of the nonce to the appropriate user ID?
Great question! The user ID is being used to generate the nonce, so the value will be unique to that user. The user ID won’t be visible in the nonce value, so a hacker can’t simply append the ID.
You don’t actually need to create user specific nonce’s. WordPress does that for you. WordPress nonce’s are actually specific to a users session, so even being logged in elsewhere as the same user renders it useless.
See the Codex https://codex.wordpress.org/WordPress_Nonces#Creating_a_nonce.
You’re right! Post has been updated to fix my mistake. Thanks for taking the time to correct me!
There’s also
check_admin_referer()
that you can use likecheck_ajax_referer()
To make it even more secure you can check for permissions with
current_user_can()
and the appropriate capability.