Anatomy of a Cross-site Request Forgery Attack

A Cross-site request forgery attack, also known as CSRF or XSRF (pronounced sea-surf) is the less well known, but equally dangerous, cousin of the Cross Site Scripting (XSS) attack. Yeah, they come from a rough family.

CSRF is a form of confused deputy attack. Imagine you’re a malcontent who wants to harm another person in a maximum security jail. You’re probably going to have a tough time reaching that person due to your lack of proper credentials. A potentially easier approach to accomplish your misdeed is to confuse a deputy to misuse his authority to commit the dastardly act on your behalf. That’s a much more effective strategy for causing mayhem!

In the case of a CSRF attack, the confused deputy is your browser. After logging into a typical website, the website will issue your browser an authentication token within a cookie. Each subsequent request to sends the cookie back to the site to let the site know that you are authorized to take whatever action you’re taking.

Suppose you visit a malicious website soon after visiting your bank website. Your session on the previous site might still be valid (though most bank websites guard against this carefully). Thus, visiting a carefully crafted malicious website (perhaps you clicked on a spam link) could cause a form post to the previous website. Your browser would send the authentication cookie back to that site and appear to be making a request on your behalf, even though you did not intend to do so.

Let’s take a look at a concrete example to make this clear. This example is the same one I demonstrated as part of my ASP.NET MVC Ninjas on Fire Black Belt Tips talk at Mix in Las Vegas. Feel free to download the source for this sample and follow along.

Here’s a simple banking website I wrote. If your banking site looks like this one, I recommend running away.

banking-login-pageThe site properly blocks anonymous users from taking any action. You can see that in the code for the controller:

[Authorize]
public class HomeController : Controller
{
  //...
}

Notice that we use the AuthorizeAttribute on the controller (without specifying any roles) to specify that all actions of this controller require the user to be authentication.

After logging in, we get a simple form that allows us to transfer money to another account in the bank. Note that for the sake of the demo, I’ve included an information disclosure vulnerability by allowing you to see the balance for other bank members. ;)

bank-transfer-screen

To transfer money to my Bookie, for example, I can enter an amount of $1000, select the Bookie account, and then click Transfer. The following shows the HTTP POST that is sent to the website (slightly edited for brevity):

POST /Home/Transfer HTTP/1.1
Referer: http://localhost:54607/csrf-mvc.html
User-Agent: ...
Content-Type: application/x-www-form-urlencoded
Host: 127.0.0.1:54607
Content-Length: 34
Cookie: .ASPXAUTH=98A250...03BB37

Amount=1000&destinationAccountId=3

There are three important things to notice here. We are posting to a well known URL, /Home/Transfer, we are sending a cookie, .ASPXAUTH, which lets the site know we are already logged in, and we are posting some data (Amount=1000&destinationAccountId=3), namely the amount we want to transfer and the account id we want to transfer to. Let’s briefly look at the code that executes the transfer.

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Transfer(int destinationAccountId, double amount) {
  string username = User.Identity.Name;
  Account source = _context.Accounts.First(a => a.Username == username);
  Account destination = _context.Accounts.FirstOrDefault(
    a => a.Id == destinationAccountId);
            
  source.Balance -= amount;
  destination.Balance += amount;
  _context.SubmitChanges();
  return RedirectToAction("Index");
}

Disclaimer: Do not write code like this. This code is for demonstration purposes only. For example, I don’t ensure that amount non-negative, which means you can enter a negative value to transfer money from another account. Like I said, if you see a bank website like this, run!

The code is straightforward. We simply transfer money from one account to another. At this point, everything looks fine. We’re making sure the user is logged in before we transfer money. And we are making sure that this method can only be called from a POST request and not a GET request (this last point is important. Never allow changes to data via a GET request).So what could go wrong?

Well BadGuy, another bank user has an idea. He sets up a website that has a page with the following code:

<html>
<head>
    <title></title>
