A Comprehensive Guide to the Same-Origin Policy and the CORS Policy

Emrebener
13 min readMay 14, 2024

--

1. Introduction

1.1. Same-Origin Policy (SOP)

SOP is a critical security mechanism built into modern web browsers that restricts scripts on web pages from freely making cross-origin requests (fetch or XMLHttpRequest). This default behavior of browsers aims to mitigate certain attack vectors on the web by controlling cross-origin requests. Without SOP in place, web applications would be vulnerable to a wide range of security threats, including various types of cross-site scripting (XSS) and cross-site request forgery (CSRF/XSRF), data theft, and session hijacking/cookie theft.

1.2. The Benefits and the Challenges of SOP

The prohibition of cross-origin reads imposed by SOP is intended to prevent malicious web sites (or legitimate websites that have been compromised by hackers) from reading confidential information from other web sites while potentially abusing users’ ongoing sessions or cookies. However, there could be genuine cases of cross-origin requests, which arose the need for a policy that would help override the SOP so that resource owners could share their resources with other origins. This is where the CORS policy comes into play.

1.3. Cross-Origin Resource Sharing (CORS)

CORS, also referred to as “HTTP access control,” is an HTTP-header based security mechanism that provides a way for servers to specify which origins are permitted to access their resources, thus relaxing the restrictions imposed by the SOP.Definition of an Origin

2. Definition of an Origin

An origin is defined by the scheme, host, and port of a URL. This concept is also known as the “scheme/host/port tuple,” the “serialized tuple,” or the “origin tuple,” where a “tuple” refers to a collection of items considered as a single entity.

Two URLs have the same origin if their scheme, host and port (if specified) are the same. The following table provides example origin comparisons:

Origin comparison examples table
Origin Comparison Examples

