DOM Access Control Using Cross-Origin Resource Sharing

Introduction

Same-origin policies are a central security concept of modern browsers. In a web context, they prevent a script hosted at one origin — meaning the same protocol, domain name, and port — from reading from or writing to the DOM of another.

This restriction is sensible and useful most of the time. Without a same-origin policy, a script hosted on http://foo.example could hijack cookie data or sensitive document information from http://bar.example and redirect it to http://evilsite.example.

Sometimes, however, a same-origin policy can be burdensome. Making requests across subdomains, for example, is prohibited by a same-origin policy. You also can't use XMLHttpRequest to pull in JSON data from a third-party API. To make matters worse, workarounds such as JSONP or document.domain can leave us vulnerable to XSS attacks.

What we need, then, is a mechanism for requesting data across origins, but with the ability to deny requests that don't come from the right source. This is the problem that Cross-Origin Resource Sharing (or CORS) solves.

Cross-Origin Resource Sharing is new in Opera 12. Support is also available in Chrome, Safari, Firefox, and the forthcoming Internet Explorer 10.

What is CORS?

CORS is a system of headers and rules that allow browsers and servers to communicate whether or not a given origin is allowed access to a resource stored on another. Understanding CORS is critical to working with modern web APIs. Cross-domain XMLHttpRequest, and Internet Explorer's XDomainRequest object, for example, both rely on it.

CORS consists of three request headers, and six response headers (see Table 1 below). Browsers automatically set request headers for some cross-origin requests, such as those made using the XMLHttpRequest object.

Figure 1: A table of cross-origin resource sharing headers
Request headersResponse headers
Origin: Lets the target host know that the request is coming from an external source, and what that source is.Access-Control-Allow-Origin: Lets the referer know whether it is allowed to use the target resource.
Access-Control-Request-Method: Included when the HTTP method used is one that may cause a side-effect (such as PUT or DELETE).Access-Control-Allow-Methods: Lets the referer know what HTTP methods are allowed, i.e. if the one(s) specified in Access-Control-Request-Method are okay.
Access-Control-Request-Headers: Included when the header is a complex header, such as If-Modified-Since, or a custom header such as Opera Mini's X-Forwarded-For.Access-Control-Allow-Headers: Lets the referer know if the headers it sent are okay.
 Access-Control-Max-Age: Explicitly informs the referer how many seconds it should store the preflight result. Within this time, it can just send the request, and doesn't need to bother sending the preflight request again.
 Access-Control-Allow-Credentials: This tells the host whether the request can include user credentials.</a>
 Access-Control-Expose-Headers: Lets the host know exactly which headers it can expose to the referring application. A header white-list.

Response headers, of course, are returned by the URI in question. You can set them in your server configuration file or per URI using a server-side language. Which approach you choose will depend on the kind of application you're building. We'll cover each response header in the Sending CORS Response Headers section.

Though cross-origin resource sharing is a permissions system of sorts, understand that it is not a form of content protection: it is a form of cross-site scripting protection. Browsers will still complete the HTTP request, but will expose the resulting response body only if the response includes the appropriate headers. You will experience this if you run the CORS demos.

Speaking of running demos, I recommend using an HTTP monitor to observe headers, as built-in developer tools can sometimes mask what's happening under the hood. A good open source choice is Wireshark, and pay-for alternatives include Charles (Mac/Win/Linux; US$50 / ~€38) and HTTPScoop (Mac; €12 / ~US$15)

How browsers make simple cross-origin requests

When a script attempts a cross-origin request, the user agent will automatically include one or more request headers, depending on how the request is formed. If the server or application sends the appropriate response headers, subsequent attempted changes to the DOM will succeed.

Here’s an example. The code below uses XMLHttpRequest to retrieve a JSON-formatted file from http://foo.example. We'll assume that this script is hosted on http://bar.example.

var xhr = new XMLHttpRequest();
xhr.onload = function(e){
  // Build a list and append it to the document's body.
}
xhr.open('GET', 'http://foo.example/data.json');
xhr.send( null );