</head>
<body>
    <form name="badform" method="post"
     action="http://localhost:54607/Home/Transfer">
        <input type="hidden" name="destinationAccountId" value="2" />
        <input type="hidden" name="amount" value="1000" />
    </form>
    <script type="text/javascript">
        document.badform.submit();
    </script>
</body>
</html>

What he’s done here is create an HTML page that replicates the fields in bank transfer form as hidden inputs and then runs some JavaScript to submit the form. The form has its action set to post to the bank’s URL.

When you visit this page it makes a form post back to the bank site. If you want to try this out, I am hosting this HTML here. You have to make sure the website sample code is running on your machine before you click that link to see it working.

Let’s look at the contents of that form post.

POST /Home/Transfer HTTP/1.1
Referer: http://haacked.com/demos/csrf-mvc.html
User-Agent: ...
Content-Type: application/x-www-form-urlencoded
Host: 127.0.0.1:54607
Content-Length: 34
Cookie: .ASPXAUTH=98A250...03BB37

Amount=1000&destinationAccountId=2

It looks exactly the same as the one before, except the Referer is different. When the unsuspecting bank user visited the bad guy’s website, it recreated a form post to transfer funds, and the browser unwittingly sent the still active session cookie containing the user’s authentication information.

The end result is that I’m out of $1000 and BadGuy has his bank account increased by $1000. Drat!

It might seem that you could rely on the checking the Referer to prevent this attack, but some proxy servers etc… will strip out the Referer field in order to maintain privacy. Also, there may be ways to spoof the Referer field. Another mitigation is to constantly change the URL used for performing sensitive operations like this.

In general, the standard approach to mitigating CSRF attacks is to render a “canary” in the form (typically a hidden input) that the attacker couldn’t know or compute. When the form is submitted, the server validates that the submitted canary is correct. Now this assumes that the browser is trusted since the point of the attack is to get the general public to misuse their own browser’s authority.

It turns out this is mostly a reasonable assumption since browsers do not allow using XmlHttp to make a cross-domain GET request. This makes it difficult for the attacker to obtain the canary using the current user’s credentials. However, a bug in an older browser, or in a browser plugin, might allow alternate means for the bad guy’s site to grab the current user’s canary.

The mitigation in ASP.NET MVC is to use the AntiForgery helpers. Steve Sanderson has a great post detailing their usage.

The first step is to add the ValidateAntiForgeryTokenAttribute to the action method. This will validate the “canary”.

[ValidateAntiForgeryToken]
public ActionResult Transfer(int destinationAccountId, double amount) {
  ///... code you've already seen ...
}

The next step is to add the canary to the form in your view via the Html.AntiForgeryToken() method.

The following shows the relevant section of the view.

<% using (Html.BeginForm("Transfer", "Home")) { %>
<p>
    <label for="Amount">Amount:</legend>
    <%= Html.TextBox("Amount")%>
</p>
<p>
    <label for="destinationAccountId">
      Destination Account:
    </legend>
    <%= Html.DropDownList("destinationAccountId", "Select an Account") %>
</p>
<p>
    <%= Html.AntiForgeryToken() %>
    <input type="submit" value="transfer" />
</p>
<% } %>

When you view source, you’ll see the following hidden input.

<input name="__RequestVerificationToken" 
  type="hidden" 
  value="WaE634+3jjeuJFgcVB7FMKNzOxKrPq/WwQmU7iqD7PxyTtf8H8M3hre+VUZY1Hxf" />

At the same time, we also issue a cookie with that value encrypted. When the form post is submitted, we compare the cookie value to the submitted verification token and ensure that they match.

Should you be worried?

The point of this post is not to be alarmist, but to raise awareness. Most sites will never really have to worry about this attack in the first place. If your site is not well known or doesn’t manage valuable resources that can be transferred to others, then it’s not as likely to be targeted by a mass phishing attack by those looking to make a buck.

Of course, financial gain is not the only motivation for a CSRF attack. Some people are just a-holes and like to grief large popular sites. For example, a bad guy might use this attack to try and post stories on a popular link aggregator site like Digg.

