Understanding CORS: Cross-Origin Resource Sharing

This article aims to provide a straightforward overview of what CORS is, and the reasoning behind it.

Background

AJAX and Cookie-Based Authentication

Thankfully, we don’t have to enter a username and password on every visit to most websites in order to interact with our personal data. Instead, logging in once results in some indicator of our identity being stored by the browser in a cookie. Subsequent requests automatically include cookies associated with the site in question, granting the same access we acquired with our initial login.

Relatively few of the HTTP requests issued by complex web applications are the result of typing a URL into a browser or clicking a link. Most are sent asynchronously via AJAX as we interact with the elements within a page. These requests may also include credentials stored in the browser’s cookies in order to authorize access to private or account-specific data.

Cross-Site Request Forgery

Since credentials may be included in AJAX requests once a cookie is stored in the browser, a whole world of devious possibilities seems to be available. What’s to stop me from creating a website that sends an AJAX request to Pizza Hut with the message “the currently logged in user orders one million pizzas”? As long as a visitor to my malicious site has credentials for their Pizza Hut account stored in their browser, shouldn’t my scheme work?

In some specific cases, historically anyway, the answer was yes. Such an exploit is called a cross-site request forgery (CSRF or XSRF). It’s a particularly nasty trick because if Pizza Hut were to review their logs, all they would see was that a valid request was made by a logged in user with good credentials.

The Same-Origin Policy

Exploits such as CSRF are prevented in most cases by the same-origin policy. The same-origin policy is a rule enforced by web browsers that prevents scripts originating in one domain from making requests to another. Clearly, any pages served by pizzahut.com should be able to communicate freely with any other resources that originate within pizzahut.com. But, in accordance with the same-origin policy, scripts served from the domain of my malicious site would not be allowed to send requests to Pizza Hut.

Cross-Origin Resource Sharing

JSONP

One ill-advised way to work around the same-origin policy—and thereby negate its benefits—is to make use of JSONP. While the same-origin policy restricts AJAX requests across domains, remote scripts loaded via the src attribute of script elements are not so restricted. Instead of requesting pure JSON data, using JSONP one would request an actual string of JavaScript that passes the desired JSON data into a local function.

The dynamically generated script loaded by the second element would be something like:

logMessage({
message: 'Hello world!'
});

It should be fairly obvious that running external scripts introduces a variety of security vulnerabilities for the client. And since cookies are sent along with the request for a JSONP script, the potential for CSRF is reintroduced. Instead of circumventing the same-origin policy, it’s best to work with it using the tools of CORS.

CORS

If the same-origin policy was set in stone and without exceptions, much of the modern web would not work. Many web services are designed specifically to be consumed by scripts running on other websites. This is achieved safely using the conventions associated with the subject of this article: cross-origin resource sharing (CORS).

The general principle of CORS is simple: resources available on the web may define their own allowable exceptions to the single-origin policy. These exceptions are coordinated through the use of specific request and response headers:

Request headers - Origin

Access-Control-Request-Method

Access-Control-Request-Headers

Response headers - Access-Control-Allow-Origin

Access-Control-Allow-Credentials

Access-Control-Expose-Headers

Access-Control-Max-Age

Access-Control-Allow-Methods

Access-Control-Allow-Headers

Let’s say we wanted to provide a service at http://orlandotemp.net/temp that responds to AJAX GET requests from the external site example.com with a JSON object containing the current temperature in Orlando, FL. All we need to do to enable this cross-domain resource sharing is ensure that the requests from example.com include the header:

Origin: example.com

(which they will by default), and that our responses include the header:

Access-Control-Allow-Origin: example.com

This is the basic mechanism of CORS: requests include headers indicating what is being requested, and responses include headers indicating what is allowed. If there is agreement, the cross-domain request is permitted.

In this example, since we are only dealing with GET requests for public information, we can open our service up to be used by any external domain by responding with the header:

Access-Control-Allow-Origin: *

However, if we wanted to allow incoming AJAX requests to include the browser’s cookies containing user credentials in order to access private, account-bound data, we would need to respond with the additional header:

Access-Control-Allow-Credentials: true

As you can see, the sometimes annoying configuration of Access-Control headers provides fine-grained control over exceptions to cross-origin constraints.

Preflight

Sending a GET request along with an access token in order to fetch private information is certainly powerful, but it’s the type of thing we do regularly when typing a URL into a browser’s address bar. GET requests should have zero risk of resulting in the destruction or manipulation of server-side data. Other types of requests—PUT, DELETE, certain POSTs, etc.—are inherently riskier, and therefore require an additional security measure when using CORS.

Instead of making an AJAX request directly and checking the relevant headers, a web browser making more sensitive requests will first send a “preflight” request: a request using the OPTIONS method that essentially asks permission before sending the more sensitive request upon confirmation.

For example, if one were to send an AJAX DELETE request to another domain, the browser would first send an OPTIONS request with the header:

Access-Control-Request-Method: DELETE

If the response contained the header:

Access-Control-Allow-Methods: DELETE

along with an appropriate Access-Control-Allow-Origin header, then the desired DELETE request would be sent.

Conclusion

There are, of course, many more details associated with cross-origin resource sharing and the reasoning behind it. You may wish to read the official W3 recommendation, which includes all of the technical specifications. Hopefully, this article has provided a conceptual overview that will enable you to deal confidently with CORS going forward.