CORS Essentials
上QQ阅读APP看书,第一时间看更新

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.

Note

Local domain and target domain explained

In this book, we will refer to the domains in a CORS request as follows:

  • Local domain (localdomain.com): The domain making the CORS request
  • Target domain (targetdomain.com): The domain receiving the CORS request, hence the target of the request

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:
    1. Defines the new XMLHttpRequest request as the variable "xhr".
    2. Checks whether the browser supports CORS via XHR by detecting the withCredentials or XDomainRequest properties.
    3. Opens the request for a resource on the target domain.
  • 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

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.

Tip

Microsoft Internet Explorer 9 makes up 9.14% of desktop browsers, so we must include a fallback check for its the XDomainRequest property when withCredentials fails.

Passing a request to a utility function

var request = createCORSRequest("get", "targetdomain.com/");
if (request){
    request.onload = function(){
        //do something with request.responseText
    };
    request.send();
}

This request does two things:

  • If createCORSRequest() returns an XHR 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?

  1. The XMLHttpRequest request is created with the detection of CORS and error handling.
  2. The responseText request returns the contents of the page at targetdomain.com with GET.
  3. The getTitle function is executed on the responseText request, and it returns the title text.
  4. 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>

Putting it all together

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.

Securing when all domains are whitelisted

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.

Methods to add security when a CORS header whitelists all 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.

Note

CORS headers can not provide reliable security

The CORS headers give browser information about allowed domains, but some other security policies, such as cookies or OAuth, can enforce tighter security in your application.

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 same GET request is made again, then the cached result will be returned. Repeating a GET 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 a POST method more than once may not return the same result. A POST will always obtain the response from the server. The content is sent separately from the headers in POST, which makes it more complicated than a simple GET 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.

    Tip

    GET, POST, and HEAD function in CORS exactly as they do for an XMLHttpRequest request.