One point I would like to stress is that it is very important to never allow any changes to data via GET requests. To understand why, check out this post as well as this story about the Google Web Accelerator.

What about Web Forms?

It turns out Web Forms are not immune to this attack by default. I have a follow-up post that talks about this and the mitigation.

If you missed the link to the sample code before, you can download the source here (compiled against ASP.NET MVC 2).

Technorati Tags: ,,,

What others have said

Requesting Gravatar... Travis Illig Apr 02, 2009 11:52 AM
# re: Anatomy of a Cross-site Request Forgery Attack
The "POST-only" protection makes a big assumption that rarely gets mentioned (or, at least, emphasized): it assumes the user is navigating the web with a trusted browser.

If the user is not using a secure browser, the anti-forgery token does no good. An insecure browser will allow a cross-domain GET (to retrieve the form) and will allow the results of the cross-domain GET to be inspected. The attacker's client-side script, then, would:

1) Do a GET to retrieve the form.
2) Do some string parsing to find the anti-forgery token.
3) Craft a valid POST request and include the anti-forgery token.

That would yield success even with the token in place.

Granted, most modern browsers are properly patched up and won't let you do that, but is everyone using the latest and greatest? Isn't there a push to rid the world of IE6 right now?

Would it surprise you to know I've seen browser reports including people coming in with the Compuserve browser and Netscape 6.x still?

You can see how the cross-domain GET-then-POST works by adding a "good" site and your test "bad" site both to Internet Explorer's "Trusted Sites" list - if they're both in that list, you can do a cross-domain GET and inspect the results. (No, users shouldn't be adding malicious sites to their trusted sites list, but can you guarantee they didn't? Plus, how many times have you been navigating in Windows Server 2003's locked-down browsing mode and just added the damn site to the list because you just needed to get something done?)

Anyway, the POST-only anti-forgery token solution is a great 80% solution, it just makes a big assumption that it may not be safe for your application to make.

Wikipedia has a good CSRF article talking about different prevention techniques.
Requesting Gravatar... Sruly Taber Apr 02, 2009 11:56 AM
# re: Anatomy of a Cross-site Request Forgery Attack
Thanks for the post. I was just wondering earlier today what the ValidateAntiForgeryToken was.
Requesting Gravatar... Jeff Apr 02, 2009 12:01 PM
# re: Anatomy of a Cross-site Request Forgery Attack
Great job of breaking down what the problem is and then explaining a way to combat it!
Requesting Gravatar... haacked Apr 02, 2009 12:06 PM
# re: Anatomy of a Cross-site Request Forgery Attack
@Travis good point, and I pointed it out in my post. In fact, it's not just the browser you have to trust. What about browser plugins like Flash? If there's a flaw in them, you could be succeptible.
Requesting Gravatar... Attila Apr 02, 2009 2:11 PM
# re: Anatomy of a Cross-site Request Forgery Attack
For Banking scenarios your duty is to make a call idempotent too. So "subtract 1000, add 1000" is not the request what should it look like, but "change from 2000 to 1000" describing a bank transfer more...I know it's not about the security perspective of the request but it's connecting :-)
Requesting Gravatar... _ Apr 02, 2009 4:32 PM
# re: Anatomy of a Cross-site Request Forgery Attack
the browser _IS_ trusted because the user chooses the browser and does not want to harm himself. if he intentionally did something to make the browser allow such an attack why would he do that? he could open fiddler and issue the request in the first place.
Requesting Gravatar... Travis Illig Apr 02, 2009 5:00 PM
# re: Anatomy of a Cross-site Request Forgery Attack
I noticed the line about the browser being trusted in the post, but it's a pretty key thing.

For the anonymous commenter, the user may or may not have the expertise to properly choose a secure browser. Just because the user has said browser doesn't make it trusted, and they may or may not have done anything to specifically "make it vulnerable." Person buys a computer that comes with IE 5.5 on it and doesn't know about turning auto-updates on or how to do it manually. Doesn't make IE 5.5 the best or most secure browsing experience.

