Forging a Defense against Cross-Site Request Forgery

November 29, 2016

Modern web development is a constant battle against fire-breathing dragons and undead zombies! Ok, maybe that description is a bit of an exaggeration, but we certainly fight our fair share of security pitfalls and shady agents looking to utilize weak links for nefarious purposes. One small mistake is all it takes to cause data to be leaked, money to be lost, and customers to become unhappy 😞 (sad customer). But like the heroes of olde, we must not become faint of heart. Rather let us take up the keyboard and coffee mug, and forge our names among the stars (or at least on Stack Overflow)!

638Px Greek Soldiers Of Greco– Persian Wars
Programmers of the Greco-Persian Wars

This post will assist you in your quest by describing a major security vulnerability in your everyday web browser: Cross-Site Request Forgery (CSRF). This definition by OWASP Top 10 Web Security Vulnerabilities explains the attack well:

“A CSRF attack forces a logged-on victim’s browser to send a forged HTTP request, including the victim’s session cookie and any other automatically included authentication information, to a vulnerable web application. This allows the attacker to force the victim’s browser to generate requests the vulnerable application thinks are legitimate requests from the victim.”

Or in other words:

“If you are logged into a website, and that website is vulnerable to CSRF, then just by navigating to another website with evil intent, your browser can be tricked into sending any command which you are authorized to perform by that login.”

That statement would normally be enough to keep you up at night, but never fear! By the end of this post you will be armed with the knowledge to defend yourself and your customers 🙂 (happy customer) from such a beast. In order to help prepare you in your fight against the Foes of Good, I have created this GitHub repository, which contains an ASP.NET project demonstrating CSRF attacks and how to prevent them. Feel free to download and try the attacks for yourself.

The Hero Ajax

11 Flaxman Ilias 1795 Zeichnung 1793 186 X 283 Mm
Ajax the Greater battling against Cross-Site Request Forgery.

One key downsides of using traditional HTML forms was that clicking submit would cause the browser to refresh the entire page or frame. Essentially, forms were customizable links that built upon the traditions of the original web concept. This was fine for early days of the web, but internet speeds increased and users began to expect a better web experience. If only there were a way of making smaller request and update the page dynamically...

Enter Ajax the Greater! He was a mighty Greek warrior-king from the Iliad who fought valiantly with both body and mind. His legacy has carried through the ages even into the modern day, in the form of asynchronous JavaScript and XML (Ajax). Modern web pages make use of Ajax for making small requests to web servers without reloading the entire page. There are several different HTML elements which allow for implementing Ajax:

  • <script> - Script tags allow for embedding JavaScript into a web page. Modern JavaScript provides a special object called XMLHttpRequest, which as its name implies can be used to send arbitrary HTTP requests (ignore the XML part). Indeed, when most programmers hear Ajax, they think of XMLHttpRequest or one of the wrappers provided by various JavaScript libraries such as jQuery. For example, here is how you could make a jQuery Ajax request:

$.ajax({
  url: url,
  data: { },
  type: 'post',
  contentType: 'application/json',
  dataType: 'json',
  headers: { },
  xhrFields: {
    withCredentials: true,
  },
})
.then(
  function (response) {
    console.log(JSON.stringify(response));
  },
  function (jqXHR, textStatus, errorThrown) {
    console.log(textStatus + " - " + errorThrown);
  }
);
  • <form> and other form elements (<input>, <select>, etc.) - While submission of form tags normally reload the page, programmers can selectively override this behavior and instead send the data via JavaScript. This allows for utilizing only the data-gathering aspect of form elements.

<form method="post" enctype="application/json" 
  onsubmit="event.preventDefault(); return submitFormAjax();">
  • <img> - Image elements load asynchronously from the rest of the content of a typical web page. This was designed intentionally because images are often an order of magnitude larger than HTML text, and it leads to a better user experience to load the content and allow users to begin reading rather than wait until all images are finished downloading. An interesting side effect of this asynchronous nature of images is that JavaScript can insert new image tags, whose source is coded to make a GET request (possibly with query parameters). The server can receive this request and return a small 1x1 pixel image to signal a satisfactory response. While not appropriate for receiving arbitrary response data, this technique is useful for sending data to the server, and is often used for "tracking" purposes. This technique is known as tracking pixel or tracking bug, among other names.
  • <iframe> - An iframe can be used to load up an entirely new HTML page within the current page. This new page will be contained within the iframe, so its loading does not cause a reload of the original page. While the "same-origin" policy of web browsers prohibits the original page from reading this page's data if the origins (protocol, domain, port) do not match, it can still be useful if the new page is on the same origin or if the purpose was to simply cause the request to be made without regards to the result (for example, if you are a sneaky attacker using cross-site scripting).

Now that you have a high-level overview of the web request options, let’s dive a bit deeper and see how these requests hold up under the heat of battle. 

The Battle Begins

If you download the project from GitHub and build it, you will find a website with various links and a simple form.

Csrf Sample App Index

On the top menu-bar, you will notice that I have a link called "Secret". This link has been protected by authorization, so it is required that the user logs in before viewing the secret page.

