• Deep Dive in CORS: History, how it Works, and best practices
  • Original author: Ilija Eftimov
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: snowyYU
  • Proofread by: Kimhooo, Chorer

Learn about the history and evolution of the same origin policy and CORS, dive into CORS and the various types of cross-domain access, and learn (some) best practices.

The translator’s note:

  • The browser used in this article is FireFox, and the code demo results are slightly different from Chrome, etc.
  • The back-end NodeJS version code can be viewed here.

Common browser console error message

No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at example.com/

Access to fetch at ‘example.com’ from origin ‘http://localhost:3000’ has been blocked by CORS policy.

You’ve probably seen these errors before, but if you haven’t, there are plenty of CORS related errors below for your reference.

It’s always annoying to see these errors. To be fair, HOWEVER, CORS is a very useful mechanism that can effectively avoid backend service configuration problems caused by vulnerabilities, prevent malicious attacks, and promote the evolution of Web standards.

Let’s start at the beginning

Start with the birth of the first child resource

A child resource is an HTML element that is typically embedded in a document flow or executed in a related context (such as a

As you can see, if the browser needs to render a page with the tag, it will fetch the associated child resources from one place. If one or more of the protocol, domain name, or port number is different from the destination address when a browser initiates a resource request, the request is a cross-domain request.

The source and across domains

A complete source is made up of three things: the protocol, the compliant domain name, and the port. For example, http://example.com and https://example.com are two different sources — the first uses HTTP and the second uses HTTPS. In addition, HTTP uses port 80 by default, while HTTPS uses port 443 by default. Although the domain names are example.com, they have different protocols and ports, so they belong to different sources.

See – if any of the three factors mentioned above are inconsistent, they are not from the same source.

We are going to https://blog.example.com/posts/foo.html and the following URL to make a comparison, whether the homologous be clear at a glance:

URL Result Reason
https://blog.example.com/posts/bar.html homologous Only the path is different
https://blog.example.com/contact.html homologous Only the path is different
http://blog.example.com/posts/bar.html Different source Different protocols
https://blog.example.com:8080/posts/bar.html Different source Different ports (https://Default port 443)
https://example.com/posts/bar.html Different source Different host names

As an example to illustrate the cross-domain request, if http://example.com/posts/bar.html this page attempts to apply colours to a drawing resources from https://example.com, this address, then the cross-domain request (note that their agreement is different.

Cross-domain requests have many hazards

We know what is homologous and what is cross-domain, now let’s look at the main problems.

After the introduction of < IMG >, new tags have sprung up. Such as

Imagine if CORS did not exist and the browser allowed a variety of cross-domain requests.

Suppose there is a

Heh heh – imagine you’re browsing the Web and you get an email from your bank congratulating you on deleting your account. I know what you’re thinking, if it’s so easy to delete an account, then you can do anything to the bank. Ahem, beside the point.

In order for my evil

Let’s look at another, less sinister example.

I would like to know the staff information of Bang Bang Company, the Intranet of their company is intra.awesome-corp.com. On my website dangerous.com, I put a tag .

For those who do not have access to the target company’s Intranet intra.awesome-corp.com, the above tag will not load the image — it will result in an error message. On the other hand, if you have access to the dangerous.com website, then I know you have access to the dangerous.com Intranet.

That means I’ll be able to get some information about you. It’s not enough information for me to launch a valuable attack, but you have access to bang Bang’s Intranet, which makes the message more valuable to the attacker.

The above two examples are very simple, but they illustrate the need for the same origin policy and CORS. Of course, cross-domain requests do more harm than that. There are hazards we can avoid, but there are also hazards we can’t do anything about — they are naturally embedded in the web. But the number of attacks launched through the media has dropped dramatically — thanks to CORS.

But before we talk about CORS, let’s talk about the same origin policy.

The same-origin policy

The same origin policy prevents cross-domain attacks by blocking access permissions for resources loaded from different sources. However, this policy still allows some tags to load resources from different sources, such as the tag.

The same origin policy was introduced in Netscape Navigator 2.02 in 1995 and was originally intended to secure cross-domain access to the DOM.

All modern browsers implement the same origin policy in their own way, although there is no hard and fast rule for implementing the same origin policy. Details on the same origin policy can be found in RFC6454 of the Internet engineering task force (IETF).

This rule set defines the implementation of the same origin policy:

Tags Cross-origin Note
<iframe> Allow the embedded Depending on theX-Frame-Options
<link> Allow the embedded Maybe it needs to be rightContent-Type
<form> Write enable This tag is often used for cross-domain writes
<img> Allow the embedded Disallows cross-domain reading and loading of JavaScript to<canvas>In the label
<audio> / <video> Allow the embedded
<script> Allow the embedded Access to specific apis may be blocked

The same origin policy solves many problems, but also brings many limitations. Especially in single-page applications and rich media sites, its many rules limit the growth of the site.

In this context, CORS was born with the goal of providing a more flexible way for cross-domain access within the framework of same-origin policy.

Into the CORS

So far we have understood what a source is, how it is defined, the disadvantages of cross-domain requests, and the same origin policy implemented by browsers.

Now it’s time to get familiar with cross-source resource sharing (CORS). CORS is a mechanism that allows access to subresources on a web page to be controlled over a network. This mechanism divides access to sub-resources into three categories:

  1. Cross-domain write operations
  2. Cross-domain resource embedding
  3. Cross-domain read operations

Before we go into all three, it’s important to understand that although browsers (by default) may allow some type of cross-domain request, this does not mean that the request will be received by the server.

Cross-domain writing includes linking, redirecting, and form submission. All of these operations are allowed with CORS enabled in the browser. In some cases, something called a precheck request can affect cross-domain writes, which we’ll describe in more detail below.

Cross-domain embedding refers to sub-resources loaded with tags such as

Subresources like that can be embedded into a website – one of the reasons they were created was to get resources from different sources. This is why cross-domain embedding and cross-domain reading are distinguished and handled differently in CORS.

Cross-domain reads are generated by AJAX/FETCH fetching child resources. By default, browsers restrict such requests. There is, of course, a way to do cross-domain reading by embedding sub-resources, but there is another strategy for dealing with this in today’s browsers.

If your browser is up to date, it should already implement the above strategy.

Cross-domain write operations

Cross-domain write operations sometimes fail, so let’s look at an example of what CORS can do in action.

First, let’s take a look at an HTTP service implemented using Crystal (the framework uses Kemal) language:

require "kemal"

port = ENV["PORT"].to_i || 4000

get "/" do
  "Hello world!"
end

get "/greet" do
  "Hey!"
end

post "/greet" do |env|
  name = env.params.json["name"].as(String)
  "Hello, #{name}!"
end

Kemal.config.port = port
Kemal.run
Copy the code

Hello #{name}! (greet) {Hello #{name}! (greet) {Hello #{name}! . We use the following command to start the small service:

$ crystal run server.cr
Copy the code

The service starts and starts listening to localhost:4000. Using a browser to access localhost:4000, you will see “Hello World” :

Now that our service is running successfully, send a POST /greet request to localhost:4000 from the browser console. We use the fetch method to initiate the request:

fetch('http://localhost:4000/greet', {
  method: 'POST'.headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Ilija' }),
})
  .then((resp) = > resp.text())
  .then(console.log)
Copy the code

After executing this code, we receive a greeting from the service:

This is a non-cross-domain POST request, originating from the http://localhost:4000 (same origin as the destination address) page.

We tried to send a cross-domain request to this address. We open https://google.com and make the same request from the TAB as above:

With this approach, we see the famous CORS error. Although the Crystal service could respond to the request, our browser blocked the request. We can tell from the error message that the request is trying to write across domains.

The first example, we request from http://localhost:4000 page to http://localhost:4000/greet, because the page address and destination address homologous, so don’t intercept the request. In contrast, in the second example, a request from a web site (https://google.com) tries to write to http://localhost:4000, and the browser flags the request and intercepts it.

Preview the request

Looking at the contents of the Network TAB in the developer console, we see that the code above makes two requests:

The interesting thing is that the method of the first request is OPTIONS and the method of the second request is POST.

If you look closely at the OPTIONS request, you’ll see that the browser sends the OPTIONS request first and then the POST request:

Interestingly, even though the response to the OPTIONS request is HTTP 200, it is still marked red in the request list.

This is a pre-check request initiated by modern browsers. If CORS considers a request to be complex, the browser initiates a precheck request first. The criteria for determining a request as complex are as follows:

  • The method used in the request is notGET,POSTorHEAD
  • The request header containsAccept,Accept-LanguageContent-LanguageFields other than
  • The request header containsContent-TypeField, and its value is not presentapplication/x-www-form-urlencoded,multipart/form-datatext/plainOf the three

So in the example above, even though we made a POST request, the browser decided that our request was complex because of the content-Type: application/ JSON in the request header.

If we modify our request and service to send and process text/plain content (instead of JSON), the browser will not make precheck requests:

require "kemal"

get "/" do
  "Hello world!"
end

get "/greet" do
  "Hey!"
end

post "/greet" do |env|
  body = env.request.body

  name = "there"
  name = body.gets.as(String) if! body.nil?

  "Hello, #{name}!"
end

Kemal.config.port = 4000
Kemal.run
Copy the code

Now we can make a request with content-Type: text/plain in the request header:

fetch('http://localhost:4000/greet', {
  method: 'POST'.headers: {
    'Content-Type': 'text/plain',},body: 'Ilija',
})
  .then((resp) = > resp.text())
  .then(console.log)
Copy the code

See, there is no precheck request this time, but the browser’s CORS policy still intercepts the response:

But because we’re not making a complex request, our browser doesn’t block the request:

In short: For cross-domain requests like Text /plain, our service lacks the configuration to respond and cannot handle the request, nor does it handle exceptions uniformly, which has nothing to do with the browser. However, the browser does its best to avoid exposing the response directly to the page and request list. So, in this case, CORS does not intercept the request — it intercepts the response.

The CORS policy in the browser considers this request to be a cross-domain read request, even though the request method is POST, and the content-Type attribute value in the request header states that it is essentially the same as a GET request. Cross-domain read requests are intercepted by default, so we see the intercepted response in the request list.

Eliminating prechecked requests in response to A CORS policy is not a good idea. In fact, if you want the server to handle prechecked requests properly, you should return a response with the correct response header for OPTIONS requests.

When processing OPTIONS requests, you need to know that the browser pays special attention to three properties that appear in the precheck request response header:

  • Access-Control-Allow-MethodsThis attribute identifies which request methods are supported by the RESPONSE URL under the CORS policy.
  • Access-Control-Allow-HeadersThis attribute identifies which headers are supported by the RESPONSE URL under the CORS policy.
  • Access-Control-Max-Age— It means it can be cachedAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersThe number of seconds of information provided in the header (default is 5).

Now look at the complex request example above:

fetch('http://localhost:4000/greet', {
  method: 'POST'.headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Ilija' }),
})
  .then((resp) = > resp.text())
  .then(console.log)
Copy the code

As we already know, when making this request, our browser first checks whether the server can handle the cross-domain request based on the response to the prechecked request. To properly respond to this cross-domain request, we first need to add the OPTIONS /greet endpoint to our service. In the response header for this service, the new endpoint tells the browser that a POST /greet request from the source https://www.google.com with a Content-Type: application/json header can be received.

To do this, we use the access-Control-allow -* response header:

options "/greet" do |env|
  # Allow `POST /greet`...
  env.response.headers["Access-Control-Allow-Methods"] = "POST"
  # ...with `Content-type` header in the request...
  env.response.headers["Access-Control-Allow-Headers"] = "Content-type"
  # ...from https://www.google.com origin.
  env.response.headers["Access-Control-Allow-Origin"] = "https://www.google.com"
end
Copy the code

Restart the service and make another request:

Our request is still being blocked. Even though our OPTIONS /greet endpoint did properly handle the request, we still saw an error message. But the web TAB in the developer tools shows us some interesting information:

The request to the OPTIONS /greet endpoint was successful! But the POST /greet call still fails. If we look at the internal structure of the POST /greet request, we will see a familiar message:

In fact, the request succeeds — the service returns HTTP 200. The precheck request did work — the browser made the POST request without a hitch. But the response to the POST request does not contain header information about the CORS, so even if the browser initiates the request, the response is intercepted by itself.

In order for the browser to properly handle the response from the POST /greet request, we also need to add a CORS header to the POST endpoint:

post "/greet" do |env|
  name = env.params.json["name"].as(String)

  env.response.headers["Access-Control-Allow-Origin"] = "https://www.google.com"

  "Hello, #{name}!"
end
Copy the code

When we add access-Control-Allow-Origin to the response header, we tell the browser to open the https://www.google.com TAB to Access the response content.

Try again:

We see that POST /greet returns the correct response without any error. Take another look at the Network TAB, and you’ll see that both requests are green:

Cross-domain requests can access the POST /greet endpoint in our service by using the correct response header at the precheck endpoint OPTIONS /greet. Most importantly, browsers can finally stop intercepting cross-domain responses after adding the correct CORS response header to the POST /greet endpoint.

Cross-domain read

As we mentioned above, cross-domain reads are blocked by default. This is intentional — we don’t want to load resources from other sources on the current page.

If we add GET /greet request to Crystal service:

get "/greet" do
  "Hey!"
end
Copy the code

We are interviewing the GET /greet endpoint from www.google.com and CORS is blocking:

A closer look at the request reveals some interesting things:

As before, the browser did allow the request to proceed — it received an HTTP 200 response. However, the browser does not display the response to that request in the page/console. Again, CORS does not intercept the request in this example — it intercepts the response.

Just like cross-domain writes, we can set up CORS and make it available for cross-domain reads — by adding headers with access-Control-Allow-Origin:

get "/greet" do |env|
  env.response.headers["Access-Control-Allow-Origin"] = "https://www.google.com"
  "Hey!"
end
Copy the code

When the browser receives a response from the server, it checks the access-Control-Allow-Origin attribute value in the response header to determine whether the page should read the response. Now that we have set the value to https://www.google.com, we can load the response correctly:

This protects the browser from cross-domain reads and gives the back-end service room to respond to specific cross-domain requests.

Configuration CORS

The access-Control-allow-origin attribute in the response header is set to https://www.google.com to comply with the CORS policy in the browser.

post "/greet" do |env|
  body = env.request.body

  name = "there"
  name = body.gets.as(String) if! body.nil?

  env.response.headers["Access-Control-Allow-Origin"] = "https://www.google.com"
  "Hello, #{name}!"
end
Copy the code

This will allow the https://www.google.com source to invoke our service without the browser reporting any errors. With the access-Control-allow-Origin value set, we can try to fetch again:

Success! You can now make a cross-domain request to /greet from https://www.google.com. Alternatively, we can set the corresponding property value in the header to * so that the browser will allow any source to make the correct cross-domain request to our service. Setting this value requires careful consideration, but is safe in most cases. Here’s a summary of advice for you: If a cross-domain request comes from a TAB in traceless mode in the browser, and it gets the data you want to display, you can set a loose value (*) for the CORS policy.

Another way to configure CORS to relax the restrictions on requests is to use response headers with the Access-Control-allow-credentials attribute. When the credetials mode of the request is include, the browser determines whether to expose the response to the front-end JavaScript code based on the access-Control-allow-credentials value in the response header.

The Credetials pattern in the request comes from the Fetch API documentation and traces its origins back to the original XMLHttpRequest object:

var client = new XMLHttpRequest()
client.open('GET'.'/')
client.withCredentials = true
Copy the code

From the fetch method documentation, we know that the withCredentials attribute in XML is used as an optional parameter in the fetch method call:

fetch('/', { credentials: 'include' }).then(/ *... * /)
Copy the code

The optional credentials attribute values are omit, SAME-origin, and include. The backend service can determine how the browser displays the response (through the access-Control-allow-credentials response header) based on the different credentials attribute values in the request.

The Fetch API documentation divides and describes the interaction between CORS and the Fetch API and the security mechanism adopted by the browser in detail.

Some best practices

Before concluding, let’s review some best practices for cross-source resource sharing (CORS).

Facing mass users

A common example is if you have a web site that displays content that is public and doesn’t require users to pay, authenticate or authorize to view it — in this case you can set the response header Access-Control-Allow-Origin: * for requests to Access that content.

It is good to set the value to * in the following scenarios:

  • A large number of users have unrestricted access to this resource
  • This resource needs to be accessible to a large number of users without restriction
  • There are so many different sources and clients accessing resources that you can’t set specific values, or you don’t care about problems caused by cross-domain requests

There are risks if this setting is applied to responding to requests for resources on a private network, such as one that is protected by a firewall or that requires a MOUNTED VPN to access. When you connect to the Intranet through VPN, you have access to Intranet files:

Now, assuming an attacker’s site dangerous.com has a link to an Intranet file, they could (in theory) create a script on their site that has access to that file:

While launching such an attack is difficult and requires a lot of knowledge about VPNS and the files stored in them, it is important to be aware that setting access-Control-Allow-Origin: * is potentially risky.

For internal

Continuing with the previous example, suppose we need to conduct statistical analysis on our website, we may need to collect user experience and behavior with the help of relevant data sent by the user’s browser.

A common approach is to periodically use JavaScript to make asynchronous requests from the user’s browser. There is an API on the back end that receives these requests and then stores and processes the data.

In this case, our back-end API is public, but we don’t want any website to be able to send data to our data capture API. In fact, we’re only interested in requests from our own site — that’s all.

In this example, we set the API’s response header attribute access-Control-Allow-Origin value to the URL of our website. This way, requests from other sources will be blocked by the browser.

The access-Control-allow-Origin attribute set in the API resource response header won’t let the request pass, even if users or other websites try desperately to stuff data into our statistics interface:

The Origin property value in the request header is NUll

Another interesting example is null sources. This happens when you use a browser to open a local file directly with a resource request. For example, a request from some JavaScript running in a static file on the local computer sets the Origin attribute in the request header to NULL.

In this case, if our service does not allow requests whose Origin value is NULL to access our resources, this may affect developer productivity. If your site/product is developer-oriented, you can Allow this type of cross-domain request to Access resources by setting access-Control-Allow-Origin.

Try to avoid using cookies

When using the access-Control-allow-credentials field, cookies are not allowed on requests by default. You only need to set the access-Control-allow-credentials header: True allows cookies to be sent across domain requests. This tells the browser backend service to allow cross-domain requests to carry authentication information (such as cookies).

Allowing and accepting cross-domain cookies can be risky. This exposes you to potential attack agents and should only be used when absolutely necessary.

Cross-domain cookies come into their own when you know exactly which clients will be accessing your server. This is why the CORS rule does not Allow us to set access-Control-allow-Origin: * when cross-domain requests are allowed to carry authentication information

Technically, access-Control-allow-origin: * and access-Control-allow-credentials: true can be used together, but this is an inverse mode and should be avoided.

If you want the service to be accessible by different clients and sources, you should consider developing an API to generate authentication information (using token-based authentication) instead of using cookies. However, if an API approach is not possible, make sure you are protected against cross-site request forgery (CSRF).

Additional reading

I hope this (long) article will give you a clear understanding of CORS, including its principles and the meaning of its existence. Here are some links to references to this article, and some articles about CORS that I personally found great:

  • Cross-Origin Resource Sharing (CORS)
  • Access-Control-Allow-Credentials header on MDN Web Docs
  • Authoritative guide to CORS (Cross-Origin Resource Sharing) for REST APIs
  • The “CORS protocol” section of the Fetch API spec
  • Same-origin policy on MDN Web Docs
  • Quentin’s great summary of CORS on StackOverflow

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.