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:
- The browser begins by sending an
OPTIONS
preflight request with anorigin
request header and one or moreaccess-control-request-*
headers; - The server responds with
access-control-allow-origin
and one or moreaccess-control-allow-*
headers, informing the browser that it is going to allow the actual request; - The browser proceeds by sending the actual request method, with the same
access-control-request-*
headers; - 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:
- Not every user-agent is programmed to use CORS. All modern browsers are, but utilities like
curl
andwget
are not. Remember this when testing new configurations. - The decision to use simple CORS versus preflight CORS is made by the browser. The server can handle both, using the same configuration rules.
- CORS does not strengthen security, it weakens security.
- 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 theOPTIONS
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 theorigin
request header does not match any hostname listed in theorigin
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 themethods
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 theheaders
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.