$ emrebener
home topics web development a comprehensive guide to the same-origin policy and the cors policy

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

author: emre bener read time: 15 min about: cross-origin resource sharing, same-origin policy
published: updated: mentions: http, javascript, web browser, rfc 6454, preflight request

1. Preface

This post walks through the same-origin policy (SOP) and the closely related CORS policy in depth. It’s meant to work two ways: as a guide if you’re learning these from the ground up, and as a quick reference once you already know the shape of them. The material is pulled mostly from the WHATWG fetch spec, the superseded W3C CORS recommendation, and MDN, then re-ordered into something readable.

2. Introduction

2.1. Same-Origin Policy (SOP)

SOP is a security mechanism built into modern browsers. It restricts scripts on web pages from freely making cross-origin requests (fetch or XMLHttpRequest), and it’s on by default. Without it, web apps would be exposed to cross-site scripting (XSS), cross-site request forgery (CSRF/XSRF), data theft, and session or cookie hijacking.

2.2. The Benefits and the Shortcomings of SOP

The SOP blocks cross-origin reads to keep malicious sites (or legitimate sites that have been compromised) from reading confidential information off other sites while piggybacking on a user’s session or cookies. But plenty of cross-origin requests are legitimate, and the web needed a way for resource owners to opt in to sharing across origins. CORS is that opt-in.

2.3. Cross-Origin Resource Sharing (CORS)

CORS (sometimes called “HTTP access control”) is a header-based mechanism that lets servers declare which origins are allowed to access their resources, relaxing the SOP on a per-resource basis.

3. 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 1Origin 2OutcomeExplanation
https://example.com/somepath/index.htmlhttps://example.com/differentpath/page.htmlSame Origin ✅The path is irrelevant for same origin checks
https://example.com/somepath/index.htmlhttp://example.com/somepath/index.htmlDifferent Origin ❌The scheme is different
https://example.com/somepath/index.htmlhttps://example.com:999/somepath/index.htmlDifferent origin ❌The port is different (default TCP port for HTTPS is 443 when unspecified)
https://example.com/somepath/index.htmlhttps://example2.com/somepath/index.htmlDifferent Origin ❌The host is different

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:devacad.com). Note that the origin must not contain a trailing slash at its end (e.g., https://devacad.com/). Moreover, a “partial wildcard”(e.g., https://*.devacad.com) is not a valid format for origin.
  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.

4. Rules of SOP

SOP generally lets one origin send information to another, but doesn’t let it receive or read information back. The three main types of cross-origin interaction shake out as follows:

Cross-origininteractionWritesallowed ✅Embeddingallowed ✅Readsblocked ❌links, redirects,form submissionsiframes, CSS, images,video/audio, scriptsfetch / XHRresponse bodiesCross-origininteractionWritesallowed ✅Embeddingallowed ✅Readsblocked ❌links, redirects,form submissionsiframes, CSS, images,video/audio, scriptsfetch / XHRresponse bodies

4.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 requests will still be suspect to CORS policy (more on this later).

4.2. Embedding

Embedding cross-origin resources is typically allowed: iframes, CSS, forms, images, video, audio, even scripts. Sites can still push back via CSP headers to block cross-origin embedding of their resources. The details:

iframesCross-origin embedding is usually permitted, but cross-origin reading the document that is loaded in an iframe is not allowed.
CSSCross-origin embedding CSS is allowed using a  element in HTML (or an @import directive in CSS). Appropriate Content-Type response header may be required.
imagesEmbedding 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.
multimediaCross-origin video and audio can be embedded using 
scriptCross-origin scripts can be embedded. However, access to certain APIs (such as cross-origin fetch requests) might be blocked.

4.3. Read Requests

Cross-origin reads are typically not allowed.

5. The CORS Mechanism

CORS works by having the browser step in and send a “preflight” request to the resource owner before any “non-simple” cross-origin read request goes out. The preflight is really just asking permission to make requests from a specific origin.

5.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.

Figuring out whether a request qualifies as “simple” is fiddlier than it sounds. The short version: a request is simple if its method is GET, POST or HEAD, all of its headers are CORS-safelisted, and the Content-Type header (if present) is one of 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.

Cross-origin requestMethod isGET, POST, or HEAD?All headersCORS-safelisted?Content-Type isform-urlencoded,multipart, or text/plain?Simple requestno preflightNon-simplepreflight requiredyesnoyesnoyesnoCross-origin requestMethod isGET, POST, or HEAD?All headersCORS-safelisted?Content-Type isform-urlencoded,multipart, or text/plain?Simple requestno preflightNon-simplepreflight requiredyesnoyesnoyesno

5.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 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.

Client browserResource serverOPTIONS preflightOrigin, Access-Control-Request-Method, Access-Control-Request-Headerspreflight 200 OKAccess-Control-Allow-Origin, Allow-Methods, Allow-Headers (and Allow-Credentials if credentialed)preflight succeeded → browser issues the actual requestactual request (e.g. DELETE)Origin (+ cookies/auth if credentials mode = include)responseAccess-Control-Allow-Origin (+ Allow-Credentials if credentialed)Client browserResource serverOPTIONS preflightOrigin, Access-Control-Request-Method, Access-Control-Request-Headerspreflight 200 OKAccess-Control-Allow-Origin, Allow-Methods, Allow-Headers (and Allow-Credentials if credentialed)preflight succeeded → browser issues the actual requestactual request (e.g. DELETE)Origin (+ cookies/auth if credentials mode = include)responseAccess-Control-Allow-Origin (+ Allow-Credentials if credentialed)

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.

5.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.

ClientServercredentialed GET requestGET /doc HTTP/1.1Origin: https://foo.exampleCookie: pageAccess=2credentialed requestswill include cookiesHTTP/1.1 200 OKHTTP/1.1 200 OKAccess-Control-Allow-Origin: https://foo.exampleAccess-Control-Allow-Credentials: truebrowser expects this header inresponses to credentialed requestsClientServercredentialed GET requestGET /doc HTTP/1.1Origin: https://foo.exampleCookie: pageAccess=2credentialed requestswill include cookiesHTTP/1.1 200 OKHTTP/1.1 200 OKAccess-Control-Allow-Origin: https://foo.exampleAccess-Control-Allow-Credentials: truebrowser expects this header inresponses to credentialed requests

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 credentialed CORS request gets back an Access-Control-Allow-Origin: *, the browser blocks access to the response and throws a CORS error into the console. Credentialed requests require the resource owner to name an actual origin — wildcards aren’t good enough.

5.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.

5.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));

