Saturday, 20 February 2016

Playing with CORS

I recently had reason to get to understand CORS in a bit more detail and I wanted to share what I learned. I did this with ASP.Net 5 / MVC6 but the principles apply more broadly.
This post is not about how to set up the built-in CORS middleware, but rather about how to handle it yourself when you want and need to. Also, I am deliberately simplifying to make this just an introduction.
If you just want to get CORS handled by default, Microsoft has a great article on how to do this in ASP.Net 5 / MVC 6.

The biggest lesson

The biggest lesson learned for me was that CORS does not (always) stop cross-origin requests from being sent to and processed on my server. It does stop the browser from giving the result back to the calling javascript.
What this means is that if you have a controller action in your ASP.Net application (and presumably the same in other frameworks) then it is entirely possible for javascript on a different site to call that API method and the code on your server will execute - it's just that the result will never get back to the calling javascript . Most of the time this is fine, assuming you are securing your controllers appropriately - but it is different from what I expected.

Broad-brush CORS

CORS is a security mechanism implemented in modern web browsers. In it's simplest terms, if you load a page from domain A and there is some javascript on that page which tries to get or post data to domain B then the browser will (usually) send that request over to domain B, but when it gets a response it will look for a set of headers that specify which domains are allowed to call services on domain B (along with some other restrictions). Note that the server will, by default, happily have processed the request and will have sent the result back - but the browser will check the headers that came back to see if the rules have been met and, if not, will discard the result and not give it back to the calling javascript.

There is an important variation to this. Under certain circumstances, the browser will do a "preflight check" before it does the actual request. What this means is that the browser will send a request to domain B with a method OPTIONS (instead of GET or POST or whatever), in which it basically says to the server on domain B "hi, I'm just about to send you a request that looks like this. Can you just tell me which CORS headers you'd return if I did that?". If configured correctly, the server on domain B will respond with the appropriate headers and the browser will determine if it should send the actual request - or not.
There are a range of scenarios in which this preflight check will take place, usually if custom headers are involved or you try to send content other than plain text or form encoded (such as JSON or XML). But, it is important to understand a few things;

  1. There are scenarios where the preflight check will not take place so you cannot rely on the preflight check to keep you safe. In those cases, your controller will be called with the full payload and will execute normally - even if you install the CORS middleware.
  2. CORS only really applies to requests from browsers so it doesn't, in any case, help against people who are attacking you directly.
  3.  If you do not have CORS middleware configured, preflight checks are passed to your controller actions. See below.

Pre-flight checks without CORS middleware installed

Let's say you have a simple API method like this;
[Route("blah")]
[HttpPost]
public IActionResult Post(Model data)
{
    //Do stuff
}        
Now imagine a preflight check comes in. It will have a method of OPTIONS so you may assume it won't hit your controller - I mean you got that nice HttpPost attribute there, right? But part of the preflight check information is that it wants to do a POST, so the call will be passed to your controller - except without any body. So, if you are not using CORS middleware you may want to explicitly handle that situation like so;

[Route("blah")]
[HttpPost]
public IActionResult Post(Model data)
{
    if (this.Request.Method == "OPTIONS")
    {
       return this.Ok(); // Well, probably not what you want to do, but you get the idea
    }
}        

In conclusion, even if you don't expect to have to support CORS, install the middleware and have one less thing to worry about.

Manually handling CORS

I have a scenario where I want to manually handle CORS. At NewOrbit we do a number of static marketing sites for people to help market the apps we build for them. Usually they need to have a contact form which means we build a quick bit of server-side code to do that - for each site. http://formspree.io/ was pointed out to us as a way to make it more simple - and a very nice solution it is. But I was curious about whether we could build something like that ourself, just for fun. Formspree allows you to do just do a FORM POST, which doesn't involve Origin headers (well, Chrome sends it but Firefox doesn't) I wanted to be able to control which sites were able to send emails via the service, which I can do by handling CORS myself, directly in my controller. This does mean the clients can't just do a FORM POST, they'll have to use javascript though.
Some benefits of this include;
  • I can dynamically add new sites that are allowed.
  • When responding to requests, I can just return the one site that is actually asking, rather than a list of all the allowed sites.
My implementation looks something like this;
public IActionResult Post([FromBody]dynamic data)
{
    var originHeader = this.GetOriginHeader();
    if (!this.IsValidOrigin(originHeader))
       return new ContentResult() { StatusCode = 403, Content = "Invalid origin" };
    }

    this.AddCORSHeaders(originHeader);
    if (this.Request.Method == "OPTIONS")
    {
        return this.Ok();
    }

    // Chrome sets the Origin header on a cross-domain post. Sadly, Firefox doesn't, so I can't allow plain form posts.
    return new ContentResult() { StatusCode = 200, Content = this.Request.Method };
}

private void AddCORSHeaders(string originHeader)
{
    this.Response.Headers.Add("Access-Control-Allow-Origin", originHeader);
    this.Response.Headers.Add("Access-Control-Allow-Methods", "POST");
    this.Response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
}

private string GetOriginHeader()
{
    var originHeaders = this.Request.Headers["Origin"];
    if (originHeaders.Count != 1)
    {
       return null;
    }
    return originHeaders[0];
}

private bool IsValidOrigin(string origin)
{
    if (String.IsNullOrEmpty(origin))
    {
       return false;
    }
    return origin == "http://localhost:5001";
}

No comments:

Post a Comment