Now let's look at how Opera and other browsers handle this cross-origin request. What follows is an example of Opera's request headers.

GET /data.json HTTP/1.1
User-Agent: Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.10.238 Version/12.00
Host: foo.example
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en, en-US
Accept-Encoding: gzip, deflate
Referer: http://bar.example/document_making_the_request.html
Connection: Keep-Alive
Origin: http://bar.example

See that Origin header? It lets http://foo.example/data.json know that this request is coming from an external source. Notice too that the Referer and Origin headers have different values, and that the value of Origin does not include a trailing slash.

Now let's look at the URI's response headers.

HTTP/1.1 200 OK
Date: Tue, 04 Oct 2011 00:18:35 GMT
Server: Apache/2.2.20
Cache-Control: max-age=0
Expires: Tue, 04 Oct 2011 00:18:35 GMT
Vary: Accept-Encoding
Content-Type: application/json
Access-Control-Allow-Origin: http://bar.example

Here we have an Access-Control-Allow-Origin response header. That header indicates whether or not http://bar.example is allowed to use this resource. Because the value of Access-Control-Allow-Origin matches http://bar.example, subsequent DOM operations requiring data.json will succeed (as you can see in my CORS example). If the Access-Control-Allow-Origin value did not match, or the header was missing, then the contents of data.json would not be made available to the DOM. We’ll discuss the Access-Control-Allow-Origin header in greater detail below.

How browsers make complex cross-origin requests

For simple request methods (GET, HEAD and POST), and simple request headers (Accept, Accept-Language, Content-Language, Last-Event-ID, or Content-Type) the exchange between the Origin header and the Access-Control-Allow-Origin header is enough.

Complex request methods and request headers (including custom headers) work a bit differently. They require that the cross-origin request be pre-approved using a preflight request.

A preflight request asks the target server whether it is okay to make a full request using a particular method or header. In a typical cross-origin request, the user agent says to the server, Hi there! It’s http://foo.example. Please send me this resource. In a preflight request, the user agent will start off by saying, Hey, hey! It’s http://foo.example. I am going to ask for this resource using the PUT method. I also plan to include an If-Modified-Since header. Will you tell me whether you can handle this method and header before I send the actual request?

During a preflight operation, the user agent first sends a request using the OPTIONS method. In addition to the Origin header, the preflight request will include the Access-Control-Request-Method and/or an Access-Control-Request-Headers header.

Access-Control-Request-Method is included when the HTTP method used is one that may have a side effect — using PUT or DELETE, for example. Browsers also send Access-Control-Request-Headers when the header is a complex header, such as If-Modified-Since, or a custom header such as Opera Mini's X-Forwarded-For.

Let's look at an example using the PUT method. This request will be made from servera.example to serverb.example using XMLHttpRequest.

var xhr = new XMLHttpRequest() ;
xhr.open('PUT', 'http://serverb.example/formhandler/');
xhr.send('data=some+data');

Now let's look at the preflight request headers.

OPTIONS /formhandler/ HTTP/1.1
User-Agent: Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.10.238 Version/12.00
Host: serverb.example
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en, en-US
Accept-Encoding: gzip, deflate
Referer: http://servera.example/make_cross_origin_request
Connection: Keep-Alive
Content-Length: 0
Origin: http://servera.example
Access-Control-Request-Method: PUT

The URI returns a standard set of response headers. But it also includes the Access-Control-Allow-Origin and Access-Control-Allow-Methods headers.

HTTP/1.1 200 OK
Date: Tue, 06 Dec 2011 23:28:16 GMT
Server: Apache/2.2.21
Access-Control-Allow-Origin: http://servera.example
Access-Control-Allow-Methods: PUT
Cache-Control: max-age=0
Expires: Tue, 06 Dec 2011 23:28:16 GMT
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 134
Content-Type: text/html; charset=UTF-8

