In this chapter, you will learn about the following:
The same-origin policy that limits sharing resources across domains
Granting access to CORS requests by setting headers
How to do something with the
responseText
request from a CORS requestRudimentary security in CORS and ways to add more security
Preflight requests to prepare for some types of CORS methods and events
Enabling the crossorigin attribute in a script tag for better troubleshooting
Alternatives to CORS: JSON-P, WebSockets, and window.postMessage
Sooner or later, web developers run up against the same-origin policy. Maybe you want to trigger a script on one domain and use the results on a different domain, but you can't.
The same-origin policy is necessary for web application security. The execution of a script may expose sensitive information. Access to this information is limited to the same domain where the script is located, unless access for an external domain has been specifically allowed by code.
Note
The same-origin policy is defined by the Internet Engineering Task Force (IETF) (https://tools.ietf.org/html/rfc6454#page-4).
A major motivation for implementing the same-origin policy is to protect sensitive information stored in cookies from being exposed to another domain. Web applications maintain authenticated user sessions in cookies. The user's personalizations and account information are stored in cookies. To ensure data confidentiality, cookies may not be shared across domains. For cookies, the same origin is shared by the domain or a sub-domain of that domain. For DOM elements such as scripts, the restrictions are more fine-grained.
The same-origin policy also applies to requests made with XMLHttpRequest (XHR). We will see how the Access-Control-Allow-Origin header facilitates the bending of the same-origin policy.
Notably, JSON-P, WebSocket, and window.postMessage are not restricted by the same-origin policy.
Access to DOM elements is allowed only when the request scheme, hostname, and port number match those of the current URI. A subdomain cannot share DOM elements with the parent domain.
Scheme in web applications is typically
http://
orhttps://
Hostname is typically the domain name plus TLD, or the unique IP address
Port number:
Typically, port
80
is implicit inhttp://
443
for SSL overhttps://
If the Scheme, Hostname, and port number do not match the DOM element, then resource sharing is prohibited as they do not share the same origin. Considering the domain http://www.example.com
, the following table provides various combinations of matching and mismatching origins:
URI | Match? | Reason |
---|---|---|
| Match | Same protocol and host |
| Match | Same protocol and host |
| Mismatch | Different host (www is a subdomain) |
| Mismatch | Different protocol(https://) |
| Mismatch | Same protocol and host but different port (81) |
| Mismatch | Different host (en is a subdomain) |
Internet Explorer (IE) implements two major differences when it comes to the same-origin policy:
IE Trust Zones allow different domains: If both domains are in a highly trusted zone, then the same-origin policy limitations are not applied.
Port is ignored: IE ignores the port in same origin components. These URIs are considered from the same origin:
The same-origin policy is not required for many resources that may be embedded in cross-origin. The sharing of specific file types is limited by file type headers and ensuring that the file extensions and file meta data match the expected type.
The following information box displays scenarios where DOM elements are allowed for cross-origin sharing:
Note
Images with the <img>
tag, as long as the file type matches expected image formats.
Media files with the <video>
and <audio>
tags as long as the file type matches expected media formats.
JavaScript with the <script src="..."></script>
tag. This method is used by many third-party applications, which embed a script to act upon the local resources, for example, a social media sharing service that analyzes the shareable images and other assets on current page and reads the URI.
CSS with the <link rel="stylesheet" href="...">
tag. Cross-origin CSS requires a correct content-type header. Client.
Plugins with the <applet>
, <object>
and <embed>
tags.
Fonts with @font-face
. Support for this method varies by client browser.
Any content or URI loaded with the <frame>
and <iframe>
tags.
The ability of WebSockets to bypass the same-origin policy is seen as a security risk. Using WebSockets on a gateway/server that supports origin-based security provides header-based security similar to CORS.
JavaScript APIs, such as iframe.contentWindow, window.parent, window.open, and window.opener, provide limited cross-origin access to the Window and Location objects. Some browsers permit access to more properties than the specification allows. You can use window.postMessage instead to communicate between documents in separate windows.
Let's consider content scraping. You can write a content scraping script that reads the rendered DOM of an external URI and creates local DOM elements with the same content, without any special configurations.
But what if you first need to run a script on the external URI, for example, to find out whether the user is the same as on your local site? You cannot trigger that external script and return the results without cross-origin sharing via CORS or a similar method to get around the same-origin policy.
JavaScript data stored in the browser as Local Storage, or in IndexedDB, is separated by origin. Each origin has distinct storage, and JavaScript in one origin cannot read from or written to storage belonging to another origin unless it is given explicit access to a script on another domain by CORS or a similar method.
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:
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.
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.
These are the parameters passed to
createCORSRequest(method, url)
:The method would typically be
GET
,POST
, or another methodThe 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.
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 requestWhen the XHR ready state is loaded--request.onload--do something with
request.responseText
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.
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.
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.
Preflight is a request the XHR
object makes to ensure it's allowed to make another request.
Note
The CORS specification requires browsers to preflight requests that do the following:
Use any methods in the request other than
GET
,POST
, orHEAD
.Include custom headers
Include content-type other than text/plain, application/x-www-form-urlencoded, or multipart/form-data
There's no preflight by default in CORS. Adding preflight makes your application more robust and handles errors better. However, it can also introduce complexities, which may be unnecessary when you are confident that the XHR
request you need to make will be answered, and you only need to use GET
, POST
, or HEAD
.
To trigger a preflight, set custom headers on the XHR
request; the Access-Control-Allow-Methods
header determines which HTTP methods can be used.
The following PHP code verifies for the OPTIONS
request method during preflight. The server responds with the X-Requested-With
header permitted:
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') { // return only headers // The Preflight checks that the GET request method is supported if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) && $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] == 'GET') { header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Headers: X-Requested-With'); } exit; }else{ // error-handling code if the OPTIONS request method is unavailable }
CORS via jQuery does not use preflight.
jQuery specifically avoids setting the custom header when making a CORS request. Therefore, it is better to use a separate preflight method when using jQuery for CORS.
Note
Here is the comment in the jQuery xhr.js library explaining why preflight is not used:
// X-Requested-With header
// For cross-domain requests, seeing as conditions for a preflight are
// akin to a jigsaw puzzle, we simply never set it to be sure.
// (it can always be set on a per-request basis or even using AJAXSetup)
// For same-domain requests, won't change header if already provided.
There are some common issues that developers face while implementing CORS preflight.
The CORS preflight request fails in Firefox when the OPTIONS
request needs to be authenticated, causing the cross-origin request to fail. The request fails because authentication tokens are not sent with the preflight request. If the OPTIONS
request fails, the preflight will result in 405 (method not allowed). Firefox ignores the request when the preflight fails.
Any CORS request that uses a non-simple
method or header requires preflight.
GET
, POST
, and HEAD
are considered simple requests (and are case-sensitive). They do not require preflight.
The simple headers that do not require preflight are as follows:
Cache-control
Content-language
Content-type
Expires
Last-modified
Pragma
Any other method or header requires preflight.
Using the XMLHttpRequest
level 2 event HandlersOriginally
, XMLHttpRequest
had only one event handler: onreadystatechange
. XMLHttpRequest2
introduces new event handlers.
You may have noticed that when defining the XHR
objects, we have used request.onload
, which corresponds to the onload
event when the request has successfully completed since we are interested in knowing whether the request has been successful.
Event handler |
Description |
---|---|
|
|
|
request starts |
|
during loading and sending data. |
|
request has been aborted |
|
request has failed |
|
request has successfully completed |
|
specified timeout has expired before the request could complete |
|
request has completed (success or failure) |
* IE's XdomainRequest does not support handlers marked with asterisks
Detecting problems with CORS requires enabling the crossorigin attribute in the <script>
tag.
Normal script tags will pass the least information to window.onerror for scripts that do not pass the standard CORS checks. To allow error logging for sites that use a separate domain for static media, several browsers have enabled the crossorigin
attribute for scripts using the same definition as the standard crossorigin attribute for the <img>
tag.
jQuery's $.ajax()
method can be used for standard XHR and CORS requests.
Note
Things to know about CORS with jQuery2
JQuery's CORS implementation doesn't support IE's XDomainRequest
object, which is needed prior to Internet Explorer 10. There are jQuery plugins and workarounds. $.support.cors
can signal support for CORS. It is set to true if the browser supports CORS (but in IE it always returns false). This can be a quick way to check for CORS support.
In jQuery, define the XHR
functions using the same techniques as for CORS with JavaScript:
$.ajax({ // The 'type' property sets the HTTP method // Any value other than GET, POST, HEAD (eg. PUT or DELETE methods) will initiate a preflight request type: 'GET', // The Target Domain URL to make the request to url: 'http://targetdomain.com', // The 'contentType' property sets the 'Content-Type' header // The JQuery default for this property is // 'application/x-www-form-urlencoded; charset=UTF-8' // If you set this value to anything other than // application/x-www-form-urlencoded, multipart/form-data, or text/plain, // you will trigger a preflight request contentType: 'text/plain', xhrFields: { // The 'xhrFields' property sets additional fields on the XMLHttpRequest // This can be used to set the 'withCredentials' property // Set the value to 'true' to pass cookies to the server // If this is enabled, your server must respond with the header // 'Access-Control-Allow-Credentials: true' // Remember that IE <= 9 does not support the 'withCredentials' property withCredentials: false }, headers: { // Set custom headers // If you set any non-simple headers, your server response must include // the headers in the 'Access-Control-Allow-Headers' response header }, success: function() { // Handler for a successful response, do something with the response.Text }, error: function() { // Error handler // Note that if the error was due to an issue with CORS, // this function will still be triggered, but there won't be any additional information about the error. } });
A jQuery plugin for CORS is available at http://plugins.jquery.com/cors.
The plugin sends cross-domain AJAX requests through corsproxy.io
.
Chapter 2, Creating Proxies for CORS, gives details about using proxies with CORS.
There are other ways to work around the same-origin policy. CORS provides better basic security, error handling, preflight, and other methods that make it a superior choice for cross-origin sharing compared to these alternatives
Alternative methods include the following:
JSON-P
WebSocket
window.postMessage
JSON-PJSONP (later dubbed JSON-P, or JSON-with-padding) was proposed in 2005 as a way to use the <script>
tag to request data in the JSON format across domains.
The term "padding" refers to a callback
function, which is defined as a query parameter attached to the <script>
tag. The callback
function is defined on the target domain. The <script>
tag on the local domain loads a function or service on the target domain. When the script executes, the function on the target domain is called, and the data returned from the target domain is passed to the callback
function on the local domain.
There is no official definition or specification for JSON-P.
A callback function is defined on the local domain:
function handle_data(data) { // something is done to the data received from the Target Domain }
A
<script>
tag on the local domain loads the script (http://targetdomain/web/service) from the target domain and passes the results to thecallback
functionhandle_data
on the local domain:<script type="application/javascript" src="http://targetdomain/web/service?callback=handle_data" </script>
JSON-P does not use AJAX XHR; therefore, error detection and handling are not possible until the data is passed to the
callback
function.Trust in the target domain is implicit. If the target domain is compromised, the local domain becomes vulnerable as well. JSON-P is subject to cross-site request forgery (CSRF or XSRF) attacks because the
<script>
tag is not restricted by the same-origin policy. A script tag on a malicious page can request and obtain JSON data of another domain. If the user is authenticated at the endpoint domain, passwords or other sensitive data may get compromised.Rosetta flash uses adobe flash player to exploit servers with a vulnerable JSON-P endpoint. It causes the Adobe Flash Player to accept a flash applet as originating from the same domain.
The standard would make JSON-P safer.
Limit the function ("padding") reference of the JSON-P response to a single expression as a function reference or an object property function reference. A single pair of enclosing parentheses should follow the expression, with a valid and parsable JSON object inside the parentheses.
Examples of safer JSON-P functions are as follows:
functionName({JSON});
obj.functionName({JSON});
obj["function-name"]({JSON});
Only whitespace or JavaScript comments may appear in the JSON-P response since whitespace and comments are ignored by the JavaScript parser. The MIME-type
application/json-p
and/ortext/json-p
must be included in the requesting<script>
element. The browser can require that the response must match the MIME-type.
WebSocket provides full-duplex communication channels over a single TCP connection. The WebSocket protocol was standardized by the IETF (https://tools.ietf.org/html/rfc6455) in 2011, and the WebSocket API is a candidate recommendation by the W3C (http://www.w3.org/TR/websockets/).
WebSocket uses TCP, not HTTP; nor does it use AJAX/XHR.
Socket.io provides a framework to use WebSocket by creating a node.js server for the socket (http://socket.io/).
The initial handshake over HTTP sets up the connection and communicates the origin policy information.
If the handshake is successful, the data transfer continues via TCP.
WebSocket creates a two-way communication channel, where each side can, independently from the other, send data at will.
The header can be spoofed. In order to secure the connection, you will need to authenticate the connection by other means.
The same-origin policy is not observed in WebSocket; therefore, Cross-Site WebSocket Hijacking (CSWSH) is possible.
WebSocket does not handle standard safeguards that need to be implemented outside of the WebSocket:
Authentication
Authorization
Sanitization of data
The postMessage
method is part of the W3c candidate recommendation for HTML5 Web Messaging ().
postMessage
allows messages between discrete documents. The documents may include an iframe embedded in a document, or any other window objects.
The postMessage
method dispatches MessageEvent
in the target window when a script is completed.
Any window can send a message to any other window. An unknown sender can send malicious messages. The sender's identity can be verified using the origin and source properties. If a site you trusted gets compromised, it can send cross-site scripting messages. The syntax of the received message can be verified against an expected pattern.
A malicious script can spoof the
location
property of the window and intercept data. Similar to avoiding the wildcard in theAccess-Control-Allow-Origin
header in CORS, specify an exact target.
We looked at the same-origin policy, which limits cross-origin resource sharing. We covered a lot of the basics needed to work around the same-origin policy with CORS, including the header and request.
We saw how a script tag on a local domain can retrieve resources from a target domain as responseText
request and how we can then do things with the responseText
request on the local domain.
We have learned when preflight is helpful, and when it is required.
We have learned how to enable the crossorigin attribute in the script tag for troubleshooting.
We have looked at CORS with jQuery and its limitations.
We have compared CORS with other cross-origin methods: JSON-P, WebSockets, and window.postMessage. We have learned why CORS can be better and more secure than these methods.
In the next chapter, we will learn how to use proxies for CORS, for example, using the CORS plugin for jQuery with corsproxy.io.