# CORS
# What is a Domain or Origin?
A domain
refers to the name we give to a webserver's ip address. We could be talking about anything on specific computer or a specific folder on a specific computer. example.com
is a domain name. he .com
part is called the super domain. We can also add a subdomain like www
in front of the domain to get www.example.com
. This would be called a Full qualified domain name.
An origin
refers to a domain name PLUS a protocol, like https:
and a port number. Eg: :80
. All together the origin would look like this - https://www.example.com:80
. That is an origin.
When an HTML file is loaded by a user-agent , typically a browser, the origin of that HTML file defines the origin
for everything else that happens on a webpage.
In this image we are seeing a fetch request for application/json
data being sent from the script that is being run from the origin https://www.mywebsite.com
. The GET
request is being sent to https://api.mywebsite.com
. The protocol, ports, and domain names match so the response is allowed and the script is able to display the data on the page.
# What Does Cross-Origin Mean?
The browser downloads and reads the HTML file looking for other assets to load. If any of those assets come from a different origin
than the HTML file, then we are talking about a cross-origin request.
If the browser is making a simple request for assets that will be displayed directly on the page with no client-side processing then we are allowed.
If we are using JavaScript to get the asset, this opens up the possibility of malicious data interacting with your script and gaining access to user data. Now our cross-origin
request can be intercepted by the browser based on the same-origin
policy.
In this second example, we are making the request from https://anotherwebsite.com
. The origins are different so the the same-origin
policy kicks in and says no.
# Bad Ports
It is worth noting that while you can generally make fetch
requests to your server and respond to them over any port that you want, there are some that are considered BAD.
If the port
you are using appears in this list then your fetch
call will be denied by the browser.
Port | Typical service |
---|---|
1 | tcpmux |
7 | echo |
9 | discard |
11 | systat |
13 | daytime |
15 | netstat |
17 | qotd |
19 | chargen |
20 | ftp-data |
21 | ftp |
22 | ssh |
23 | telnet |
25 | smtp |
37 | time |
42 | name |
43 | nicname |
53 | domain |
69 | tftp |
77 | — |
79 | finger |
87 | — |
95 | supdup |
101 | hostname |
102 | iso-tsap |
103 | gppitnp |
104 | acr-nema |
109 | pop2 |
110 | pop3 |
111 | sunrpc |
113 | auth |
115 | sftp |
117 | uucp-path |
119 | nntp |
123 | ntp |
135 | epmap |
137 | netbios-ns |
139 | 9 netbios-ssn |
143 | imap |
161 | snmp |
179 | bgp |
389 | ldap |
427 | svrloc |
465 | submissions |
512 | exec |
513 | login |
514 | shell |
515 | print err |
526 | tempo |
530 | courier |
531 | chat |
532 | netnews |
540 | uucp |
548 | afp |
554 | rtsp |
556 | remotefs |
563 | nntps |
587 | submission |
601 | syslog-conn |
636 | ldaps |
993 | imaps |
995 | pop3s |
1719 | h323 gatestat |
1720 | h323 hostcall |
1723 | pptp |
2049 | nfs |
3659 | apple-sasl |
4045 | npp |
5060 | sip |
5061 | sips |
6000 | x11 |
6566 | sane-port |
6665 | ircu |
6666 | ircu |
6667 | ircu |
6668 | ircu |
6669 | ircu |
6697 | ircs-u |
# Same-Origin Policy
The same-origin
policy is implemented by all modern browsers and has been for years. You would have to go back to IE 10, whose long term support has officially ended, to find a browser that doesn't implement the CORS same-origin
policy.
Rule One in this policy is that if the origin
of your HTML file is the same as the origin
of the resource that you are requesting with fetch()
then all is fine. Do whatever you want with the asset. Download it, parse it, process it, or display it on the page.
The challenges and rules come when you need an image or JSON data from a different origin
.
# NOT the same origin
When a request is NOT covered by the same-origin
policy, that is when CORS kicks in.
When we make a fetch
call, we can add an options setting for mode
and set it to cors
. We are explicitly telling the browser that we intend to make a CORS
request.
Try running this following line in the browser dev console.
fetch('https://cors-demo.glitch.me/', { mode: 'cors' });
We will get an error message that says something like this:
Access to fetch at 'https://cors-demo.glitch.me/'
from origin 'https://www.example.com' has
been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present
on the requested resource. If an opaque response
serves your needs, set the request's mode
to 'no-cors' to fetch the resource with CORS disabled.
2
3
4
5
6
7
This error message is actually telling us how to fix the problem.
# The Server Needs to Help
The requested resource is missing an Access-Control-Allow-Origin
header. The server needed to send us an HTTP Response which included that header and the value of that header needed to match the origin of our HTML file.
Here is another fetch request that you can test in the console.
fetch('https://cors-demo.glitch.me/allow-cors', { mode: 'cors' });
This one is to a different end point /allow-cors
. The end point name is not important. What is important are the headers in the response.
Open the network tab, find the request for the /allow-cors
endpoint, and click on it. You should see the headers listed now. Scroll down to the Response Headers section. And you should see this:
access-control-allow-origin: *
That is the wildcard character, meaning that ALL origins are allowed to request this resource.
Note: if a request requires credentials then there must be an exact match for the origin. The wildcard character is NOT acceptable.
Ever want to protect a JSON resource so it can only be used on your own website? Server-side, add this header to all responses so that it can only be accessed by your own domain.
With a NodeJS server using Express, it would look something like this:
app.get('/my-resource', (req, res) => {
res.set(('Content-Type': 'application/json'));
res.set(('Access-control-allow-origin': 'https://example.com'));
res.status = 200;
res.send(`{"API-KEY":"123abcdef456"}`);
});
2
3
4
5
6
Now, only requests that are coming from https://example.com
would be allowed to open and view that data.
These rules and steps may seem painful for developers to work around but there are real security risks that we are protecting our users from by the browser forcing us to follow the steps.
# Preflight Requests
When a browser has decided that you are not doing a same-origin
request and it needs to do a CORS test, then it will make a preflight request.
Basically, it is still a fetch
request but it uses the OPTIONS
method, a smaller set of headers, and NO data or encoded files are sent to or received from the server.
The browser wants to know if it is allowed to talk to the server and request files of a specific mime-type
from that origin, for its own current origin.
# Simple Requests
Once we are past the same-origin
policy check the browser will next check to see if the Request is a simple request. A simple request will NOT trigger a pre-flight request.
A request is simple if it:
- Uses
GET
,POST
, orHEAD
. - If headers are added by the script they are only these ones:
Accept
* Safari has extra restrictions on values for this.Accept-Language
* Safari has extra restrictions on values for this.Content-Language
* Safari has extra restrictions on values for this.Content-Type
- If a
Content-Type
header was added, it only has one of these values:
text/plain
application/x-www-form-urlencoded
multipart/form-data
- If using the old
XMLHttpRequest
object, there is no listener for theupload
event. - No
ReadableStream
object is added to the request.
So, again, if it is a Simple Request, there will be no pre-flight request and everything is allowed to proceed.
# Not So Simple
If the request is determined to NOT be a simple request then the preflight request is sent using the OPTIONS
method.
When the browser receive the response to the preflight request, it looks for the access-control-allow-origin
header and compares it to your origin
header.
If the two do NOT match, then the fetch
request is denied and you get the error message in the console, that we saw above.
# Other Access-Control- Headers
There are actually other Headers
in the Request
and the Response
that start with Access-Control-
.
Here is an example of a preflight request and response.
OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:71.0) Gecko/20100101 Firefox/81.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
HTTP/1.1 204 No Content
Date: Mon, 29 Mar 2021 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
In the Request we are using *-Request-Method
to explain what we want to do after the preflight. We are using *-Request-Headers
to list the headers that we will be adding via Script to the actual request.
In the Response we are getting *-Allow-Origin
for the browser to compare to our origin
header from the Request. We get *-Allow-Methods
to say what the server is willing to accept for that endpoint. We get *-Allow-Headers
to confirm that we are allowed to use those custom headers in our full request. And finally, we get *-Max-Age
to say that this response will be valid for 86400 seconds (24 hours).
There is also a Access-Control-Allow-Credentials
Response header to indicate whether or not cookies and authentication information is being sent and received. See the HTML crossorigin attribute notes att the bottom of this page for an example of sending those credentials.
If you want to send these credentials with your fetch
request then add this to the options object in your fetch call. The value can be either omit
or include
. The default value is omit
.
fetch(url, {
mode: 'cors',
credentials: 'include',
});
2
3
4
You won't see a
credentials
header in your Request. If you set it toinclude
, then you will see thecookies
andauthentication
headers as required.
Another response header is Access-Control-Expose-Headers
which is a list of custom headers that are not just allowed by the server, but ones that you are allowed to access through the Response with JavaScript.
Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
If we got this response in our preflight request then we would be able to do this.
fetch(url).then((response) => {
console.log(response.headers.get('X-My-Custom-Header'));
console.log(response.headers.get('X-Another-Custom-Header'));
return response.json();
});
2
3
4
5
# Request Modes for Fetch Requests
When making your fetch
call we can set a mode
option.
fetch(url, {
method: `POST`
mode: `cors`,
credentials: 'omit'
})
2
3
4
5
You can define a mode for a fetch request such that only certain requests will resolve. The modes you can set are as follows:
navigate
You will see this value in the network tab of the browser dev tools. We cannot use this for a fetch because it means that the user-agent wants to replace the current document with the one being retrieved.same-origin
only succeeds for requests for assets on the same origin, all other requests will reject.cors
will allow requests for assets on the same-origin and other origins which return the appropriate CORs headers.cors-with-forced-preflight
will always perform a preflight check before making the actual request.no-cors
is intended to make requests to other origins that do not have CORS headers and result in an opaque response, but, this isn't possible in the window global scope at the moment.
# Preflight Responses
If the browser gets to the point of sending a preflight request, when the response comes back from the server, then the primary requirement is this:
The origin
Request header value MUST match the value in the Access-Control-Allow-Origin
Response header.
If this does not happen then you will get that error.
# What is an Opaque Response
An opaque response is for a request made for a resource on a different origin that doesn't return CORS headers. With an opaque response we won't be able to read the data returned or view the status of the request, meaning we can't check if the request was successful or not.
We get a response from the server. Just not a usable one.
Did you fetch an image and you now want to put it into a <canvas>
element? That means being able to access the actual data in the file. If you get an opaque response you would not be able to put the image into the <canvas>
.
# Mime-Type Failures
Browsers can also do checks to see if the Response Content-Type
header matches what the content appears to be. For example, if the Content-Type
header says that the file is text/plain
but it is clearly a binary file like an image/png
then the browser can also reject the response.
# Summary of Steps
If your request:
- Does Not meet the
same-origin
policy. - Uses a port from the BAD PORT list.
- Does Not meet the Simple Request requirements.
- Does Not successfully complete a preflight request and match the
origin
with theaccess-control-allow-origin
. - Does not meet the requirements of the other
Access-Control-*
headers. - Does not have a valid
Content-Type
.
Then...
Since you made it this far, we should also talk about CORB.
# HTML crossorigin attribute
While not directly connected with the CORS policy it is useful to discuss this attribute because it does have to do with cross-origin
security.
The crossorigin attribute, can be added to the <audio>
, <img>
, <link>
, <script>
, and <video>
elements, to provide support for CORS, by defining how the element handles cross-origin
requests, thereby enabling the configuration of the CORS requests for the element's fetched data. Depending on the element, the attribute can be a CORS settings attribute.
The values for the attribute are anonymous
or use-credentials
<img
src="https://picsum.photos/id/1023/300/300"
crossorigin="use-credentials"
alt="sample image"
/>
2
3
4
5
Adding this attribute to those elements lets you decide whether or not identifying information along with those requests that are being sent to different origins.
If you needed to have a cookie set or other identifying information so that the server will allow you to get the asset, then you need to use the value "use-credentials".