Same-origin access control – how to implement it and why it matters
Did you know that you can host a JavaScript application on the same domain as an API, but still have the API treat requests from the application as cross-origin?
First, some background.
Cookies
On the web, a cookie is a name-value pair that is sent from a web site to a browser, stored, and sent back on subsequent requests to the same web site. If you’re in Europe (or even if you’re not), you’ve probably seen the endless notices on web sites advising you that cookies might be used for targeted advertising – but they also have other, more benign uses. Notably, they’re often used in authentication. When you log in to a web site, it gives you a cookie with a unique value by which it can recognize you on subsequent requests, so that you don’t have to log in again with every click.
Cross-site requests
Cookies work for tracking and targeted advertising because they can be sent with almost any request to a web site – not just requests that the user makes explicitly. For example, a page might be hosted on https://www.example.com/ but might also load some scripts from a content delivery network (CDN), fonts from a font service, and advertising from an ad network, each with their own domain names. If the user already has cookies from those domains, the browser will (by default) send the cookies back with those requests. The responses can also set new cookies for their respective domains. If different sites use the same ad network (or CDN etc.) then users can be re-identified and tracked across those sites.
Same-origin policy
This poses an immediate problem if cookies are also used for authentication. A third-party web site could use JavaScript to make a user’s browser send a request to a private site where the user is already logged in. The browser would include any existing authentication cookies with that request. It would seem that this would allow the third-party site’s JavaScript to read the user’s private content! Fortunately, browsers protect against exactly this by enforcing a policy (or at least, a set of very similar policies) known as same-origin policy. JavaScript running on a third-party web site (or origin, usually defined as domain + port + protocol) can make requests with cookies, but can’t read the responses by default; and some types of requests are blocked entirely.
Cross-site request forgery
But what about POST requests? These are allowed by same-origin policy. However, in this case, web developers have to be a bit more careful: if a request has side effects (such as writing data to a database) – as POST requests generally do – then preventing a third-party domain from reading the response won’t be enough: you also want to prevent the side effects from occurring. What we usually do (and what Drupal does) is to require an additional piece of secret information – a token – in any request with side effects. This token is obtained from the response to a previous authenticated request. For example, in an HTML form, it’s included as a hidden field. This means you need to be able to send an authenticated request and read the response in order to obtain the token. Third-party server-side code won’t have a valid cookie to make an authenticated request; and while JavaScript running on a third-party domain can make the request, it won’t be able to read the response, thanks to same-origin policy.
Cross-Origin Resource Sharing
Sometimes, you don’t want these restrictions. For example, maybe you want to provide an API for use from JavaScript on any web page. You can use one or more of a set of HTTP headers defined by the Cross-Origin Resource Sharing (CORS) protocol. These headers instruct the browser to relax its same-origin policy for a particular request. This might make sense if the API doesn’t require authentication, or if it uses some authentication mechanism other than cookies that’s known to be safe for cross-origin use. CORS is safe (if used correctly) because it’s under the control of the server, and the server operator knows which requests are safe to handle without same-origin policy being enforced.
Content Security Policy’s sandbox directive
With all that out of the way, let’s return to the original goal: to host a JavaScript application on the same domain as an API, but still have the API treat requests from the application as cross-origin.
Content Security Policy (CSP) is widely known as a technology for limiting JavaScript injection to prevent cross-site scripting attacks, but that’s not all it can do.
CSP works by specifying an HTTP header, Content-Security-Policy, that can be used to specify directives that instruct the browser to place certain restrictions on a page. One such directive is sandbox. This directive allows you to restrict the current page as if it were being loaded in an <iframe> with the sandbox attribute. This enforces a number of limitations which can then be lifted individually – for example, the page can’t run any scripts (unless you say so), it can’t display pop-up windows… and it can’t make same-origin requests. That is: any requests it makes will be treated as cross-origin, even if they’re actually requests to what would otherwise be considered the same origin.
This could be useful if you want to build apps that use your API and host them on the same domain as the API and other parts of your web site, but still want them to be subject to the same security restrictions as apps running on third-party domains.
This even works in Internet Explorer. IE doesn’t support most of the CSP standard, but it does support the sandbox directive, at least in some versions. However, it expects the HTTP header to be named X-Content-Security-Policy rather than the standard Content-Security-Policy, so you need to specify both.
Of course, it’s still possible that some users might be using very old versions of IE, or other browsers that don’t support CSP (though there aren’t too many left); so this shouldn’t be considered a strong security control suitable for use with entirely untrusted code, but it could serve well as a defence-in-depth measure.