DOM Access Control Using Cross-Origin Resource Sharing
This piece was originally published on Dev.Opera. Some links and images may be broken. Some formatting may be off. You may republish this post under a CC-BY 3.0 license.
- Introduction
- What is CORS?
- How browsers make simple cross-origin requests
- How browsers make complex cross-origin requests
- Sending CORS response headers
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
- Access-Control-Allow-Credentials
- Access-Control-Expose-Headers
- Access-Control-Max-Age
- How to set response headers
- Conditional CORS
- Learn more 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.{:.note}
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.
Request headers | Response 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. | |
`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
In a preflight request, the user agent will start off by saying, http://foo.example
. Please send me this resource.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.
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.