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" />
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" />