Csrf Sample App Secret

On the home page, a form has been embedded as an iframe. It could just as easily have been written directly into the page, but I also want to access the form from another domain and did not want to write the form twice. Since this form is loaded from the same origin, you can click the two buttons to submit a request to the Secret page. Whether you submit the form via Ajax or normal form submission, it should succeed as long as you are logged in.

Csrf Sample App Secret Form Success

So far all is as it should be, but what happens if we were to access our page from from another origin? At the bottom of the page, I display the local filesystem URL to the form. Right-click this link and select "Copy Link Address". Then open a new tab and paste the url in the address bar. This will open the form in a different origin by changing the protocol to "file:///". You cannot click this link directly because the browser will not open local files from origins using the "http://" protocol.

Now begins the scary part. In this local file domain, you will notice that you can still perform normal form submission and access the Secret page!

Csrf Sample App Secret Form Success Cross Site

As a minor relief, submitting via Ajax fails because Cross-Origin Ajax calls are disabled by default in modern browsers.

Csrf Sample App Secret Form Fail Cors

However, if you explicitly allow Cross-Origin Ajax via CORS (Cross-Origin-Resource-Sharing), then the same security hole is opened up.

Csrf Sample App Secret Form Success Cors

So, we can clearly see that our site's secret data can be accessed by other domains! How is this possible, and what can we do about it?

Post-Battle Analysis

Let us break CSRF down into the separate elements which enable the attack:

  • You must be logged into a website. The crux of the attack lies in the fact that web browsers remember and send authorization credentials without explicitly asking the user for them every time. It would be obvious that something isn’t right if you navigated to www.evil.com, but your browser presented a login dialog for www.mybank.com. However, for practical reasons we need our browsers to remember and send authorizations on our behalf, so the best we can do is improve the detection of when an authorization is sent from a real user action and not by an attacker.
    • It is important to note that authorization can come in several formats: stored cookies, HTTP authorization (simple, digest), etc. The common theme among these authorizations is that they are sent along automatically by the browser when making the HTTP request. There do exist other authorization techniques which will not be sent automatically, and I will present at least one later in this post.
  • The website must be vulnerable to CSRF. If the server-side code takes proper precautions, an invalid cross-domain request can be detected and stopped. However, this requires that the server-side developer be aware of this attack and put forth the extra effort. Additionally, client-side development must be done to allow legitimate users to login to the system. If you enable CSRF protection but do not change your client-side JavaScript, then no one will be able to login.
  • You must navigate to an evil site. While it would be nice if we could simply tell our users to not visit www.evil.com, the truth is that potentially any website could allow the attackers to take advantage of CSRF. Many high-profile websites have Cross-Site Scripting (CSS) vulnerabilities which allow for insertion of form, image, iframe, and script tags. These tags can be used to make requests to any website of the attacker's choosing.
    • Note that Cross-Site Scripting (CSS) a different security flaw and worthy of its blog post on another day.

False Hope

There are several common misconceptions on how to protect against CSRF:

  • HTTPS - Securing your site with an SSL certificate does not prevent the attack because this only protects the data in-transit. It does not do anything to prevent the attacker from the making the request and your browser fulfilling it.
  • POST - Some people think that forcing all server controller actions to use POST will solve the issue. While this does cut down on attack surface by preventing GET requests, it does not completely solve the problem, since attackers can still make POST requests via forms or JavaScript. Also, you may still want to enable GET requests for various reasons, so you may not be able to do this anyway.
  • HTTP-only cookies - While HTTP-only cookies would prevent an attacker from reading your login credentials directly via cross-site scripting, they may still send a request and your browser will happily send the cookies along.

Veni Vidi Vici

Stele Of Vultures Detail 01A
Ancient Sumerians forming a Secure Credentials Phalanx

Now that your basic training on CSRF is complete, I can present mitigation techniques for ASP.NET which will take your security-smithing to the next level.

The first technique is for handling ASP.NET MVC controllers. Use Microsoft's provided HtmlHelper.AntiForgeryToken method within your forms. This will generate a hidden input field called "__RequestVerificationToken" that will store a token, and also cause a cookie token with the same name to be set.

@Html.AntiForgeryToken();

Csrf Sample App Secret Antiforgerytoken

Then, on your MVC controllers or actions which handle the form, place a [ValidateAntiForgeryToken] attribute. This will automatically check that the cookie token matches the hidden form token, and throw an exception otherwise.

[Authorize]
[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)]
[ValidateAntiForgeryToken]
public ActionResult SecretWithCsrfProtection()
{
    ViewBag.Message = "The secret page.";

    return View();
}

Click the red and green buttons titled "Secret w/ CSRF" at the top of the app to see the protected page deny or accept the form using this protections scheme.

Csrf Sample App Secret Antiforgerytoken Fail