In CORS policy, the Origin header is included in every CORS request, and has one of the following values:

  1. Null: The Origin header will be null for requests that are not cross-origin, such as requests from the same origin.
  2. Origin: For cross-origin requests, the Origin header will contain the actual origin of the requesting site, which includes the scheme (http/https), domain, and port (e.g., https:coolcatfacts.com). The origin must NOT contain a trailing slash at its end (e.g., https://coolcatfacts.com/), and a “partial wildcard” format (e.g., https://*.coolcatfacts.com) is NOT a valid value for the Origin header.
  3. Wildcard (*): The origin header’s value can also be a wildcard (*), which indicates that the request is a simple cross-origin request and does not include credentials (e.g., cookies, authorization headers). This allows the server to respond with the appropriate CORS headers to indicate that any origin is allowed to access the resource.

3. Rules of SOP

SOP generally allows one origin to send information to another origin, but does not allow recieving/reading information from another origin. The three main types of cross-origin interactions are categorized as follows:

3.1. Write Requests

The cross-origin writes are typically allowed, such as links, redirects, and form submissions. However, the reciever of the form submit must still configure CORS and include the Access-Control-Allow-Origin response header if they wish to share the response with the client.

Note that non-simple write requests will still be suspect to CORS policy (more on this later).

3.2. Embedding

Embedding cross-origin resources is typically allowed, including iframes, CSS, forms, images, videos, audio, and even scripts. Note that sites can use CSP headers to prevent cross-origin embedding of their resources. Here’s the details:

  • Iframes: Cross-origin embedding is usually permitted, but cross-origin reading the document that is loaded in an iframe is not allowed.
  • CSS: Cross-origin embedding CSS is allowed using a <link> element in HTML (or an @import directive in CSS). Appropriate Content-Type response header may be required.
  • Images: Embedding cross-origin images is permitted. However, reading cross-origin image data (such as retrieving binary data from a cross-origin image using JavaScript) is not allowed.
  • Multimedia: Cross-origin video and audio can be embedded using <video> and <audio> elements.
  • Scripts: Cross-origin scripts can be embedded. However, access to certain APIs (such as cross-origin fetch requests) might be blocked.

3.3. Read Requests

Cross-origin reads are NOT allowed unless they are simple requests (more on this later).

4. The CORS Mechanism

The CORS policy relies on a mechanism where browsers step in and make a “preflight” request to the resource owner before initiating “non-simple” cross-origin read requests. The preflight request essentially asks for permission to make requests from a specific origin.

4.1. Simple Requests

Requests that don’t trigger a CORS-preflight are referred to as “simple requests”. Note that this terminology was last used in the obsolete W3C spec and is not officially mentioned in the fetch spec which is regarded as the current spec for CORS protocol. Nevertheless, I’ve decided to carry on with the term “simple request” from the old spec to define “allowed cross-origin requests” in this blog, because the same logic still exists in the new spec.

The process of determining whether a request is “simple” is rather complicated. In essence, a request is considered to be a “simple request” if its HTTP request method is either GET, POST or HEAD, all its headers are CORS-safelisted, and if the Content-Type header (if present) is one of the following: application/x-www-form-urlencoded, multipart/form-data, or text/plain.

For a detailed breakdown of CORS-safelisted request headers, you can refer to the CORS Specification.

4.2. CORS-Preflight Request

The CORS-preflight request is an OPTIONS request and includes the following headers:

  • Origin: As every CORS request, the CORS-preflight request will also contain the origin information as a header
  • Access-Control-Request-Method: Indicates which HTTP request method is desired to be used
  • Access-Control-Request-Headers (optional): Informs which headers a future CORS request to the same resource might use.

An example CORS-preflight request looks like this;

OPTIONS /getcatfact HTTP/1.1
Origin: <https://coolcatfacts.com>
Access-Control-Request-Method: DELETE

Upon recieving a CORS-preflight request, the resource owner responds with information about whether it accepts requests from the client’s origin, and if so, with information about the methods and headers it accepts. In the CORS-preflight response, resource owners can also inform clients whether “credentials” (such as Cookies or HTTP Authentication) should be sent.

Note that for the CORS-preflight request, the credentials mode is always set to same-origin. Therefore, a CORS-preflight request never includes credentials. However, for subsequent CORS requests, the credentials mode could change, which depends on whether the resource owner requested credentials.

An example CORS-preflight response could look like this;

HTTP/1.1 200 OK
Access-Control-Allow-Origin: <https://coolcatfacts.com>
Access-Control-Allow-Methods: GET, DELETE, HEAD, OPTIONS

In the CORS-preflight response, if the Access-Control-Allow-Origin header contains either the client’s origin or the wildcard (*) and the HTTP status code is an “OK” code (e.g., 200 or 204), it means the preflight succeeded.

For security reasons, CORS-preflight responses are not available to the calling script when they fail.

While successful CORS-preflight responses are limited to HTTP status codes indicating success (e.g., 200), the specification does not impose any restrictions on the status codes for successful non-preflight CORS requests. As a result, successful HTTP responses to non-preflight CORS requests can use any HTTP status code.

4.3. Credentialed Requests

The CORS policy supports credentialed requests, which instructs the browser to include HTTP cookies and HTTP authentication information with cross-origin requests. Note that by default, fetch() or XMLHttpRequest calls will not send credentials in cross-origin requests.

When using fetch(), the credentials mode is enabled by setting the credentials property to include.

const url = "<https://coolcatfacts.com/GetConfidentialCatInformation>";
const request = new Request(url, { credentials: "include" });
const fetchPromise = fetch(request);
fetchPromise.then((response) => console.log(response));

For XMLHttpRequest, the credentials mode is enabled by setting the withCredentials property to true.

var client = new XMLHttpRequest()
client.open("GET", "<https://coolcatfacts.com/GetConfidentialCatInformation>")
client.withCredentials = true
/* … */

When the browser sends a credentialed request, it will reject the response if the Access-Control-Allow-Credentials header is not set to true. This will cause the response body to be hidden from the calling script even though a response was recieved.

Credentialed CORS request flow illustration
Credentialed Request Flow Illustration

Note that browsers will never include credentials in the preflight request. However, the response to the preflight needs to have Access-Control-Allow-Credentials: true to indicate that the actual request can be made with credentials.

Similarly, if a response to a credentialed CORS request includes an Access-Control-Allow-Origin: * header, the browser will block access to the response and a CORS error will be thrown into console. This is because credentialed CORS requests require the resource owner to indicate an actual origin.

4.4. Browser Handling of Non-CORS-Safelisted Response Headers

For security reasons, browsers will only expose the CORS-safelisted response headers to the calling script, and will filter out all other headers from the response (if there are any). For the purposes of this blog, it is sufficient to understand that browsers will hide non-CORS-related headers in CORS responses, unless the resource owner explicitly specifies the additional headers to be exposed using the Access-Control-Expose-Headers response header.

4.5. No-Cors Mode

It is possible to disable CORS policy at the cost of not being able to see the server response from the calling script. This is called the no-cors mode. In no-cors mode, the browser does not include the Origin header, and hides the response body from the calling script. Here is an example of setting request mode to no-cors in fetch API:

fetch('<https://example.com/api/notify>', {
mode: 'no-cors'
})
.then(response => console.log(response))
.catch(error => console.error(error));

5. Case Study: CORS Policy Demonstration

Below is an example cross-origin request that will trigger a preflight;

const fetchPromise = fetch("<https://example2.com/path>", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "text/xml"
},
body: "<person><name>Emre</name></person>",
});