6. Case Study: CORS Policy Demonstration

Below is an example request that will trigger a preflight;

const fetchPromise = fetch("<https://meowfacts.com/sendcatfact>", {
  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 /doc HTTP/1.1
Host: domain2.com -> resource owner's host
...
Origin: <https://domain1.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://domain1.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
...

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.

7. Security Considerations

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

Even with SOP and CORS 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 CORS to protect your website; utilize anti forgery tokens wherever possible and validate requests.
  • If you need to control cross-origin requests 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!

7.2. Machine-to-Machine (M2M) Requests

SOP and CORS are browser-level security policies, and they do not apply to M2M communications. Therefore, you should always authenticate and authorize users before responding to any read requests and validate requests before processing any write requests.

7.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.

7.4. Preventing Access to Embeddable Resources

SOP doesn’t prohibit cross-origin embedding of resources like CSS files, images, video, audio and even scripts. So if you want to prevent cross-origin reads of a static resource, make sure it isn’t embeddable by configuring the necessary CSP headers on your server.

8. Additional Insights and Tips

8.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.

8.2. The Deprecated Origin Setter

It used to be possible to use the document.domain setter to change a document’s origin, as long as the new value was a superdomain of the current one (going from store.company.com to company.com, say), in order to dodge CORS. That feature has since been deprecated — it undercut the protections SOP was meant to provide and made the browser’s origin model harder to reason about, with real interop and security fallout.

8.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.

9. Quick Reference: Request Headers

This table lists the HTTP request headers that clients may use as defined by the CORS policy specification:

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.

10. Quick Reference: Response Headers

This table lists the HTTP response headers that servers return for CORS requests as defined by the CORS policy specification:

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-Control-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.

11. 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’.
    • 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.
    • ‘Basic’ responses are completely available to the calling script.
  • Credentials: 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 set of values that the credentials mode accepts are:
    1. Omit: The default mode. Means that the browser will not include any credentials in the cross-origin request.
    2. Same-origin: The browser includes credentials in the request only if the request is made to the same origin.
    3. Include: The browser always includes credentials in the requests regardless of the origin of the request.

12. References

CORS

SOP