Tuesday, January 28, 2014

Turbo API: How to use CORS without Preflights

From official doc on Cross Origin Resource Sharing
header is said to be a simple header if the header field name is an ASCII case-insensitive match for AcceptAccept-Language, or Content-Language or if it is an ASCII case-insensitive match for Content-Type and the header field value media type (excluding parameters) is an ASCII case-insensitive match for application/x-www-form-urlencodedmultipart/form-data, or text/plain.
CORS is really strict about headers. As you can see only Accept/Accept-Language/Content-Language can be replaced with arbitrary field values.

This behavior is only intended to "secure" poorly designed apps, e.g. those ones who rely on X-Requested-With as a CSRF protection

Your app is not poorly designed, right? And you have some API, requiring additional headers, such as Authorization or X-Token.

Every browser is doomed to hit your app with preflights "asking" to use X-Token header in the next, actual request, or to use "special method" like PUT or DELETE. Your API is supposed to respond:

Access-Control-Allow-Origin: http://hello-world.example
Access-Control-Max-Age: 3628800
Access-Control-Allow-Methods: PUT, DELETE

This sucks! Even when you use Max-age to cache headers, it is stored for 5 minutes and only for this exact request, then browser is have to perform useless preflight request again. 

My idea is to bypass this annoying behavior by putting all extra headers you need (and HTTP method) in the Accept (or Accept-Language/Content-Language) header:

x=new XMLHttpRequest;
x.open('post','http://www.google.com');
x.setRequestHeader('Accept', 'Accept:actual-accept-value; Content-Type:application/json; X-Token:123123; HTTP-Method:Put');
x.send('{"json":123}')


Now you only need few lines of code on server side in the beginning of your app:

request.headers["Accept"].split(';').each{|header|
  new_header = header.split(':')
  request.headers[new_header[0].strip] = new_header[1].strip
}

You can routinely monkey-patch setRequestHeader and add few lines of code on the server side. Try to do it and get brand new turbo API!

I proposed to allow CORS-* headers by default. CORS-* headers are not going to be useful to hack currently existing apps, but will remove futile preflight requests. 

To be honest, I would get rid of all CORS headers but one: to perform state-changing requests you need to know csrf_token anyway, to read the response you need suitable Access-Control-Allow-Origin. The rest of headers is just legacy bullshit to "save" already broken apps. There's no point to "allow" headers, nor withCredentials.

I really hope pre-approved headers will be added, because currently CORS sends twice more requests than needed, which makes it slower than alternative cross domain transports & overloads API with pointless payloads.

3 comments:

  1. It would be safer to base64-encode the header you're passing in. Colons in the header values often confuse the header parsers.

    ReplyDelete
    Replies
    1. Good point, but it'd be harder to debug.

      Delete
  2. Also, CORS shines when you're white-listing domains that you don't have control over, i.e. you couldn't use a CSRF token (think Amazon S3 buckets).

    Removing CORS completely there would mean any domain that gets hold of an upload ticket could be used to upload stuff.

    ReplyDelete