Relaxing the same-origin policy

Cross Origin

Preliminaries

This note describes how to configure the server to relax the strict same-origin policy imposed by browsers regarding the use of resources from other domains, also known as the CORS protocol.

One of the most important ways that browsers protect users from bad actors is by limiting what each session can access through the same-origin policy. This policy prevents JavaScript access to resources that come from a domain that is different from the current document's domain.

Sometimes large websites will purposely split the resources it needs into two or more separate domains, in order to improve performance, or provide redundancy, or implement special security requirements. When this is the case, the browser's same-origin policy can be relaxed to allow a script hosted on one domain to access resources hosted on another domain. This is done through cross-origin requests.

Simple CORS requests

Cross-origin requests are only successful when the browser receives positive confirmation from the second domain's server that such a request is allowed. Such confirmation is initiated when a browser determines that a request is potentially unsafe, and sends the second server a request with an origin header, informing it that the request is coming from another domain. If allowed, the second server sends the confirmation in the form of an access-control-allow-origin response header, and the browser makes the response available to the initiator's JavaScript.

This simple exchange of request and response headers may occur with GET, HEAD and POST methods, but only when the request is limited to using these safelisted headers:

  • accept
  • accept-language
  • content-language
  • content-type: application/x-www-form-urlencoded
  • content-type: multipart/form-data
  • content-type: text/plain

Preflighted requests

When an attempt to issue a cross-origin request is detected by the browser that does not meet the requirements outlined for a simple CORS request, as outlined above, the browser initiates a preflight CORS request. Preflight requests are necessary in order to prevent the real request from even being attempted if the other domain's server is not going to allow it.

In these cases the protocol proceeds with these steps:

  1. The browser begins by sending an OPTIONS preflight request with an origin request header and one or more access-control-request-* headers;
  2. The server responds with access-control-allow-origin and one or more access-control-allow-* headers, informing the browser that it is going to allow the actual request;
  3. The browser proceeds by sending the actual request method, with the same access-control-request-* headers;
  4. The server carries out the actual request and answers with the actual response.

Allowing methods

The browser normally only allows GET, HEAD and POST methods to be used in CORS requests. If another method is attempted, the browser sends a access-control-request-method in the preflight request and the server responds with access-control-allow-methods to tell the browser which methods are allowed for the specified origin.

Allowing credentials

The browser will not allow CORS responses to access sensitive data unless the server explicitly tells it otherwise. The server does this by sending an access-control-allow-credentials response header, in which case the browser is instructed to allow scripts to access cookies and other sensitive data.

Allowing request headers

The browser normally only allows the safelisted headers to be used in CORS requests. If other headers are necessary for the request, the browser sends a access-control-request-headers in the preflight request and the server responds with access-control-allow-headers to tell the browser which non-safelisted headers are allowed for the specified origin.

Exposing response headers

The browser normally filters out all CORS response headers that are not in the safelisted headers list. If the server wishes to allow certain other headers to be visible to the browser's script, the server should respond to the preflight request, and the actual request, with an access-control-expose-headers informing the browser of which other response headers are to be made visible for script requests from the specified origin.

Caching preflight responses

Some applications will issue numerous CORS requests. In order to reduce the number of preflight requests that need to be made, a browser may keep the previous preflight confirmation in its cache. A server can tell the browser how long it can keep any such cache using the access-control-max-age response header. Subsequent requests to the same domain for the same resource, same method, and same headers can skip the preflight request when it occurs within the window of time specified in that max-age.

Notes

Things to note regarding the cross-origin resource sharing protocol:

  1. Not every user-agent is programmed to use CORS. All modern browsers are, but utilities like curl and wget are not. Remember this when testing new configurations.
  2. The decision to use simple CORS versus preflight CORS is made by the browser. The server can handle both, using the same configuration rules.
  3. CORS does not strengthen security, it weakens security.
  4. Responding with access-control-allow-origin: * punches a hole through the same-origin policy, and should be used only with that awareness.

Configuration

To begin accepting CORS requests, the cross-origin module must be enabled in the module section and the OPTIONS method must be added to the methods entry.

All other configuration settings are specified using the cross-origin section, which is subordinate to the request section. It comprises a collection of entries, where each entry has a path-pattern, an origin hostname, and one or more optional attribute values which specify additional rules to apply to the path pattern.

Refer to the separate note regarding Path Pattern rules.

The origin attribute is a single DNS hostname, or a hostname beginning with the '*' wildcard symbol, or a comma separated list of hostnames, or the special value '*' by itself.

Each entry may have any combination of these five attributes:

  • The credentials attribute may be 'true' or 'false', indicating whether or not the browser should allow access to cookies and other sensitive data.
  • The max-age attribute is a positive integer value specifying the number of seconds that the browser is allowed to cache the OPTIONS response.
  • The methods attribute is a comma-separated list of HTTP methods that the browser is allowed to use in a cross-domain resource request.
  • The headers attribute is a comma-separated list of HTTP request headers that the browser is allowed to send in a cross-domain resource request.
  • The expose attribute is a comma-separated list of HTTP response headers that the browser should allow the cross-domain script to access.

Information Headers