fetchPromise.then((response) => {
console.log(response.status);
});

This code illustrates creating an XML body to send as a POST request. Since the request contains a Content-Type header that is neither application/x-www-form-urlencoded, multipart/form-data, nor text/plain, the request is preflighted by the browser. The preflight request with the OPTIONS method will look like this (I struck through irrelevant headers):

OPTIONS /path HTTP/1.1
Host: example2.com -> resource owner's host
Origin: <https://example1.com> -> client's serialized origin
Access-Control-Request-Method: POST -> notifies what the method of the actual request will be
Access-Control-Request-Headers: content-type -> notifies what headers the actual request will include

💡 OPTIONS is a safe HTTP/1.1 method that is used to retrieve information from servers, meaning it can’t be used to alter the state of the server.

And below is the CORS-preflight response from the resource owner;

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: <https://example1.com> -> client's origin got returned, meaning preflight succeeded
Access-Control-Allow-Methods: POST, GET, OPTIONS -> these are the methods resource owner allows in future CORS requests
Access-Control-Allow-Headers: Content-Type -> these are the headers resource owner allows in future CORS requests
Access-Control-Max-Age: 86400 -> duration in seconds for which the preflight response can be cached
Vary: Origin

Then, since the preflight succeeded, the actual request will be sent by the browser. This request will still contain the “Origin” header, as every CORS request includes this header.

6. Security Considerations

6.1. Not Every Cross-Origin Request Is Subject to CORS

Even with SOP in place, scripts can still initiate cross-origin requests as long as certain requirements are met (e.g., simple requests). Therefore:

  • Don’t simply rely on SOP to protect your website; utilize anti forgery tokens wherever possible and validate requests.
  • If you need to restrict cross-origin requests being made from your web app, use Content Security Policy (CSP). I am planning to dedicate a blog post to CSP in the future, so stay in touch!

6.2. The Risk of Machine-to-Machine (M2M) Requests

SOP and CORS are browser-level policies, and they do not apply to M2M communications (e.g., HttpClient in .NET, or Postman). Again; SOP is a layer of protection, but you can’t rely on it to protect your server. Therefore, you should always authenticate and authorize users before responding to any read requests and validate requests before processing any write requests.

6.3. Credentialed Requests

Both sharing responses and allowing requests with credentials is rather unsafe, and extreme care must be taken to prevent the confused deputy problem.

6.4. Preventing Access to Embeddable Resources

SOP doesn’t prohibit cross-origin embedding of resources such as CSS files, images, videos, audio, and even scripts. Therefore, if you wish to prevent cross-origin reads of a static resource, ensure that it is not embeddable by configuring necessary CSP headers on your server.

7. Additional Insights and Tips

7.1. Non-Standard Handling of Redirected Preflights

The CORS protocol initially prohibited redirections to a preflight, but this requirement was later removed from the specification. However, this change has not been universally implemented across all browsers. As a result, redirected preflights might still fail on some browsers. Therefore, until all browsers catch up, it is advisable to either avoid the need for a redirection or to perform simple requests that will not trigger CORS.

7.2. The Deprecated Origin Setter

It used to be possible to use the document.domain setter to change origin of the document, as long as the new value was a superdomain of the current domain (such as going from subdomain.domain.com to domain.com) in order to not trigger CORS. However, this feature has been deprecated as it undermined the security protections provided by SOP and complicated the origin model in browsers, leading to interoperability problems and security risks.

