What is XSRF?
An XSRF vulnerability is one which allows a malicious user or website to make an unsuspecting user follow a URL or post a form which will perform an action on your site which the user didn't want to happen.
As a basic example, imagine you allow users to post images in your comments. If a user puts "http://example.com/logout.php" as the image's URL, where example.com is your domain, then any time a logged in user views that comment, they will be logged out.
Or maybe it could be that you allow a user account to be deleted without confirming the action or protecting it from XSRF in any way, so any user visiting the page would then get deleted instantly!
I thought I'd make a comment on your site. Check out this cool image!
<img src='http://example.com/delete_my_account.php" />
A different example would be if a malicious website contains a form, which an unsuspecting user then submits, it can actually submit information to another site which the user is logged in to, and that site will think the request came from the logged in user and will just perform the update as if the user had done it intentionally.
It doesn't even have to be a link/button the user clicks on, as shown in the first example it could happen even if just viewing a site. Obviously, this could become quite annoying for your users.
Annoying is one thing, but it can also be pretty dangerous. For example, let's say I'm logged into an account on a simple shopping cart site. I then go and browse to another unrelated website. The other website has a button which just says "Click here to register". Seems simple enough, this other website wants me to register an account. However, this is a malicious site and when I click the link, it's actually submitting a request to the original shopping cart website as if I'd clicked the "Order me 1000 of some expensive item in one-click" button. If the shopping cart website is susceptible to XSRF then it will think the request to order 1000 items was genuinely submitted by me, and I'll get a nice surprise in the mail and on my credit card statements.
It's a particularly difficult type of attack to get your head around as it's very subtle, but once you understand how it works it's not that difficult to protect against it. It's very easy to go down the bad route though.
Protecting against XSRF - The bad way
So what can you do as a web developer to prevent such attacks? Effectively you're just wanting to make sure a request came from your site and was actually intended to be run by the user, so as a first step you can just check the referer header to make sure the request came from your own site.
<?php
$url = parse_url($_SERVER['HTTP_REFERER']);
if ($url['host'] != "wblinks.com")
{
die("You're not coming from my site. Possible XSRF attack");
exit();
}
?>
While this may work in some cases, it's going to be about as effective as an underwater hair dryer as far as stopping an XSRF attack. Altering the referer header is pretty trivial and it will also have the downside of making the site unusable for lots of people, since many browser or proxies can strip the referer header when in "private" mode.
This also wouldn't protect against the first example of XSRF, where someone just uses the logout URL as an image URL in a comment. As the request would come from the correct referer in that case.
So not only will you not prevent XSRF attacks, but you'll also annoy some of your users. Not a good solution.
Protecting against XSRF - The good way
What we really need is a one-time key (or token) which allows us to validate that the request came from a form we presented to the user intentionally. Here's how I accomplish that on my sites (although you'll probably notice not on this site yet... that's because this is just a blog, and doesn't have registered users anyway. Also I haven't updated this site to use the latest version of my framework just yet as it requires some refactoring).
I have an Auth class which handles everything to do with user authentication on my sites as well as authenticating that a request was valid, etc. I have 3 very simple functions in this class, which are as follows,
// This is a key used to stop CSRF/XSRF attacks. It's stored in the user session.
public static function getNewRKey()
{
$rkey = substr(sha1(uniqid(rand(), true)), 0, 20);
$rkeys = Session::getInstance()->getVar("rkeys");
$rkeys[] = $rkey;
Session::getInstance()->setVar("rkeys", $rkeys);
return $rkey;
}
// This will determine if an rkey is valid
public static function validateRKey($rkey)
{
$rkeys = Session::getInstance()->getVar("rkeys");
if ($rkeys == null) { return false; }
return in_array($rkey, $rkeys);
}
// Creates the form input to use
public static function formRkey()
{
return "<input type=\"hidden\" name=\"rkey\" value=\"".Auth::getNewRKey()."\" />";
}
So what's going on here? Anytime I have a form on the site which POSTs data, I will output the formRKey() function to add a hidden field to this form. This hidden field contains the value returned from getNewRKey(), this method generates a random 20 character hash value, and stores it into the user session (I'll get to why that's an array in a moment) and then returns this hash so it can be put into the hidden input.
<form action=”do_something.php” method=”POST”>
<fieldset>
<?=Auth::formRKey();?>
<input ... />
</fieldset>
</form>
So, the user session will now contain an array called “rkeys”, which contains a random 20 character hash value.
$_SESSION['rkeys'] (1) =>
{
[0] => '3748ab53cf129d536eca'
}
This has also added a hidden field to the HTML form which will look like this,
<input type="hidden" name="rkey" value="3748ab53cf129d536eca" />";
So when the user submits this form, the idea is that we take the rkey value and check it against the ones we've stored in the user session. If it's there, then the request is valid and came from a form which the site generated. If not, then it's a possible XSRF attack and the output should be stopped and logged. The validation is done in the validateRKey() method.
Now the final step, is to run something like the following code on any page that takes input. I actually have it in my bootstrap file which is run on every page request to the site.
if (count($_POST) > 0 && class_exists("Auth"))
{
if (!Auth::validateRKey(Sanitizer::sanitize($_POST['rkey'])))
{
Log::create("XSRF", "Possible XSRF attack", LOG::NOTIFY, LOG::NOTIFY_ADMIN);
die("Detected a possible cross site request forgery attack. "
."Perhaps you tried to refresh a POST request?");
exit();
}
}
// Clear any current rkeys
Session::getInstance()->clearVar("rkeys");
So if anything has been POSTed to the page load, and the Auth class exists (this is because it's part of the framework I use on a lot of sites.. not all of which require Auth stuff). I validate that the POSTed rkey is in the list of expected ones. If not, I log the error and then kill the page load and present an error to the user. You could perhaps redirect there instead of just calling die(), and present something more friendly to the user, it's up to you. For my purposes, this suffices.
Importantly, you also need to clear the session variable of acceptable rkeys, since a request has now come in and been validated, so all keys are now expired. If you don't do this, then it's pointless, since someone can just screenscrape the rkey and it would always be valid in the user session.
Why an array of rkeys?
Originally it was just one rkey instead of an array, but that presented a problem. If I had more than one form on the page, only the final form would be valid, any other form would present the XSRF error, since a new rkey was generated each time a form was created and then only that new rkey was valid. An easy mistake to make, but something I noticed quite quickly. Of course, if you only ever have one form on a page, then feel free just to use one value, but I would still recommend using an array since it allows you to easily expand in future if you do end up using more than one form on a page.
Why only POST?
You may be wondering why I'm only checking POST variables to prevent XSRF rather than both POST and GET. The answer is because GET should never be susceptible to such an attack if you're using it correctly.
So when should you use GET and when should you use POST? Well, it's all in the name. GET variables ideally should be used when the contents of the page are read-only, so nothing on the page gets changed by the request. So a GET request should be idempotent (I should be able to trigger the same GET request as many times as I want and it shouldn't affect the result I get. Basically, you want to make sure it doesn't have any side-effects). POST should be used whenever it causes a destructive action (I don't just mean deletes... I mean destructive as in something changes). So login, logout, updates, creation, deletion, comments, voting, etc.
This is why browsers will generally prompt you to confirm when resending a POST request, whereas they won't with a GET request. This is because a POST request will generally change the outcome each time it is run so you want to make sure you're not going to accidentally run it again and change something you didn't want to. A GET request shouldn't change any data that way, and so it doesn't need to be confirmed.
If you do want to use a GET for a destructive action, say you want it to be an anchor tag rather than a button otherwise your style will look strange, then you must make sure that you at least redirect to a confirmation page to confirm the YES/NO, ideally this confirmation page should use POST.
If not, then say you were to have a link in your admin area which deleted an item using GET, without any form of confirmation. Some browsers do what's called pre-fetching, where they examine the links on a page, and pre-fetch the websites you'd get by cliking these links, under the assumption that you will go to them. Then when you do click them, the page can be displayed very quickly. If you just delete with a GET, then simple visiting the admin page could cause the browser to follow all those links in the background and delete everything. Obviously not something you want. Yes, this happened to me in the past, so please learn from my mistakes.
POST Refresh
Another thing to keep in mind, is that you should redirect users to a GET based URL once the POST request has been handled. This means users will be able to hit F5 and refresh the result page without being prompted if they want to resubmit the POST request (since that will now cause an XSRF error to be displayed). This is common practice on most sites and something users now expect to be able to do. So they'll probably not be too happy if they have to re-send requests and then get errors saying they're XSRF attacking the site.