Cross-domain XHR, Access-Control, preflight
It looks like my previous post about the browsers sending OPTIONS request instead of GET has nothing to do with Dojo, which got quite obvious as I saw Prototype is also behaving the same way. I’ve researched about the topic and here’s my insights.
It turned out that some new specifications were implemented in IE8, Safari 4, FF 3.5 and Chrome which allows you to do cross-domain XHR. Which means the pure JS implementation I have demonstrated wasn’t supposed to work at all unless this new specification was implemented. Here’s what the old XHR spec has to say about cross-domain (cross-origin) requests. Taken from http://www.w3.org/TR/XMLHttpRequest/#the-open-method
If the origin of url is not same origin with the
XMLHttpRequestorigin the user agent should raise aSECURITY_ERRexception and terminate these steps.
Not allowing cross-domain XHR was and is really a deal breaker and actually it pretty much stops you from implementing SOA (service oriented architectures) flexibly. But for some good reasons.
Here are a few theoretical scenarios:
- Imagine you are visiting attacker.com which serves a script that requests bank.com/?action=money_transfer&to=attacker&amount=999999. Assuming you have an active session with the bank, if your browser sends this request to bank.com along with the session cookie, attacker would be able to transfer money to himself. This is called CSRF (Cross-site request forgery)
- Imagine you are visiting attacker.com which serves a script that requests 10.0.0.50/confidential_intranet_document.html and sends it to himself via script. This means any client in the trusted LAN network might leak information from the LAN to internet.
- Imagine you are visiting trusted.com which happens to have a security hole so that the attacker can inject malicious code in its web pages. For instance, imagine you could embed Javascript in the messages in Facebook. When other users see that message and the Javascript code you injected works on their browser, you could read their cookies, hence steal Facebook session. This is called XSS (cross-site scripting).
Though there are other transport mechanisms, such as <script> element which is not restricted by this Same Origin Policy. These mechanisms were used instead of the obvious XHR method to achieve cross-domain requests so far. Though these elements are restricted in their own ways, see below for more detail.
There is a new specification being drafted to address these issues, http://www.w3.org/TR/access-control/ which is the reason why OPTIONS request was being sent instead of GET in my previous post. The new spec says that it is OK to send a simple request (which is defined as GET, HEAD and POST) cross-domain as long as there’s no custom header in it. If these conditions are not met, there should be a preflight request to ensure that the domain we’re requesting the document from allows us to fetch it — much like Flash’s policy file.
Note the custom headers clause above. That’s the exact reason why Prototype and Dojo was causing an OPTIONS request instead of GET, where regular JS was simply sending GET request. Dojo and Prototype adds custom headers to the requests.
So you might ask; cross-domain XHR was not allowed for a good reason, why is it being allowed now ?
Yes, cross-domain XHR is allowed now, but apparently no different than cross-domain requests you can send via img or script elements. Remember that you could always do cross-domain requests with img element too, but img element has two features that makes it not a security problem:
- img only can send the cookies for the domain it is loaded from. i.e. it is hard to use a remote session since it won’t send the target site’s cookie.
Consider the first scenario above. If the request does not include a cookie for the bank.com, there’ll be no session. It will be a anonymous request. (Of course unless the target site uses session ID as a part of the URL, and the attacker got that SID, which is very unlikely. And if he has the SID he’ll hijack your session all together anyway). - You cannot read the contents of an img element, hence you cannot steal sensitive information which you aren’t supposed to read.
Now, I have demonstrated myself in my previous post that cross-domain XHR worked out fine. My server received the GET request. BUT in the client xhr.responseText was empty and xhr.status was 0 (not 200). It is true that the request was actually made, but you cannot read the contents of the resource. Here’s what access-control spec says about this in http://www.w3.org/TR/access-control/#requirements
- Should not allow loading and exposing of resources from 3rd party servers without explicit consent of these servers as such resources can contain sensitive information.
One of the requirements of the spec is not to expose resources without explicit consent. From what I understand, here, explicit consent means Access-Control-Allow-Origin header. If the third party server allows other hosts to read its resources via this header, everything will be fine. So, this means that the new XHR is no security hole bigger than the IMG itself.
In fact, I’ve tested this. It turns out that when you add this header to your resource, cross-domain XHR starts to work to the fullest. i.e. you can read the content of the requested resource, as in, it is readable in xhr.responseText.
For your information, you can add any headers to your resources with mod_header module of Apache httpd. Just add this directive for whatever directory you want;
Header set Access-Control-Allow-Origin "*"
Keep in mind that, this will expose all of your resources in that directory for anyone to read. So, do this if your resources are public anyway. Or just allow the hosts you want. It could be better to do this in the programming layer, such as PHP or ASP.NET.
So in conclusion, with the new access-control spec, XHR is pretty similar to the Flash’s security design. Browser checks if the third party host allows you to read your resources, if so your script is allowed to read it. Note that you can make the request anyway, but reading the resource is not allowed.
This is a nice step forward actually, but since it will take some time that majority of the market is using browsers implement this new spec, web developers are bound to use iframe or script transports for cross-domain request.