One of the easier to understand vulnerabilities is the CSRF. It’s also one of the most common issues we see in plugins and themes, because people rarely think about it.
Imagine that I have a form that takes input, like so:
<form action="http://example.com/example.php" method="GET"> <input type="text" name="demo" /> </form>
Now, that’s a simple form (and missing a submit button to boot), but you get the idea. It takes a text input. Presumably, something on the other end (at /example.php) processes that input, saves it in a database, something like that. Easy.
First question: Is this necessary?
The main question I see asked when this concept is explained to people is “why is this necessary?”. Some people believe that since you have to be logged in to access admin screens in the first place, then you can’t get to the forms and submit them. Why have all this protection and checking for a form submission when the form is hidden behind a login screen?
What you need to understand is the difference between “authority” and “intent“.
In real world cases where we are processing that input, we generally want to limit who is allowed to submit that form in some way. A plugin will want to only allow admins to change settings. A theme will only want to allow site owners to adjust the display of the site. Things of that nature. For these cases, we use methods of authentication.
There’s several ways to do this, we can check the current_user information. WordPress has capability checks for users to know what they are and are not allowed to do. When we check these, we’re verifying authority. Making sure that the user is allowed to do these things.
But something else that we need to check which most people don’t think about is intent. Did the user actually intend to submit that form, or did their browser submit it for them automatically, perhaps without their knowledge?
Examine that form again, and consider what would happen if you were to visit a webpage, anywhere on the internet, that contains this:
<img src="http://example.com/example.php?demo=pwned" />
Now, you might be thinking that this is a rather contrived example, and you’d be right on that score, but it serves to demonstrate the point. Your browser loads this URL and that is the equivalent action to submitting that form, with “pwned” as the text in question.
Here’s the kicker, all those authority checks do us no good in preventing this. You actually do have the authority to submit that form, and your browser, using your authority, just submitted it for you. Pwned, indeed.
What we need is to verify intent. We need to know that the user submitted that form, and not just the browser doing it for them automatically.
WordPress used to do this (a looong time ago) using the referer. For those who don’t know, referer is a URL passed by your browser to indicate where a user came from. So one could check that the referer says that the form was submitted from the form’s page and not from some other page on the internet. The problem is that referer is not reliable. Some browsers have the ability for script to fake the referer. Firewalls and proxies often strip the referer out, for privacy concerns. And so forth.
WordPress now does this using nonces. A nonce is a “number used once” in its purest form. Basically, it’s a one-time password. When we generate the form, we generate a number. When the form is submitted, we check the number. If the number is wrong or missing, we don’t allow the form to be submitted. A script cannot know the number in advance. Other sites cannot guess the number.
Now, technically, WordPress doesn’t use real nonces, because they’re not “used once”. Instead, WordPress nonces revolve on a 12 hour rotating system (where 24 hours are accepted). For any given 12 hour period, the nonce number for a given action will be the same. But it’s close enough to a real nonce to eliminate the issue, but notably it’s only for the issue of verifying intent. Don’t try to use WordPress nonces for anything else.
So, when we generate a form, we generate a nonce. This nonce is based on five things: site, user, time, the action being performed, and the object that the action is being performed on. Changing any of these gives us a different nonce.
Let’s say I want to delete a post. To do that, I need to know the nonce for deleting that specific post, as me, on my site, within the last 24 hours. Without that nonce, I cannot perform the action. More importantly, in order for somebody to “trick” my browser into doing it for me, they need to get that specific nonce and get my browser to load it within 24 hours. Tough to do. And even if they pull it off, they only have been able to perform that very specific action, the nonce obtained is useless for any other purpose. They don’t get any form of full control via this manner. They can’t make my browser do anything on mysite that they don’t have the nonce for.
Protecting a link can be done with wp_nonce_url(). It takes a URL and an action and adds a valid nonce onto that URL. It works like this:
$nonced_url = wp_nonce_url( $url, 'action_'.$object_id );
Here, we’re taking some URL, and adding a nonce onto it for a specific action on some specific object. This is important, actions and objects need to both be specified if there is some object being referred to. An example might be a link to delete a specific post. Such code would look like this:
wp_nonce_url( $url, 'trash-post_'.$post->ID )
The action is “trash-post” and the post being trashed has its ID number appended to that action. Thus, the nonce will let you trash that post and only that post.
On the other hand, maybe we have a form that we need to protect instead. Inside that form, we can add something like this:
wp_nonce_field( 'delete-comment_'.$comment_id );
This is the nonce for deleting a comment. It outputs a couple of form fields, like so:
<input type="hidden" id="_wpnonce" name="_wpnonce" value="1234567890" /> <input type="hidden" name="_wp_http_referer" value="/wp-admin/edit-comments.php" />
The value for the nonce will be specific to deleting that comment, on that site, by that user.
Sometimes we just need to generate the nonce directly, in no specific format. One case might be for an AJAX type call, where the data is being submitted by jQuery. In such a case, you can use the wp_create_nonce function to get just that nonce value, like so:
wp_create_nonce( 'action_'.$object_id );
For AJAX requests, you’ll want to include that nonce value in the submitted data with a name of “_ajax_nonce”. Why that particular name? Because it’s what WordPress checks when verifying the nonce. Speaking of verification:
Generating these numbers is no good if you don’t check them as well. Fortunately, WordPress makes this easy. There’s two functions to verify incoming nonces.
check_admin_referer( 'action_'.$object_id );
The name of the function refers back to the time before nonces, when this function call was checking the referer value from the browser. Nowadays, it checks nonces instead. If the _wpnonce sent back in the form does not match the action and ID here, then this function stops further processing. This is the cause of the “Are you sure you want to do this?” screen that is sometimes reported by users. To avoid getting this screen, the nonce being checked has to match.
An alternative to checking forms or links is checking ajax requests, which is why we have this function:
check_ajax_referer( 'action_'.$object_id );
In either case, if the nonce fails, the script exits. No action is taken. The form is not processed, the post not deleted. That’s the sort of check you need to prevent CSRF attacks.
If you have a plugin or a theme or any type of code that “does something” in WordPress, then you need to protect that action with a nonce. If you’re not protecting it with a nonce, then it’s possible for somebody else to trick your browser into performing that action on your behalf.
Also, note that it’s not enough to just name the action. You generally are taking action on some specific “thing”, and the ID of that thing needs to be included in your nonce as well. The more specific the action, the better.
Any form, any action, no matter how much “authentication” you have on checking it, can be exploited, because you’re not really authenticating the “user”, you’re authenticating that it’s coming from “the user’s browser”. You need to have something else that changes regularly, so that you can verify that the user did indeed load that particular form and submit it relatively recently, and thus probably intended to perform that action.
Nonces are easy to implement. So do it already. We have enough plugins not doing it that this clearly needs to be said.