How CORS works – the header and the request
Note
CORS is a specification of World Wide Web Consortium (W3C) http://www.w3.org/TR/cors/.
Cross Origin Resource Sharing (CORS) is allowed by having a header on the target domain where your local domain needs access. Local domains whitelisted in the allow-origin header can now send an XMLHttpRequest (XHR) request or other types of request to the target domain and receive a response.
The CORS header
The CORS header whitelists access to one domain or any domain with the wildcard *
. This header allows access from only one domain—the one specified:
Access-Control-Allow-Origin: http://localdomain.com
To enable access from multiple domains, a wild card is used:
Access-Control-Allow-Origin: *
Tip
Not being able to whitelist a list of allowed domains is a major complaint about CORS. Although a list of allowed domains is part of the W3C specification, in practical terms, it is not supported by browsers. (http://www.w3.org/TR/cors/#list-of-origins)
You can specify a single allowed domain or allow from all. The wildcard opens the target site to possible security risks because any domain is allowed to send a cross-origin request to the target domain.
Some authentication/authorization must be added outside of the CORS code to provide security, particularly when using the wildcard.
Example 1 – CORS request with JavaScript
The following example explains the CORS syntax, without actually doing anything with the responseText
request:
- Create the
XMLHttpRequest
object:// Create the XHR object. function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { // XHR for Chrome/Firefox/Opera/Safari and IE >= 10 xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // XDomainRequest for IE <= 9 xhr = new XDomainRequest(); xhr.open(method, url); } else { // CORS not supported xhr = null; } return xhr; }
- The
createCORSRequest
function does the following:- Defines the new
XMLHttpRequest
request as the variable"xhr".
- Checks whether the browser supports CORS via
XHR
by detecting thewithCredentials
orXDomainRequest
properties. - Opens the request for a resource on the target domain.
- Defines the new
- These are the parameters passed to
createCORSRequest(method, url)
:- The method would typically be
GET
,POST
, or another method - The URL is the URI of the resource requested by the local domain
- The method would typically be
IE 10 supports the working draft XMLHttpRequest
level 2. Therefore, the withCredentials
property can be used to detect CORS support in most browsers, including IE >= 10. To provide backwards compatibility for IE < 10, use its XDomainRequest
property.
var request = createCORSRequest("get", "targetdomain.com/"); if (request){ request.onload = function(){ //do something with request.responseText }; request.send(); }
- If
createCORSRequest()
returns anXHR
object, it sends the request - When the XHR ready state is loaded--request.onload--do something with
request.responseText
Example 2: the CORS transaction to retrieve the title tag
This CORS request retrieves the page title from the target domain, targetdomain.com
. It parses the responseText
request to get the title and sends the target domain and the retrieved page title text to the console.log
:
// Create the XHR object. function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { // XHR for Chrome/Firefox/Opera/Safari and IE >= 10 xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // XDomainRequest for IE <= 9 xhr = new XDomainRequest(); xhr.open(method, url); } else { // CORS not supported xhr = null; } return xhr; } // utility function to parse the title tag from the response function getTitle(text) { return text.match('<title>(.*)?</title>')[1]; } // Make the actual CORS request function makeCorsRequest() { // we want the title of the page at targetdomain.com var url = 'targetdomain.com'; // use the GET method to return the entire page var xhr = createCORSRequest('GET', url); if (!xhr) { // log message if CORS is not supported console.log('CORS not supported'); return; } // Response handlers. // on readyState = load xhr.onload = function() { // xhr.responseText contains the HTML for the page at targetdomain.com var text = xhr.responseText; // send the responseText to the utility function to extract the page title var title = getTitle(text); // do something with the processed responseText, in this case log a message console.log('response from request to ' + url + ': ' + title); }; // error handler xhr.onerror = function() { console.log('error making the request'); }; // send the request xhr.send(); }
What happens in this CORS request?
- The
XMLHttpRequest
request is created with the detection of CORS and error handling. - The
responseText
request returns the contents of the page attargetdomain.com
withGET
. - The
getTitle
function is executed on theresponseText
request, and it returns the title text. - The target domain URL and the title text are sent to the
console.log
.
You're probably thinking, "Big deal! I can get the title text in other ways.". But you could do more than retrieving a DOM element.
Distributing DOM elements to multiple domains
Let's consider a scenario in which you want to distribute a block-level DOM element, for example, a navigation menu from a target domain to multiple pages on multiple domains, along with customized CSS and JavaScript for the menu. You only change the navigation menu once on the target domain and copy it to multiple pages on multiple domains with CORS.
We will examine the pieces and then put them all together.
A script
tag on the local domain embeds a script from the target domain. The same origin policy allows script
tags to request resources across domains. The CORS script on the target domain will contain the createCORSRequest
function and a request like this:
var request = createCORSRequest("GET", "targetdomain.com/header.php");
The CORS request allows you to GET
a PHP file from the target domain and use it on the local domain.
You are not limited to requesting the HTML for a page on the target domain in the responseText
request, as in example 2; header.php
on the target domain may contain HTML, CSS, JavaScript, and any other code that is allowed in a PHP file.
Note
We are only reading header.php
. Its contents are created by a process on the target domain outside of CORS. A script on the target domain scrapes the navigation menu and adds the necessary CSS and JavaScript. This script may be run as a Cron job to automatically update header.php
, and it can also be triggered manually by an administrator on the target domain.
If the request is successful, it returns the contents of header.php and replaces the contents of a DOM element #global-header
on the local domain with the responseText
request:
if (xhr){ xhr.onload = function(){ // do stuff if request is successful; document.getElementById('#global-header').innerHTML = xhr.responseText ; }; request.send(); }
Adding the Access-Control-Allow-Origin
header in header.php
on the target domain allows access from the local domain. Since header.php
is a PHP file, we add the header with the PHP code. Use the wildcard *
to allow access from any domain because we making the CORS request from multiple domains:
<?php header('Access-Control-Allow-Origin: *'); ?>
You can place the CORS request script on the target domain as cors_script.js
and trigger it with the script tag on your local domain. The responseText
request is sent to any local domain page that contains the script tag. The DOM selector #global-header is replaced on the local domain with the responseText
request contents of header.php
, which contains the navigation menu HTML, CSS, and JavaScript from the target domain. We are also going to replace the logo image on the local domain with the one from the target domain.
By placing a script tag on your local domain page, you can access a target domain from any local domain, run a script on it, and do something on your local domain:
<script src="http://targetdomain.com/cors_script.js"></script>
The contents of cors_script.js
in the target domain are as follows:
// Create the XHR object. function createCORSRequest(method, url) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { // XHR for Chrome/Firefox/Opera/Safari and IE >= 10 xhr.open(method, url, true); } else if (typeof XDomainRequest != "undefined") { // XDomainRequest for IE <= 9 xhr = new XDomainRequest(); xhr.open(method, url); } else { // CORS not supported xhr = null; } return xhr; } // set some variable values for use in the request processing // the Target Domain is contained in document.domain var rawdomain = document.domain; // add the http:// scheme var sourceURL = "//" + rawdomain; // use the Protocol-relative shorthand // // define XHR request var request = createCORSRequest("get", sourceURL + "/[path-to-file]/header.php"); // send the request and process it if it is successful if (request){ request.onload = function(){ // do stuff if CORS request is successful and loads // if it fails, there is no replacement of existing HTML in target site // replace contents of #global-header on Local Domain with the responseText document.getElementById('global-header').innerHTML = request.responseText; // use logo image from Target Domain inside #branding container on Local Domain document.getElementById('branding').getElementsByTagName('img')[0].src = sourceURL + '[path-to-logo]/targetdomain_logo.png'; }; request.send(); }
The navigation menu is automatically distributed to any number of pages on any number of domains via CORS whenever a page with the script tag loads.
What happens if someone copies your script tag to some other domain where the script was not intended to run? Since we whitelisted access from any domain with Access-Control-Allow-Origin: *
, the request will be allowed from any domain; if the page also has the matching DOM selector #global-header
, the script will copy the content from the target domain to the page making the request.
Although the W3C specification for CORS recommends providing a list of allowed origins, in practice, this is not widely implemented in browsers.
Tip
Ways to add security when a CORS header whitelists all domains
Techniques have been proposed to first match $_SERVER['HTTP_ORIGIN']
to an allowed list, then write the header that allows the matched origin. Since $_SERVER['HTTP_ORIGIN']
is not reliable, or the requesting domain may be served via a CDN that does not match the expected domain, this technique may not work.
An alternative method is to add allowed domains in .htaccess
or in the server conf, which may have the same trouble with CDN domains.
There are a few methods to secure when all the domains are whitelisted in the CORS header. The following code compares the HTTP_ORIGIN
with a list of allowed domains; if it matches, then the CORS header is written using the matched domain:
$http_origin = $_SERVER['HTTP_ORIGIN']; if ($http_origin == "http://domain1.com" || $http_origin == "http://domain2.com" || $http_origin == "http://domain3.info") { header("Access-Control-Allow-Origin: $http_origin"); }
This technique may not work because $_SERVER['HTTP_ORIGIN']
is not reliable, or the requesting domain may be served via a CDN that does not match the expected domain.
An alternative method is to add allowed domains in .htaccess
or in the server conf, which may have the same trouble with CDN domains.
Simple CORS request methods
Most CORS request methods use either GET
or POST
, and less often HEAD. Keep these differences in mind when you are selecting the method to use:
- Browsers cache the result from a
GET
request; if the sameGET
request is made again, then the cached result will be returned. Repeating aGET
request that has been cached will NOT return a response after the first request. If your code checks for a response, it will only be returned the first time. - The
POST
method is typically used when you are updating information on the server. Repeating aPOST
method more than once may not return the same result. APOST
will always obtain the response from the server. The content is sent separately from the headers inPOST
, which makes it more complicated than a simpleGET
request. - The
HEAD
method is used to check resources, so only the headers are returned without any content.HEAD
can check for the existence of a resource, its size, or to see whether it has been recently updated.