7.3. Cross-Origin Browser Data Storage Access

While not necessarily related to SOP or CORS, it’s important to note that access to data stored in Web Storage or IndexedDB is strictly separated by origin. This means that each origin gets its own storage, and scripts from one origin cannot read from or write to the storage that belongs to another origin.

8. Quick References

8.1. Request Headers

  • Origin: This header indicates the origin of the client from which the request originates. It is included in every CORS request sent by the client.
  • Access-Control-Request-Method: This header is used in CORS-preflight requests to let the resource owner know which HTTP method will be used with the actual request. The resource owner responds with the complementary Access-Control-Allow-Methods header.
  • Access-Control-Request-Headers (optional): This header is used in CORS-preflight requests to let the resource owner know which HTTP headers will be used with the actual request. The resource owner responds with the complementary Access-Control-Allow-Headers header.

8.2. Response Headers

  • Access-Control-Allow-Origin: Tells the browser whether the requested resource can be shared. The value can either be the Origin of the client, *, or null. If the value is the origin of the client or wildcard (*), it means the resource can be shared. If the value is null, it means the resource owner refused cross-origin access to requested resource. If “credentials mode” is “include” (indicating a credentialed request), the value cannot be wildcard.
  • Access-Origin-Allow-Credentials: When a CORS-preflight request’s credentials mode was include, this header tells the browser whether or not the actual request can be made using credentials. This header will be ignored by browser if request was not credentialed (”request mode” was not “include”).
  • Access-Control-Allow-Methods: Tells the browser which HTTP request methods are allowed by the resource owner. Wildcard is not allowed if request is credentialed.
  • Access-Control-Allow-Headers: Tells the browser which HTTP headers are allowed by the resource owner. Wildcard is not allowed if request is credentialed.
  • Access-Control-Max-Age: Tells the browser the duration in seconds to cache the results of a preflight request. The default value is 5 seconds.
  • Access-Control-Expose-Headers: Tells the browser which non-CORS-safelisted headers in the response can be exposed/shown to the calling script by listing their names. The value can be wildcard to indicate that all headers can be exposed. Wildcard is not allowed if request is credentialed. Note that CORS-safelisted response headers are already available to the calling script.

9. Glossary

  • CORS Request: As a general term, a CORS request is an HTTP request that includes an ‘Origin’ header.
  • Resource Owner: In the context of CORS, the resource owner is the server that recieves cross-origin requests for its resources. Due to SOP, resource owners must configure CORS in order to be able to serve resources to origins other than its own.
  • Client: In the context of CORS, the client is the party that initiates a cross-origin request to a resource owner. Whether this cross-origin request succeeds or fails will depend on the CORS configuration of the resource owner.
  • Header: Headers, more specifically “HTTP headers,” are components of the HTTP protocol used for communication between a client (such as a web browser) and a server. They are additional pieces of information sent along with the request or response to provide metadata about the HTTP request.
  • Opaque Response: In no-cors mode, the browser does not include the Origin header in the request, and the server's response is considered “opaque”, meaning that the response body will not be accessible to the calling script. This mode is useful in cases where the response from the server is not needed, such as making a request to inform an analytics API. It's important to note that the term 'opaque' is a general concept referring to responses that cannot be accessed, and this is a simplified explanation of its use in the context of the Fetch API.
  • Response Tainting: Each request has an associated ‘response tainting’ value, which can be ‘basic’, ‘cors’, or ‘opaque’. The default value is ‘basic’. CORS requests are marked with ‘cors’, while requests made with ‘no-cors’ mode are marked as ‘opaque’. The response body of ‘opaque’ requests is entirely hidden from the calling script. ‘Cors’ response headers are partially filtered based on certain rules, while ‘basic’ responses are completely available to the calling script.
  • Credentials Mode: In the context of CORS policy, the “credentials mode” is a setting that determines whether the browser should include credentials such as cookies in cross-origin requests. The credentials mode gets set using the credentials property in a fetch request or the withCredentials property in an XMLHttpRequest. The are three modes of “credentials” setting are Omit, Same-Origin and Include. Omit is the default credentials mode which doesn’t include credentials with the request. Same-Origin mode includes credentials in the request only if the request is made to the same origin. Include mode always includes credentials in the requests regardless of the origin of the request.

--

--