The second technique is for handling ASP.NET Web API controllers. In this case, you do not have the opportunity to embed the tokens automatically, so you must do a little extra work. While I mentioned earlier that we need the browser to remember and send credentials without requiring explicit login every time, that does not mean we must rely on the browser’s default mechanisms for handling this. Instead, use a combination of client-side and server-side code to verify that the request is coming from the user on the proper origin.

One way to accomplish this is to manually attach the credentials to the HTTP request header or body. The credentials still need to be stored somewhere on the client (Local Storage, Cookies, JavaScript memory), but regardless of where they are stored, the key is that those credentials could only be read by JavaScript on the same domain. You can see in my sample form that I provide a field for entering the headers to be sent, which will be attached to the request via JavaScript.

Csrf Sample App Headers

On the server-side, check that the right credentials appear in the HTTP request header or body. If we see the credentials there, then we will know that this is a legitimate request from the user within the confines of our website. To perform the check, you could create a custom ActionFilter which can be applied to any controller or action which should be protected. I will take the easy approach and make use of the same Microsoft ASP.NET MVC methods for checking the token. Notice that the token to be inspected was simply a concatenation of the cookie and form tokens with a ":" in-between. This approach was taken from the asp.net documentation, but other approaches are possible for generating and validating the tokens.

public class CsrfProtectionFilter: ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        string cookieToken = string.Empty;
        string formToken = string.Empty;
        IEnumerable <string> tokenHeaders;

        if (actionContext.Request.Headers.TryGetValues("CsrfToken", out tokenHeaders))
        {
            string[] tokens = tokenHeaders.FirstOrDefault()?.Split(':') ?? new string[]{};
            if (tokens.Length == 2)
            {
                cookieToken = tokens[0];
                formToken = tokens[1];
            }
        }
            
        try
        {
            // Throws an exception if tokens do not match
            AntiForgery.Validate(cookieToken, formToken);
        }
        catch (Exception ex)
        {
            actionContext.Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
            {
                ReasonPhrase = "Expected valid CSRF tokens",
            };
        }

        base.OnActionExecuting(actionContext);
    }
}

Some additional considerations come into play when you are making an API which should be accessible across domains (also known as Cross-Origin Resource Sharing or CORS). To enable support for CORS in any ASP.NET project, you can always add the expected header yourself:

Response.AppendHeader("Access-Control-Allow-Origin", "*");

To enable CORS in an ASP.NET Web API project, add the nugget package Microsoft.AspNet.WebApi.Cors via package manager or right-click-solution->Manage Nuget Packages. Then add this code to WebApiConfig.cs or other equivalent registration location:

public static void Register(HttpConfiguration config)
{
       // …
    config.EnableCors();
       // …
}

On Web API controllers or actions, add an [EnableCors] attribute with the allowed origins, headers, and methods:

[Authorize]
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class SecretWithCorsController : ApiController {  /* ... */ }

If you want to enable CORS globally across the application, then you can alternatively add the attribute to the HttpConfiguration:

public static void Register(HttpConfiguration config)
{
  var corsAttr = new EnableCorsAttribute("http://example.com", "*", "*");
  config.EnableCors(corsAttr);
  /* ... */
}

I have read that IIS7 has some difficulty with picking up the CORS settings. If you run into any trouble, try adding your settings to the web.config file.

<configuration>
 <system.webServer>
   <httpProtocol>
     <customHeaders>
       <add name="Access-Control-Allow-Origin" value="*" />
     </customHeaders>
   </httpProtocol>
 </system.webServer>
</configuration>

If you want to require authorization with CORS, then make sure that you set the “SupportsCredentials” parameter to true. Also make sure that you change the origins from a wildcard “*” to an explicit white-list of domains which you expect to work with. 

[Authorize]
[EnableCors(origins: "http://myawesomesite.com", headers: "*", methods: "*", SupportsCredentials = true)]
public class SecretWithCorsController : ApiController { /* ... */ }

On the client-side, make sure that your Ajax library of choice is sending the credentials too.

$.ajax({
    url: url,
    data: data,
    /* ... */
    xhrFields: {
        withCredentials: true,
    },
})
/* ... */

In case you were wondering how to allow all origins (“*”) but still require authentication, my answer would be that situation is the very essence of CSRF and should not be allowed. 

Go Forth and Forge

Check out these articles for additional details regarding CSRF:

If you seek further weapons in your daily battle against security vulnerabilities, feel free to message the developers at Sparkhound. While we may appear to be mighty heroes, most of us are approachable mortals who enjoy discussing the latest web development trends and helping our clients achieve their goals.

Until next time, good luck out there and happy forging!

Information and material in our blog posts are provided "as is" with no warranties either expressed or implied. Each post is an individual expression of our Sparkies. Should you identify any such content that is harmful, malicious, sensitive or unnecessary, please contact marketing@sparkhound.com.

Meet Sparkhound

Review our capabilities and services, meet the leadership team, see our valued partnerships, and read about the hardware we've earned.

Learn How We Work

See how our Plan/Build/Run methodology drives real client success, and gain our team's perspectives on timely tech topics.

Engage With Us

Get in touch any of our offices, or checkout our open career positions and consider joining Sparkhound's dynamic team.