In principle, cross-site request forgery is a pretty straightforward kind of website vulnerability. Easy to test, common, and not trivial, but also not very severe.
- I send a request to a website to perform some action on my behalf.
- The website understands the action I want to perform by the data contained in my request: which URL it’s going to and what GET or POST parameters it’s carrying. It understands who I am by looking at my session cookie, a separate piece of data which I get when I log on and send with every subsequent request.
- I capture my request, make it into a link, and send you the link, disguised as something else.
- You click the link. The website sees whatever action I previously undertook, and because you’re logged into the website on your device, it sees your cookie data along with that action. Therefore, you perform the action.
As a developer, you protect against this sort of thing by randomly generating a unique “CSRF token” and placing it in a hidden field in all of the site’s forms. You can generate this token either once per user session or generate new ones for every page. Other forms of protection include
- requiring the user to enter a CAPTCHA or One Time PIN to confirm their submissions.
- sending the user’s cookie data in both their cookie and the request POST data and checking that they match upon arrival at the server.1
If you don’t do one of these things, your application is vulnerable. Well, mostly and usually.
A while ago I wrote a post about cross-site request forgery and JSON APIs. The upshot of that was that modern browsers have built-in controls that prevent sites from arbitrarily sending JSON data to each other: JSON post requests are always pre-empted with a polite OPTIONS request asking the target server if it’s supposed to be receiving JSON data from other origins. This can be bypassed by setting the content type of the request to
text/plain, but that’s pretty useless if the application server only accepts
The ASP.Net ViewState presents a similar situation where partial, unintentional CSRF protection exists, depending on variables.
The ViewState was originally introduced in Microsoft’s ASP web framework as a way of making it easier for developers of traditional Windows forms applications to make the transition to web applications. Because HTTP is a stateless protocol, every time you submit a form that does a postback, the form page will be reloaded and the previous state of its fields and checkboxes and other controls lost forever. This obviously freaked out stalwart Windows application developers quite a bit, and so Microsoft came up with this idea of the ViewState: a base 64 encoded blob of data describing the state of everything on the page that is passed between the client and the server on every action, within a POST parameter, and lets web applications play at having persistent state.
In the old days, sending this often massive blob of base 64 back and forth between the client and server with every request and response was a nasty overhead, but today it’s not usually noticeable. Additionally, the integrity of the ViewState – generally composed of highly sensitive data critical to application’s proper functioning – data that in most applications would never leave the server – is protected by a MAC check, so any ViewState that’s been tampered with between landing with the client’s browser and returning to the application server is rejected and causes an error (and so it should). The absence of a ViewState in a request usually causes a similar error.
CSRF relies on a separation of identity and action in order to work: the user’s identity is determined by their cookies, and their actions are determined by the contents of the GET and POST requests they send to the application server. If I can get you to submit a request to your internet banking with an action which says “transfer $1 000 000 to David’s account” while you’re logged in, your browser will submit your cookies along with my action, making it look like you want to give me lots of money.2 But once we muddy up that identity/action division, things start getting messy.
Let’s say, for example, that the ViewState which relates to the transfer money action contains a piece of data specifying the user’s account number. Because the request will fail without a valid ViewState, I am forced to include the ViewState I received when crafted this attack by capturing requests from the internet banking application we both use. As I did this on my account, the bit of data in the ViewState indicating the current user’s account will contain my account – remember, I can’t change anything in the ViewState without invalidating it.
In this new version of the attack, the application will receive a request with your identity and the action “transfer $1 000 000 to David’s account from my account which is David’s account”. At worst, it’ll make the transfer both from and to my account, but it’s more likely that it’ll encounter an error as (1) you’re not supposed to transfer money out of my account and (2) you’re not supposed to transfer money from an account into itself.
So, in summary, if you don’t want to implement CSRF tokens or cookie double submission on your web app, another solution is to pass all used state data between the client and the server with every request in a MAC verified form, possibly encrypted, and you’ll be guaranteed to reduce CSRF at least a little. Definitely the simple option.
I only found out about this one recently. It’s simple, but elegant. ↩︎
In this example, we’re ignoring One Time PINs and suchlike for the sake of simplicity. ↩︎