The response code to a simple CORS request that is not acceptable is 403 with an information header of rw-origin-not-allowed.

The response code to an OPTIONS preflight request is always 200, whether it is accepted or denied by the server. Requests that are denied omit the access-control-allow-origin header (per CORS specification), and add an rw-origin-not-allowed information header.

The response code to an actual CORS request that is not acceptable is 403 with one of these information headers:

  • rw-origin-not-allowed when the requested resource does not match any configured path pattern, or when it matches a configured path pattern but the origin request header does not match any hostname listed in the origin attribute's comma-separated list.
  • rw-method-not-allowed when the requested resource matches a configured path pattern but the actual HTTP request method does not match any method listed in the methods attribute's comma-separated list.
  • rw-header-not-allowed when the requested resource matches a configured path pattern but the actual HTTP request contains a non-safelisted request header that is not whitelisted in the headers attribute's comma-separated list.

EBNF

SP ::= U+20
CR ::= U+0D
QUOTATION-MARK ::= U+22
ASTERISK ::= U+2A
COMMA ::= U+2C
HYPHEN ::= U+2D
FULL-STOP ::= U+2E
QUESTION-MARK ::= U+3F
SOLIDUS ::= U+2F
EQUALS-SIGN ::= U+3D
GRAVE-ACCENT ::= U+60
LEFT-CURLY-BRACKET ::= U+7B
RIGHT-CURLY-BRACKET ::= U+7D
file-system-chars ::= (ALPHA | DIGIT | )*
wildcards ::= ASTERISK | QUESTION-MARK
path-pattern ::= (SOLIDUS | file-system-chars | wildcards)*
delimited-path-pattern ::= GRAVE-ACCENT path-pattern GRAVE-ACCENT
origin-hostname ::= (ALPHA | DIGIT | FULL-STOP | HYPHEN)*
wildcard-hostname ::= ASTERISK origin-hostname
comma-separated-hostnames ::= ((origin-hostname | wildcard-hostname) COMMA)*
any-hostname ::= ASTERISK
delimited-hostnames ::= QUOTATION-MARK (comma-separated-hostnames | any-hostname) QUOTATION-MARK
hostname-attribute ::= ASTERISK 'origin' EQUALS-SIGN delimited-hostnames
cors-credentials ::= ASTERISK 'credentials' EQUALS-SIGN ('true' | 'false')
cors-max-age ::= ASTERISK 'max-age' EQUALS-SIGN [1..86400]
comma-separated-methods ::= ('GET' | 'HEAD' | 'PUT' | 'POST' | 'DELETE') COMMA)*
cors-methods ::= ASTERISK 'methods' EQUALS-SIGN comma-separated-methods
http-header ::= (LOWERCASE-ALPHA | DIGIT | HYPHEN)*
comma-separated-headers ::= ((http-header) COMMA)*
cors-headers ::= ASTERISK 'headers' EQUALS-SIGN comma-separated-headers
cors-expose ::= ASTERISK 'expose' EQUALS-SIGN comma-separated-headers
cors-entry ::= delimited-path-pattern hostname-attribute [cors-credentials | cors-max-age | cors-methods | cors-headers | cors-expose]* CR
cors-section ::= 'cross-origin' SP LEFT-CURLY-BRACKET CR
cors-entry*
RIGHT-CURLY-BRACKET CR

† Legal file system characters vary by platform.

Cookbook

Example 1: Enabling the cross-origin module for CORS
host {
methods GET,HEAD,OPTIONS

modules {
cross-origin on
}
}
Example 2: Allowing a single host to access anything
host {
request {
cross-origin {
`*` *origin="https://trusted.example.com"
}
}
}
Example 3: Allowing subdomains of single host
host {
request {
cross-origin {
`*` *origin="https://*.example.com"
}
}
}
Example 4: Allowing multiple hosts to access a single directory
host {
request {
cross-origin {
`/special-trust-area/*` *origin="https://a.example.com,https://b.example.com,https://c.example.com"
}
}
}
Example 5: Wide-open cross-domain access
host {
request {
cross-origin {
`/abusive.js` *origin="*"
// Instruct browser to allow any hostname to access this script
}
}
}
Example 6: Allowing scripts to access credentials
host {
request {
cross-origin {
`/special-trust-area/*` *origin="https://example.com" *credentials=true
}
}
}
Example 7: Allowing non-standard methods
host {
request {
cross-origin {
`/special-trust-area/*` *origin="https://example.com" *methods=GET,HEAD,POST,PUT,DELETE
}
}
}
Example 8: Allowing non safelisted request headers
host {
request {
cross-origin {
`/special-trust-area/*` *origin="https://example.com" *headers=x-jwt-key,x-special-value
}
}
}
Example 9: Exposing additional response headers to scripts
host {
request {
cross-origin {
`/special-trust-area/*` *origin="https://example.com" *expose=x-security-token,x-access-token
}
}
}

Review

Key points to remember:

  • CORS is a weakening of the strict same-origin policy used to safeguard users.
  • CORS is needed when applications split their resources into multiple domains.
  • Many CORS requests can be configured with simply a path-pattern and an origin.
  • The optional CORS configuration attributes are only needed for special cases.

Relaxing the same-origin policy