Here the values of Access-Control-Allow-Origin and Access-Control-Allow-Methods match the values of Origin and Access-Control-Request-Method, respectively. As a result, this preflight request will be followed by an actual request that includes the request body (in this case, data=some+data).

We will cover Access-Control-Allow-Methods and a similar header, Access-Control-Allow-Headers, in the Sending CORS response headers section. For now, it's enough to understand that if either of these headers were missing or contained values that did not match, the browser would cancel the actual request.

Sending CORS response headers

Scripts can initiate cross-origin requests, but the target URI must permit fetching by sending the appropriate response headers. Let’s look at each possible response header.

Access-Control-Allow-Origin

As its name suggests, the Access-Control-Allow-Origin header is a response to the Origin request header. It tells the user agent whether the requesting origin has permission to fetch the resource.

Access-Control-Allow-Origin can be set to one of three values:

  • null, which denies all origins;
  • *, the wildcard operator, which allows all origins; or
  • An origin list of one or more space-separated origins.

The following examples are all valid headers.

  • Access-Control-Allow-Origin: null
  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Origin: http://foo.example
  • Access-Control-Allow-Origin: http://foo.example http://bar.example

In practice, however, origin lists (Access-Control-Allow-Origin: http://foo.example http://bar.example) do not yet work in any browser. Instead, servers and applications must return an Access-Control-Allow-Origin header conditionally, based on the value of the Origin request header. An example of how to do this follows, in the Conditional CORS implementation section.

Also keep in mind that, though it is possible to use a wildcard value, it isn't necessarily a good idea. Doing so will allow scripts from any origin access to your document tree. It is safest to limit access to origins you know, and authenticate requests for sensitive data.

Access-Control-Allow-Methods

If a preflight request contains an Access-Control-Request-Method header, the target URI must return an Access-Control-Allow-Methods header for the request to be completed successfully. The header's value must be one or more HTTP methods such as PUT, DELETE, TRACE or CONNECT (again, GET, POST, and HEAD are considered simple methods, and will not cause this header to be included).

It's perfectly valid to allow multiple methods. However, you must separate them with a comma: Access-Control-Allow-Methods: PUT, DELETE.

Access-Control-Allow-Headers

Access-Control-Allow-Headers has a similar function to Access-Control-Allow-Methods, but instead tells the browser whether a particular header is allowed.

Standard or custom headers are appropriate values for Access-Control-Allow-Headers. For the cross-origin request to succeed, its value must match (or include) the value of the Access-Control-Request-Headers header. Let’s look at an example.

OPTIONS /data.json HTTP/1.1
User-Agent: Opera/9.80 Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.10.238 Version/12.00
Host: domain.example
Accept: text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1
Accept-Language: en, en-US
Accept-Encoding: gzip, deflate
Referer: http://requestingserver.example/path/to/document_making_the_request/
Connection: Keep-Alive
Origin: http://requestingserver.example
Access-Control-Request-Headers: X-Secret-Request-Header

The response headers might look like this:

HTTP/1.1 200 OK
Date: Tue, 04 Oct 2011 00:18:35 GMT
Server: Apache/2.2.20
Cache-Control: max-age=0
Expires: Tue, 04 Oct 2011 00:18:35 GMT
Vary: Accept-Encoding
Content-Type: text/html; charset=UTF-8
Access-Control-Allow-Origin: http://requestingserver.example
Access-Control-Allow-Headers: X-Secret-Request-Header, X-Forwarded-For

In this case, the request succeeds. If the value of Access-Control-Allow-Headers was X-Not-A-Secret instead, or missing entirely, this would have failed. As with Access-Control-Allow-Methods, multiple header values must be separated by a comma.

Access-Control-Allow-Credentials

Cross-origin requests do not include cookies or HTTP authentication information by default; they can, however, if the credentials flag is set to true. In the case of XMLHttpRequest, the credentials flag can be set using the withCredentials property. Below is an example of such a request. If a user cookie is available, it will be sent to the server.

xhr = new XMLHttpRequest();
xhr.open('GET','/page_requiring_authentication/');
xhr.withCredentials = true;
xhr.send( null );

Setting Access-Control-Allow-Credentials tells the user agent whether the response should be exposed when the credentials flag is true. If sent in response to a preflight request, it indicates that the actual request can include user credentials. In these cases, the Access-Control-Allow-Origin header must match the origin in order for the request to succeed; a wild card value will not work. Again, if the header is missing entirely, the request will fail (view my Access-Control-Allow-Credentials demo).

Access-Control-Expose-Headers

Browsers, by default, limit which cross-origin response headers are available to the DOM. Using the XMLHttpRequest.getResponseHeader() to read the Content-Length header will result in a null value. You may however want your application to know how many bytes of content to expect. Access-Control-Expose-Headers is designed to let developers white-list headers that can safely be exposed to the requesting origin.

Unfortunately, Access-Control-Expose-Headers does not yet work as you might expect in some browsers. To date:

  • Opera and Firefox will permit both standard HTTP headers and custom headers to be exposed.
  • Chrome and Safari will not expose headers it deems unsafe, including custom headers.
  • Internet Explorer will expose custom headers, but not standard ones that it deems unsafe.

Content-Length, for example, can be exposed in Firefox and Opera, but not Internet Explorer, Chrome, or Safari. A custom header such as X-Secret-Request-Header can be exposed in Opera, Internet Explorer, and Firefox, but not Chrome or Safari. To see this for yourself, compare how my Access-Control-Expose-Headers demo works in different browsers.

Access-Control-Max-Age

When a user agent makes a preflight request, the result is stored in the preflight result cache. The default expiration varies from browser to browser, but cross-origin requests made after the result cache expires will be preceded by another preflight request.

Access-Control-Max-Age explicitly informs the user agent how many seconds it should store the preflight result (try viewing my Access-Control-Max-Age demo). Access-Control-Max-Age: 15, for instance, tells the browser If you make another request in the next fifteen seconds, you can skip the preflight process. Just send the request. Setting Access-Control-Max-Age to zero (Access-Control-Max-Age: 0) disables the preflight result cache.

How to Set Response Headers

The easiest way to enable cross-origin resource sharing is to set response headers per file type or directory using a server configuration file. The example that follows is specific to Apache, and requires mod_headers. To permit requests for all JSON files from http://foo.example, your .htaccess file should contain the following.

<IfModule mod_headers.c>
  <FilesMatch "\.json$">
      Header set Access-Control-Allow-Origin "http://foo.example"
  </FilesMatch>
<IfModule>

If you use another web server, consult its documentation for instructions.

Setting CORS headers in the server configuration is adequate in some situations, although in most cases you’ll want to set access control response headers per URI. This should be done at the application level using a server-side language of your choice.

Conditional CORS

As discussed above, no major browser yet supports multiple origins as a value for the Access-Control-Allow-Origin header. So what do you do if you want to share data across several origins? The solution is to set the value conditionally.

The simple example that follows uses PHP to send an Access-Control-Allow-Origin response header only if the supplied origin is in our white list ($allowed):

<?php
# First check whether the Origin header exists
if( in_array('HTTP_ORIGIN', $_SERVER) ) {
  # Define a list of permitted origins
  $allowed = array('http://foo.example','http://bar.example','http://dom.example');

  # Check whether our origin is permitted.
  if(in_array($_SERVER['HTTP_ORIGIN'], $allowed) ){
    $filtered_url = filter_input(INPUT_SERVER, 'HTTP_ORIGIN', FILTER_SANITIZE_URL);
    $send_header  = 'Access-Control-Allow-Origin: '.$filtered_url;
    header($send_header);
    // Send your content here.
  }
} else {
  exit;
}
?>

A more robust version of the above example might keep a list of allowed origins for each URI in a datastore. Again, this is not an effective way to protect sensitive data. But it is a bullwark against XSS attacks.

Learn More

For a greater understanding of cross-domain scripting and cross-origin resource sharing, visit the resources below.