The user may also not have the opportunity to change browsers even if they want to. Some corporate environments have their stuff locked down so even if you know the browser isn't secure, you either use it or you don't browse - no choice.

And how many times have security patches come out for one thing that have broken another?

Like I said, the anti-forgery token for POST requests is a good 80% solution, but it makes a big assumption that can't be understated. Bold, underlined, and italic - it assumes you trust the browser.
Requesting Gravatar... Filip C Apr 03, 2009 12:35 AM
# re: Anatomy of a Cross-site Request Forgery Attack
i think there is a little mistake in your html:

<label for="Amount">Amount:</legend>
Requesting Gravatar... ANaimi Apr 03, 2009 6:51 AM
# re: Anatomy of a Cross-site Request Forgery Attack
Thanks Phill.

Seems to me that we will never be able to protect against such techniques - if the method was crafted enough.

I worked for sometime with a Performance Testing (more accurately stress testing) team before in a corporate environment, who were relaying on Best Money Can Buy tools to automate such scenarios. And we always found a way to do it.

@Travis has a excellent point. All you need is (and sorry for the iteration):
1- Get the HTML containing the form
2- Parse it, get the token/canary
3- Post your evil request

This can be done using plain old JavaScript. Even with an up-to-date browser, you still can achieve all three requirements.

To protect against this advanced JavaScript attack, you can validate the referrer header. But again, using Flex or Silverlight, you might be able to pull it off. (or maybe you're not allowed to change that header?)
Requesting Gravatar... James Apr 03, 2009 4:53 PM
# re: Anatomy of a Cross-site Request Forgery Attack
@ANaimi

Are you sure that's possible with javascript? As Phil points out, XMLHttpRequest will only work for the current domain.
Requesting Gravatar... James Apr 03, 2009 4:58 PM
# re: Anatomy of a Cross-site Request Forgery Attack
Actually having just thought about it some more, I suppose you could use an IFrame to load the remote page and then parse the DOM.
Requesting Gravatar... haacked Apr 03, 2009 9:07 PM
# re: Anatomy of a Cross-site Request Forgery Attack
@James actually, modern browsers protect against that.

@ANaimi, what James said is correct. Browsers don't allow cross domain GET. If you don't believe it, you should create a sample that works against our helpers. We'd love to know about it.
Requesting Gravatar... Rush Apr 04, 2009 11:43 PM
# re: Anatomy of a Cross-site Request Forgery Attack
absolute value FTW. hackers will pay for their dirty deeds!
Requesting Gravatar... magellings Apr 06, 2009 7:49 AM
# re: Anatomy of a Cross-site Request Forgery Attack
Hey Phil. When I do my banking online with IE 7 I "always" delete all content from my browser by going to tools > internet options > delete > delete all. I have to assume doing this, since am deleting all cookies, defends against a CSRF attack?
Requesting Gravatar... reece Apr 07, 2010 8:47 AM
# re: Anatomy of a Cross-site Request Forgery Attack
download link doesn't work
Requesting Gravatar... Dixin May 26, 2010 2:44 AM
# re: Anatomy of a Cross-site Request Forgery Attack
I just blogged some ideas on this topic.

Anti-Forgery Request Recipes For ASP.NET MVC And AJAX
http://weblogs.asp.net/dixin/archive/2010/05/22/anti-forgery-request-recipes-for-asp-net-mvc-and-ajax.aspx
Requesting Gravatar... Ingo Lundberg Jul 16, 2010 1:01 AM
# re: Anatomy of a Cross-site Request Forgery Attack
The 2nd time you show post payload, it says
Amount=1000&destinationAccountId=3

I thought it should say
Amount=1000&destinationAccountId=2

2 == id of BadGuy

/ingo
Requesting Gravatar... haacked Aug 06, 2010 12:44 PM
# re: Anatomy of a Cross-site Request Forgery Attack
@Ingo you sir, are absolutely correct! Thanks for commenting. That was a bad typo.

What do you have to say?

(will show your gravatar)
Please add 3 and 8 and type the answer here: