Anatomy of a Cross-site Request Forgery Attack

asp.net, code, asp.net mvc 0 comments suggest edit

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: https://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: aspnetmvc,asp.net,csrf,security

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

30 responses

  1. Avatar for Travis Illig
    Travis Illig April 2nd, 2009

    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.

  2. Avatar for Sruly Taber
    Sruly Taber April 2nd, 2009

    Thanks for the post. I was just wondering earlier today what the ValidateAntiForgeryToken was.

  3. Avatar for Jeff
    Jeff April 2nd, 2009

    Great job of breaking down what the problem is and then explaining a way to combat it!

  4. Avatar for haacked
    haacked April 2nd, 2009

    @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.

  5. Avatar for Attila
    Attila April 2nd, 2009

    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 :-)

  6. Avatar for _
    _ April 2nd, 2009

    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.

  7. Avatar for Travis Illig
    Travis Illig April 2nd, 2009

    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.

  8. Avatar for Filip C
    Filip C April 2nd, 2009

    i think there is a little mistake in your html:
    <label for="Amount">Amount:</legend>

  9. Avatar for ANaimi
    ANaimi April 2nd, 2009

    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?)

  10. Avatar for James
    James April 3rd, 2009

    @ANaimi
    Are you sure that's possible with javascript? As Phil points out, XMLHttpRequest will only work for the current domain.

  11. Avatar for James
    James April 3rd, 2009

    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.

  12. Avatar for haacked
    haacked April 3rd, 2009

    @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.

  13. Avatar for Rush
    Rush April 4th, 2009

    absolute value FTW. hackers will pay for their dirty deeds!

  14. Avatar for magellings
    magellings April 5th, 2009

    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?

  15. Avatar for reece
    reece April 6th, 2010

    download link doesn't work

  16. Avatar for Dixin
    Dixin May 25th, 2010

    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

  17. Avatar for Ingo Lundberg
    Ingo Lundberg July 15th, 2010

    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

  18. Avatar for haacked
    haacked August 6th, 2010

    @Ingo you sir, are absolutely correct! Thanks for commenting. That was a bad typo.

  19. Avatar for BlueRaja
    BlueRaja December 4th, 2010

    Phil,
    Just out of curiosity, is there a reason this isn't done by default, as it is in many other frameworks?
    It seems like the small cost in performance would be worth it for the increase in security - most security holes are caused by someone not knowing or not remembering to do something like this, so the less they have to know/remember to do, the better.

  20. Avatar for Saeed Neamati
    Saeed Neamati October 10th, 2011

    Great explanation about CSRF. To understand it even further, I think I'm going to implement an Anti-Forgery framework on my own. The link to the other article (from blog.codeville.net) was also very helpful. Thanks.

  21. Avatar for Pat James
    Pat James October 11th, 2011

    Given the caveat that this method protects you only in scenarios where the client is a "trusted browser", I would suggest that the presence of a RequestVerificationToken hidden form field is a bright burning beacon announcing to any motivated blackhat "here I am, I have something of value I thought was worth trying to protect". Any motivated hacker will be aware of the flaw in this protection, and certainly won't need to rely on trusted browsers as tools for his attack.

  22. Avatar for haacked
    haacked October 11th, 2011

    @Pat, well if the blackhat can get an untrusted client out there, why even bother with CSRF? CSRF is the least of your worries. If you've compromized the client, go to town directly. Scan the hard-drive. Etc.

  23. Avatar for Zeeshan Umar
    Zeeshan Umar January 17th, 2012

    Interesting article, I was looking for it for quite some time.

  24. Avatar for Fernando Correia
    Fernando Correia April 1st, 2012

    After all this time, Microsoft is still providing templates for code generation that output code that is vulnerable to CSRF. stackoverflow.com/...

  25. Avatar for reject all get request
    reject all get request May 17th, 2012

    can we reject all get method from asp.net 2.0

  26. Avatar for vishnu rao
    vishnu rao September 19th, 2012

    very nice article.
    i had a doubt, will the semantics described above change if the channel is secure - i.e. https:/127.0.0.1:54607 instead of http.
    is the attack avoidable by removing the cookie at all costs ?

  27. Avatar for Sudharsan
    Sudharsan March 15th, 2014

    I am getting error in this line " Account account = _context.Accounts.First(a => a.Username == username); " The error is:InvalidOperationException was unhandled by use code. Could you please let me know how to rectify it.

  28. Avatar for Daniel Vieira Costa
    Daniel Vieira Costa May 5th, 2015

    Does the __RequestVerificationToken has expiration?

  29. Avatar for Mani
    Mani December 14th, 2015

    hello sir .I take Detection and prevention of CSRF project....so i need some papers for this project...if you done this project...pls send me this project....plz sir.....

  30. Avatar for Mani
    Mani December 14th, 2015

    hello sir .I take Detection and prevention of CSRF project....so i need some papers for this project...if you done this project...pls send me this project....plz sir.....plz sir