I’m building a front-end only basic Weather App using reactjs. For API requests I’m using Fetch API.
In my app, I’m getting the current location from a simple API I found
and it gives the location as a JSON object. But when I request it through Fetch API, I’m getting this error.
Failed to load http://ip-api.com/json: Request header field Access-Control-Allow-Origin is not allowed by Access-Control-Allow-Headers in preflight response.
So I searched through and found multiple solutions to fix this.
- Enabling CORS in Chrome solves the error but when I deploy the app on heroku, how can I access it through a mobile device without running into the same CORS issue.
- I found an proxy API which enables the CORS requests. But as this is a location request, this gives me the location of the proxy server. So it’s not a solution.
- I’ve gone through this Stackoverflow question and added the headers to the header in my http request but it doesn’t solve the problem. (Still it gives the same error).
So how can I solve the issue permanently ? What’s the best solution I can use to solve the CORS issue for http requests in Fetch API ?
Phil
155k23 gold badges240 silver badges243 bronze badges
asked Feb 11, 2018 at 4:09
Thidasa PankajaThidasa Pankaja
9118 gold badges25 silver badges42 bronze badges
2
To the countless future visitors:
If my original answer doesn’t help you, you may have been looking for:
- Trying to use fetch and pass in mode: no-cors
- What is an opaque response, and what purpose does it serve?
Regarding the issue faced by the OP…
That API appears to be permissive, responding with Access-Control-Allow-Origin:*
I haven’t figured out what is causing your problem, but I don’t think it is simply the fetch API.
This worked fine for me in both Firefox and Chrome…
fetch('http://ip-api.com/json')
.then( response => response.json() )
.then( data => console.log(data) )
answered Feb 11, 2018 at 5:15
Brent BradburnBrent Bradburn
51k17 gold badges151 silver badges171 bronze badges
5
if you are making a post, put or patch request, you have to stringify your data with body: JSON.stringify(data)
fetch(URL,
{
method: "POST",
body: JSON.stringify(data),
mode: 'cors',
headers: {
'Content-Type': 'application/json',
}
}
).then(response => response.json())
.then(data => {
....
})
.catch((err) => {
....
})
});
answered Apr 9, 2021 at 1:45
2
You should use the proxy solution, but pass it the IP of the client instead of the proxy. Here is an example URL format for the API you specified, using the IP of WikiMedia:
http://ip-api.com/json/208.80.152.201
answered Feb 11, 2018 at 4:16
TallboyTallboy
12.7k13 gold badges78 silver badges172 bronze badges
4
Many websites have JavaScript functions that make network requests to a server, such as a REST API. The web pages and APIs are often in different domains. This introduces security issues in that any website can request data from an API. Cross-Origin Resource Sharing (CORS) provides a solution to these issues. It became a W3C recommendation in 2014. It makes it the responsibility of the web browser to prevent unauthorized access to APIs. All modern web browsers enforce CORS. They prevent JavaScript from obtaining data from a server in a domain different than the domain the website was loaded from, unless the REST API server gives permission.
From a developer’s perspective, CORS is often a cause of much grief when it blocks network requests. CORS provides a number of different mechanisms for limiting JavaScript access to APIs. It is often not obvious which mechanism is blocking the request. We are going to build a simple web application that makes REST calls to a server in a different domain. We will deliberately make requests that the browser will block because of CORS policies and then show how to fix the issues. Let’s get started!
NOTE: The code for this project can be found on GitHub.
Table of Contents
- Prerequisites to Building a Go Application
- How to Build a Simple Web Front End
- How to Build a Simple REST API in Go
- How to Solve a Simple CORS Issue
- Allowing Access from Any Origin Domain
- CORS in Flight
- What Else Does CORS Block?
- Restrictions on Response Headers
- Credentials Are a Special Case
- Control CORS Cache Configuration
- How to Prevent CORS Issues with Okta
- How CORS Prevents Security Issues
Prerequisites to Building a Go Application
First things first, if you don’t already have Go installed on your computer you will need to download and install the Go Programming Language.
Now, create a directory where all of our future code will live.
Finally, we will make our directory a Go module and install the Gin package (a Go web framework) to implement a web server.
go mod init cors
go get github.com/gin-gonic/gin
go get github.com/gin-contrib/static
A file called go.mod
will get created containing the dependencies.
How to Build a Simple Web Front End
We are going to build a simple HTML and JavaScript front end and serve it from a web server written using Gin.
First of all, create a directory called frontend
and create a file called frontend/index.html
with the following content:
<html>
<head>
<meta charset="UTF-8" />
<title>Fixing Common Issues with CORS</title>
</head>
<body>
<h1>Fixing Common Issues with CORS</h1>
<div>
<textarea id="messages" name="messages" rows="10" cols="50">Messages</textarea><br/>
<form id="form1">
<input type="button" value="Get v1" onclick="onGet('v1')"/>
<input type="button" value="Get v2" onclick="onGet('v2')"/>
</form>
</div>
</body>
</html>
The web page has a text area to display messages and a simple form with two buttons. When a button is clicked it calls the JavaScript function onGet()
passing it a version number. The idea being that v1
requests will always fail due to CORS issues, and v2
will fix the issue.
Next, create a file called frontend/control.js
containing the following JavaScript:
function onGet(version) {
const url = "http://localhost:8000/api/" + version + "/messages";
var headers = {}
fetch(url, {
method : "GET",
mode: 'cors',
headers: headers
})
.then((response) => {
if (!response.ok) {
throw new Error(response.error)
}
return response.json();
})
.then(data => {
document.getElementById('messages').value = data.messages;
})
.catch(function(error) {
document.getElementById('messages').value = error;
});
}
The onGet()
function inserts the version number into the URL and then makes a fetch request to the API server. A successful request will return a list of messages. The messages are displayed in the text area.
Finally, create a file called frontend/.go
containing the following Go code:
package main
import (
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(static.Serve("/", static.LocalFile("./frontend", false)))
r.Run(":8080")
}
This code simply serves the contents of the frontend
directory on requests on port 8080. Note that JavaScript makes a call to port http://localhost:8000
which is a separate service.
Start the server and point a web browser at http://localhost:8080
to see the static content.
How to Build a Simple REST API in Go
Create a directory called rest
to contain the code for a basic REST API.
NOTE: A separate directory is required as Go doesn’t allow two program entry points in the same directory.
Create a file called rest/server.go
containing the following Go code:
package main
import (
"fmt"
"strconv"
"net/http"
"github.com/gin-gonic/gin"
)
var messages []string
func GetMessages(c *gin.Context) {
version := c.Param("version")
fmt.Println("Version", version)
c.JSON(http.StatusOK, gin.H{"messages": messages})
}
func main() {
messages = append(messages, "Hello CORS!")
r := gin.Default()
r.GET("/api/:version/messages", GetMessages)
r.Run(":8000")
}
A list called messages
is created to hold message objects.
The function GetMessages()
is called whenever a GET request is made to the specified URL. It returns a JSON string containing the messages. The URL contains a path parameter which will be v1
or v2
. The server listens on port 8000.
The server can be run using:
How to Solve a Simple CORS Issue
We now have two servers—the front-end one on http://localhost:8080
, and the REST API server on http://localhost:8000
. Even though the two servers have the same hostname, the fact that they are listening on different port numbers puts them in different domains from the CORS perspective. The domain of the web content is referred to as the origin. If the JavaScript fetch
request specifies cors
a request header will be added identifying the origin.
Origin: http://localhost:8080
Make sure both the frontend and REST servers are running.
Next, point a web browser at http://localhost:8080
to display the web page. We are going to get JavaScript errors, so open your browser’s developer console so that we can see what is going on. In Chrome it is *View** > Developer > Developer Tools.
Next, click on the Send v1 button. You will get a JavaScript error displayed in the console:
Access to fetch at ‘http://localhost:8000/api/v1/messages’ from origin ‘http://localhost:8080’ 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.
The message says that the browser has blocked the request because of a CORS policy. It suggests two solutions. The second suggestion is to change the mode
from cors
to no-cors
in the JavaScript fetch request. This is not an option as the browser always deletes the response data when in no-cors
mode to prevent data from being read by an unauthorized client.
The solution to the issue is for the server to set a response header that allows the browser to make cross-domain requests to it.
Access-Control-Allow-Origin: http://localhost:8080
This tells the web browser that the cross-origin requests are to be allowed for the specified domain. If the domain specified in the response header matches the domain of the web page, specified in the Origin
request header, then the browser will not block the response being received by JavaScript.
We are going to set the header when the URL contains v2
. Change the GetMessages()
function in cors/server.go
to the following:
func GetMessages(c *gin.Context) {
version := c.Param("version")
fmt.Println("Version", version)
if version == "v2" {
c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
}
c.JSON(http.StatusOK, gin.H{"messages": messages})
}
This sets a header to allow cross-origin requests for the v2
URI.
Restart the server and go to the web page. If you click on Get v1
you will get blocked by CORS. If you click on Get v2
, the request will be allowed.
A response can only have at most one Access-Control-Allow-Origin
header. The header can only specify only one domain. If the server needs to allow requests from multiple origin domains, it needs to generate an Access-Control-Allow-Origin
response header with the same value as the Origin
request header.
Allowing Access from Any Origin Domain
There is an option to prevent CORS from blocking any domain. This is very popular with developers!
Access-Control-Allow-Origin: *
Be careful when using this option. It will get flagged in a security audit. It may also be in violation of an information security policy, which could have serious consequences!
CORS in Flight
Although we have fixed the main CORS issue, there are some limitations. One of the limitations is that only the HTTP GET, and OPTIONS methods are allowed. The GET and OPTIONS methods are read-only and are considered safe as they don’t modify existing content. The POST, PUT, and DELETE methods can add or change existing content. These are considered unsafe. Let’s see what happens when we make a PUT request.
First of all, add a new form to client/index.html
:
<form id="form2">
<input type="text" id="puttext" name="puttext"/>
<input type="button" value="Put v1" onclick="onPut('v1')"/>
<input type="button" value="Put v2" onclick="onPut('v2')"/>
</form>
This form has a text input and the two send buttons as before that call a new JavaScript function.
Next, add the JavaScript funtion to client/control.js
:
function onPut(version) {
const url = "http://localhost:8000/api/" + version + "/messages/0";
var headers = {}
fetch(url, {
method : "PUT",
mode: 'cors',
headers: headers,
body: new URLSearchParams(new FormData(document.getElementById("form2"))),
})
.then((response) => {
if (!response.ok) {
throw new Error(response.error)
}
return response.json();
})
.then(data => {
document.getElementById('messages').value = data.messages;
})
.catch(function(error) {
document.getElementById('messages').value = error;
});
}
This makes a PUT request sending the form parameters in the request body. Note that the URI ends in /0
. This means that the request is to create or change the message with the identifier 0
.
Next, define a PUT handler in the main()
function of rest/server.go
:
r.PUT("/api/:version/messages/:id", PutMessage)
The message identifier is extracted as a path parameter.
Finally, add the request handler function to rest/server.go
:
func PutMessage(c *gin.Context) {
version := c.Param("version")
id, _ := strconv.Atoi(c.Param("id"))
text := c.PostForm("puttext")
messages[id] = text
if version == "v2" {
c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
}
c.JSON(http.StatusOK, gin.H{"messages": messages})
}
This updates the message from the form data and sends the new list of messages. The function also always sets the allow origin header, as we know that it is required.
Now, restart the servers and load the web page. Make sure that the developer console is open. Add some text to the text input and hit the Send v1
button.
You will see a slightly different CORS error in the console:
Access to fetch at ‘http://localhost:8000/api/v1/messages/0’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: 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.
This is saying that a preflight check was made, and that it didn’t set the Access-Control-Allow-Origin
header.
Now, look at the console output from the API server:
[GIN] 2020/12/01 - 11:10:09 | 404 | 447ns | ::1 | OPTIONS "/api/v1/messages/0"
So, what is happening here? JavaScript is trying to make a PUT request. This is not allowed by CORS policy. In the GET example, the browser made the request and blocked the response. In this case, the browser refuses to make the PUT request. Instead, it sent an OPTIONS request to the same URI. It will only send the PUT if the OPTIONS request returns the correct CORS header. This is called a preflight request. As the server doesn’t know what method the OPTIONS preflight request is for, it specifies the method in a request header:
Access-Control-Request-Method: PUT
Let’s fix this by adding a handler for the OPTIONS request that sets the allow origin header in cors/server.go
:
func OptionMessage(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
}
func main() {
messages = append(messages, "Hello CORS!")
r := gin.Default()
r.GET("/api/:version/messages", GetMessages)
r.PUT("/api/:version/messages/:id", PutMessage)
r.OPTIONS("/api/v2/messages/:id", OptionMessage)
r.Run(":8000")
}
Notice that the OPTIONS handler is only set for the v2
URI as we don’t want to fix v1
.
Now, restart the server and send a message using the Put v2
button. We get yet another CORS error!
Access to fetch at ‘http://localhost:8000/api/v2/messages/0’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.
This is saying that the preflight check needs another header to stop the PUT request from being blocked.
Add the response header to cors/server.go
:
func OptionMessage(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
c.Header("Access-Control-Allow-Methods", "GET, OPTIONS, POST, PUT")
}
Restart the server and resend the message. The CORS issues are resolved.
What Else Does CORS Block?
CORS has a very restrictive policy regarding which HTTP request headers are allowed. It only allows safe listed request headers. These are Accept
, Accept-Language
, Content-Language
, and Content-Type
. They can only contain printable characters and some punctuation characters are not allowed. Header values can’t have more than 128 characters.
There are further restrictions on the Content-Type
header. It can only be one of application/x-www-form-urlencoded
, multipart/form-data
, and text/plain
. It is interesting to note that application/json
is not allowed.
Let’s see what happens if we send a custom request header. Modify the onPut()
function in frontend/control.js
to set a header:
var headers = { "X-Token": "abcd1234" }
Now, make sure that the servers are running and load or reload the web page. Type in a message and click the Put v1
button. You will see a CORS error in the developer console.
Access to fetch at ‘http://localhost:8000/api/v2/messages/0’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: Request header field x-token is not allowed by Access-Control-Allow-Headers in preflight response.
Any header which is not CORS safe-listed causes a preflight request. This also contains a request header that specifies the header that needs to be allowed:
Access-Control-Request-Headers: x-token
Note that the header name x-token
is specified in lowercase.
The preflight response can allow non-safe-listed headers and can remove restrictions on safe listed headers:
Access-Control-Allow-Headers: X_Token, Content-Type
Next, change the function in cors/server.go
to allow the custom header:
func OptionMessage(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT")
c.Header("Access-Control-Allow-Headers", "X-Token")
}
Restart the server and resend the message by clicking the Put v2
button. The request should now be successful.
CORS also places restrictions on response headers. There are seven whitelisted response headers: Cache-Control
, Content-Language
, Content-Length
, Content-Type
, Expires
, Last-Modified
, and Pragma
. These are the only response headers that can be accessed from JavaScript. Let’s see this in action.
First of all, add a text area to frontend/index.html
to display the headers:
<textarea id="headers" name="headers" rows="10" cols="50">Headers</textarea><br/>
Next, replace the first then
block in the onPut
function in frontend/control.js
to display the response headers:
.then((response) => {
if (!response.ok) {
throw new Error(response.error)
}
response.headers.forEach(function(val, key) {
document.getElementById('headers').value += 'n' + key + ': ' + val;
});
return response.json();
})
Finally, set a custom header in rest/server.go
:
func PutMessage(c *gin.Context) {
version := c.Param("version")
id, _ := strconv.Atoi(c.Param("id"))
text := c.PostForm("puttext")
messages[id] = text
if version == "v2" {
c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
}
c.Header("X-Custom", "123456789")
c.JSON(http.StatusOK, gin.H{"messages": messages})
}
Now, restart the server and reload the web page. Type in a message and hit Put v2
. You should see some headers displayed, but not the custom header. CORS has blocked it.
This can be fixed by setting another response header to expose the custom header in server/server.go
:
func PutMessage(c *gin.Context) {
version := c.Param("version")
id, _ := strconv.Atoi(c.Param("id"))
text := c.PostForm("puttext")
messages[id] = text
if version == "v2" {
c.Header("Access-Control-Expose-Headers", "X-Custom")
c.Header("Access-Control-Allow-Origin", "http://localhost:8080")
}
c.Header("X-Custom", "123456789")
c.JSON(http.StatusOK, gin.H{"messages": messages})
}
Restart the server and reload the web page. Type in a message and hit Put v2
. You should see some headers displayed, including the custom header. CORS has now allowed it.
Credentials Are a Special Case
There is yet another CORS blocking scenario. JavaScript has a credentials
request mode. This determines how user credentials, such as cookies are handled. The options are:
omit
: Never send or receive cookies.same-origin
: This is the default, that allows user credentials to be sent to the same origin.include
: Send user credentials even if cross-origin.
Let’s see what happens.
Modify the fetch call in the onPut()
function in frontend/Control.js
:
fetch(url, {
method : "PUT",
mode: 'cors',
credentials: 'include',
headers: headers,
body: new URLSearchParams(new FormData(document.getElementById("form2"))),
})
Now, make sure that the client and server are running and reload the web page. Send a message as before. You will get another CORS error in the developer console:
Access to fetch at ‘http://localhost:8000/api/v2/messages/0’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: The value of the ‘Access-Control-Allow-Credentials’ header in the response is ‘’ which must be ‘true’ when the request’s credentials mode is ‘include’.
The fix for this is to add another header to both the OptionMessage()
, and PutMessage()
functions in cors/server.go
as the header needs to be present in both the preflight request and the actual request:
c.Header("Access-Control-Allow-Credentials", "true")
The request should now work correctly.
The credentials issue can also be resolved on the client by setting the credentials mode to omit
and sending credentials as request parameters, instead of using cookies.
Control CORS Cache Configuration
The is one more CORS header that hasn’t yet been discussed. The browser can cache the preflight request results. The Access-Control-Max-Age
header specifies the maximum time, in seconds, the results can be cached. It should be added to the preflight response headers as it controls how long the Access-Control-Allow-Methods
and Access-Control-Allow-Headers
results can be cached.
Access-Control-Max-Age: 600
Browsers typically have a cap on the maximum time. Setting the time to -1
prevents caching and forces a preflight check on all calls that require it.
How to Prevent CORS Issues with Okta
Authentication providers, such as Okta, need to handle cross-domain requests to their authentication servers and API servers. This is done by providing a list of trusted origins. See Okta Enable CORS for more information.
How CORS Prevents Security Issues
CORS is an important security feature that is designed to prevent JavaScript clients from accessing data from other domains without authorization.
Modern web browsers implement CORS to block cross-domain JavaScript requests by default. The server needs to authorize cross-domain requests by setting the correct response headers.
CORS has several layers. Simply allowing an origin domain only works for a subset of request methods and request headers. Some request methods and headers force a preflight request as a further means of protecting data. The actual request is only allowed if the preflight response headers permit it.
CORS is a major pain point for developers. If a request is blocked, it is not easy to understand why, and how to fix it. An understanding of the nature of the blocked request, and of the CORS mechanisms, can easily overcome such issues.
If you enjoyed this post, you might like related ones on this blog:
- What is the OAuth 2.0 Implicit Grant Type?
- Combat Side-Channel Attacks with Cross-Origin Read Blocking
Follow us for more great content and updates from our team! You can find us on Twitter, Facebook, subscribe to our YouTube Channel or start the conversation below.
В этой статье подробно разобрана история и эволюция политики одинакового источника и CORS, а также расписаны разные типы доступа между различными источниками, а также несколько оптимальных решений работы с ними.
Если вы давно хотели разобраться в CORS и вас достали постоянные ошибки, добро пожаловать под кат.
Ошибка в консоли вашего браузера
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 https://example.com/
Access to fetch at ‘https://example.com’ from origin ‘http://localhost:3000’ has been blocked by CORS policy.
Я уверен, вам уже доводилось видеть похожие сообщения об ошибках в консоли вашего браузера. Если нет, не волнуйтесь, скоро увидите. Все программисты достаточно часто натыкаются на CORS-ошибки.
Эти всплывающие ошибки в процессе разработки просто раздражают. Но на самом деле, CORS — это невероятно полезный механизм в мире неправильно настроенных веб серверов, злоумышленников, орудующих в интернете и организаций, продвигающих веб-стандарты.
Но давайте-ка пойдем к истокам…
В начале был первый субресурс
Субресурс — это HTML элемент, который требуется вставить в документ или выполнить в контексте этого документа. В 1993 году был введен первый тег <img>
. С появлением веб стал более красивым, но заодно и стал сложнее.
Верни мне мой 1993 г.
Как вы поняли, если ваш браузер отображает страницу с <img>
, он должен запросить этот тег из источника. Если браузер запрашивает тег из источника, который отличается от получателя по схеме, в полностью определенному имени хоста или порту, то это и есть запрос между различными источниками (cross-origin request).
Источники & cross-origin
Источник идентифицируется следующей тройкой параметров: схема, полностью определенное имя хоста и порт. Например, <http://example.com>
и <https://example.com>
имеют разные источники: первый использует схему http, а второй https. Вдобавок, портом для http по умолчанию является 80, тогда как для https — 443. Следовательно, в данном примере 2 источника отличаются схемой и портом, тогда как хост один и тот же (example.com).
Таким образом, если хотя бы один из трех элементов у двух ресурсов отличается, то источник ресурсов также считается разным.
Если, к примеру, мы будем сравнивать источник <https://blog.example.com/posts/foo.html>
с другими источниками, то мы получим следующие результаты:
Пример запроса между различными источниками: когда ресурс (то есть, страница) типа <http://example.com/posts/bar.html>
попробует отобразить тег из источника <https://example.com>
(заметьте, что схема поменялась!).
Слишком много опасностей запроса между различными источниками
Теперь, когда мы определились, что такое совместное использования ресурсов между разными и одинаковыми источниками, давайте посмотрим, в чем же дело.
Когда тег <img>
появился во Всемирной Паутине, мы тем самым открыли ящик Пандоры. Некоторое время спустя в Сети появились теги <script>
, <frame>
, <video>
, <audio>
, <iframe>
, <link>
, <form>
и так далее. Эти теги могут быть загружены браузером уже после загрузки страницы, поэтому они все могут быть запросами в пределах одного источника и между о разными источниками.
Давайте погрузимся в воображаемый мир, где не существует CORS и веб-браузеры допускают все типы запросов между источниками.
Предположим, у меня есть страница на сайте evil.com с <script>
. На первый взгляд это обычная страница, где можно прочесть полезную информацию. Но я специально создал код в теге <script>
, который будет отправлять специально созданный запрос по удалению аккаунта (DELETE/account) на сайт банка. Как только вы загрузили страницу, JavaScript запускается и AJAX-запрос попадает в API банка.
Вжух, нет вашего аккаунта
Немыслимо, да? Представьте, что вы читаете что-то на веб странице и вам приходит электронное письмо от банка, что ваш аккаунт успешно удален. Знаю, знаю… если бы было так просто провести любую банковскую операцию… Отвлекся.
Для того чтобы мой вредоносный <script>
сработал, ваш браузер должен был также отправить ваши учетные данные (куки), взятые с банковского сайта, как часть запроса. Именно таким образом банковские серверы идентифицировали бы вас и знали, какой аккаунт нужно удалить.
Давайте рассмотрим другой, не такой зловещий сценарий.
Мне нужно опознать людей, которые работают на Awesome Corp, внутренний сайт этой компании находится по адресу intra.awesome-corp.com
. На моем сайте, dangerous.com
, у меня есть <img src="https://intra.awesome-corp.com/avatars/john-doe.png">
.
У пользователей, у которых нет активного сеанса с intra.awesome-corp.com, аватарка не отобразится, так как это приведет к ошибке. Однако если вы совершили вход во внутреннюю сеть Awesome Corp., как только вы откроете мой dangerous.com сайт, я буду знать, что у вас там есть аккаунт.
Это означает, что я смогу прощупать определенную информацию о вас. Конечно, для меня будет сложнее устроить атаку, но знание о том, что у вас есть доступ к Awesome Corp., является потенциальным направлением для атаки.
Утечка информации к 3-им лицам
Эти два примера крайне упрощены, но именно такие угрозы обусловили необходимость политики одинакового источника и CORS… Существуют разнообразные опасности, связанные с запросами между разными источниками. Некоторые из них можно сгладить, другие нет: они укоренены в природе интернета. Однако огромное количество заблокированных атак — это заслуга CORS.
Но до зарождения CORS существовала политика одинакового источника.
Политика одинакового источника
Политика одинакового источника предотвращает cross-origin атаки, блокируя доступ для прочтения загружаемых ресурсов из другого источника. Такая политика все еще разрешает нескольким тегам вроде <img>
загружать ресурсы из других источников.
Политика одинакового источника введена Netscape Navigator 2.02 в 1995 году, изначально для защищенного cross-origin доступа к Объектной модели документа (DOM).
Даже несмотря на то, что внедрение политики одинакового источника не требует придерживаться определенного порядка действий, все современные браузеры следуют этой политике в той или иной форме. Принципы политики описаны в запросе на спецификацию RFC6454 Инженерного совета интернета (Internet Engineering Task Force).
Выполнение политики одинакового источника определено этим сводом правил:
Политика одинакового источника решает много проблем, но она довольно ограничительная. В век одностраничных приложений и сайтов, нагруженных медиа-контентом, эта политика не дает ни воздуха разработчикам, ни легко играться настройками.
CORS же появился с целью смягчения политики одинакового источника и для тонкой настройки доступа между различными источниками.
Врываемся в CORS
Я уже разъяснил, что такое источник, как он определяется, какие ошибки бывают у запросов с различными источниками и политику общего происхождения, выполняемые браузером.
Давайте разберемся с совместным использованием ресурсов различными источниками (CORS). CORS — это механизм, который дает контролировать доступ к тегам на веб странице по сети. Механизм классифицируется на три разные категории доступа тегов:
- Запись из разных источников
- Вставка из разных источников
- Считывание из разных источников
До того, как я начну объяснять каждую из этих категорий, очень важно понять, что несмотря на то, что браузер по умолчанию может разрешить определенный тип запросов между различными источниками, это не означает, что данный запрос будет принят сервером.
Запись из разных источников — это ссылки, переадресации и отправка форм. С активным CORS в вашем браузере все эти операции разрешены. Существует также штука под названием предварительный запрос, которая настраивает запись из разных источников. Таким образом, если некоторые записи могут быть разрешены, это не означают, что они будут выполнены на практике. Мы вернемся к этому немного позже.
Вставки из разных источников — это теги, загружаемые через <script>
, <link>
, <img>
, <video>
, <audio>
, <object>
, <embed>
, <iframe>
и т.п. Все они разрешены по умолчанию. <iframe>
выделяется на их фоне, так как он используется для загрузки другой страницы внутри фрейма. Его обрамление в зависимости от источника может регулироваться посредством использования заголовка X-Frame-options.
Что касается <img>
и других вставных тегов, то они устроены так, что сами инициируют запросы из разных источников cross-origin запроса. Именно поэтому в CORS существует различие между вставкой из разных источников и считыванием из разных источников.
Считывание из разных источников — это теги, загружаемые через вызовы AJAX/ fetch. Все они по умолчанию заблокированы вашим браузером. Существует обходной путь для вставки таких тегов на странице, но такие трюки регулируются другой политикой, которая соблюдается в современных браузерах.
Если ваш браузер обновлён, то он уже дополнен всей этой эвристикой.
Запись из разных источников
Операции записи из разных источников порой очень проблематичны. Давайте рассмотрим пример и посмотрим на CORS в деле.
Во-первых, у нас будет простой Crystal (с использованием Kemal) HTTP сервер:
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
Он просто берет запрос по ссылке /greet
с name
в теле запроса и возвращает Hello #{name}!
. Чтобы запустить это маленький Crystal сервер мы можем написать
$ crystal run server.cr
Так запускается сервер, который будет слушать localhost:4000
. Если мы откроем localhost:4000
в нашем браузере, то появится страница с тривиальным «Hello World».
Hello world!
Теперь, когда мы знаем, что наш сервер работает, давайте из консоли нашего браузера сделаем запрос POST /greet
на сервер, слушающий localhost:4000,
. Мы можем это сделать, используя fetch:
fetch(
'http://localhost:4000/greet',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Ilija'})
}
).then(resp => resp.text()).then(console.log)
Как только мы его запустили, мы увидим, что приветствие вернется из сервера:
Привет!
Это был POST запрос, но не из разных источников. Мы отправили запрос из браузера, где была отображена страница с адреса http://localhost:4000
(источник), к тому же источнику.
Теперь, давайте попробуем повторить такой же запрос но с различными источниками. Мы откроем https://google.com
и попробуем тот же запрос с той же вкладки нашего браузера:
Привет, CORS!
Мы смогли добраться до знаменитой ошибки CORS. Несмотря на то что наш Crystal сервер смог совершить запрос, наш браузер защищает нас от нас самих. Он говорит нам, что сайт, который мы открыли, хочет внести изменения на другом сайте.
В первом примере, где мы отправили запрос в http://localhost:4000/greet
из вкладки, которая отображала http://localhost:4000
, наш браузер смотрит на запрос и разрешает, так как ему кажется, что наш сайт запрашивает наш сервер (что есть отлично). Но во втором примере где наш сайт (https://google.com) хочет написать на http://localhost:4000
, тогда наш браузер отмечает этот запрос и не разрешает ему пройти.
Предварительные запросы
Если поподробнее разобраться в консоли разработчика, в частности, заглянуть во вкладку Network, то на самом деле мы увидим два запроса вместо одного, что мы отправили:
Как видно в панеле Network, отправленных запроса две штуки
Интересно заметить, то у первого запроса в HTTP фигурирует метод OPTIONS, в то время как у второго – метод POST.
Если внимательно посмотреть на запрос OPTIONS, то мы увидим, что этот запрос отправлен нашим браузером до отправления запроса POST.
Смотрим запрос OPTIONS
Интересно, что несмотря на то, что статус запроса OPTIONS был HTTP 200, он был все же отмечен красным в списке запросов. Почему?
Это предварительный запрос, который делают современные браузеры. Предварительные запросы выполняются перед запросами, которые CORS считает сложными. Признаки, свидетельствующие о сложности запроса:
- Запрос использует методы отличные от GET, POST, или HEAD
- Запрос включает заголовки отличные от Accept, Accept-Language или Content-Language
- Запрос имеет значение заголовка Content-Type отличное от application/x-www-form-urlencoded, multipart/form-data, или text/plain.
Следовательно, в примере выше, несмотря на то, что мы отправили запрос POST, браузер считает наш запрос сложным из-за заголовка Content-Type: application/json.
Если бы мы изменили наш сервер так, чтобы он обрабатывал контент text/plain (вместо JSON), мы бы могли обойтись без предварительных запросов:
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
Теперь, когда мы можем отправить наш запрос с заголовком Content-type: text/plain:
fetch(
'http://localhost:4000/greet',
{
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: 'Ilija'
}
)
.then(resp => resp.text())
.then(console.log)
Теперь, пока предварительный запрос не будет отправлен, CORS политика браузера будет постоянно блокировать запрос:
CORS стоит насмерть
Но так как мы создали запрос, который не классифицируется как сложный, наш браузер не заблокирует запрос.
Запрос прошел
Проще говоря, наш сервер неправильно настроен на принятие text/plain запросов из разных источников без какой-либо защиты и наш браузер не сможет ничего с этим поделать. Но все же он делает следующую вещь: он не показывает нашу открытую страницу/вкладку в ответ на это запрос. Следовательно, в этом случае CORS не блокирует запрос, он блокирует ответ.
CORS политика вашего браузера считает, что это фактически считывание из разных источников, так как, несмотря на то, что запрос был отправлен как POST, Content-type значение заголовка по сути приравнивает его к GET. Считывания из разных источников заблокированы по умолчанию, следовательно мы видим заблокированный запрос в нашей панели Network.
Не рекомендуется избегать предварительных запросов, то есть, действовать, как в вышеприведенном примере. Если вы ожидаете, что ваш сервер должен будет незаметно обрабатывать предварительные запросы, то на в таком случае он должен будет реализовывать конечные точки для приема запросов OPTIONS и возвращать правильные заголовки.
Выполняя запрос OPTIONS, вы должны помнить, что предварительный запрос браузера проверяет наличие трех заголовков, которые могут быть в ответе:
Access-Control-Allow-Methods
, который указывает на то, какие методы поддерживаются URL-ом ответа в контексте CORS протокола.Access-Control-Allow-Headers
, который указывает, на то, какие заголовки поддерживаются URL-ом ответа в контексте CORS протокола.Access-Control-Max-Age
, который указывает число секунд (5 по умолчанию) и это значение соответствует периоду, на который предоставляемая заголовкамиAccess-Control-Allow-Methods
иAccess-Control-Allow-Headers
информация может быть кэширована.
Давайте вернемся к предыдущему примеру, где мы отправили сложный запрос:
etch(
'http://localhost:4000/greet',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Ilija'})
}
).then(resp => resp.text()).then(console.log
Мы уже выяснили, что когда мы отправляем запрос, наш браузер будет сверяться с сервером, можно ли выполнить запрос с данными из разных источников. Чтобы обеспечить работоспособность в среде с разными источниками, мы должны сначала добавить конечную точку OPTIONS/greet к нашему серверу. В заголовке ответа новая конечная точка должна сообщить браузеру, что запрос на POST /greet с заголовком Content-type: application/json из источника https://www.google.com
может быть принят.
Мы это сделаем, используя заголовки Access-Control-Allow-*:
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
Если мы запустим сервер и отправим запрос, то
Все еще заблокирован?
Наш запрос остается заблокированным. Даже несмотря на то что наша конечная точка OPTIONS/greet в самом деле разрешила запрос, мы пока еще видим сообщение об ошибке. В нашей панели Network происходит кое-что интересное:
OPTIONS стал зеленым!
Запрос в конечную точку OPTIONS/greet прошел успешно! Однако запрос POST /greet все еще терпит неудачу. Если взглянуть на внутрь запроса POST /greet мы увидим знакомую картинку:
POST тоже стал зеленым?
На самом деле запрос удался: сервер вернул HTTP 200. Предварительный запрос заработал. Браузер совершил POST-запрос вместо того, чтобы его заблокировать. Однако ответ на запрос POST не содержит никаких CORS заголовков, так что даже несмотря на то, что браузер сделал запрос, он заблокировал любой ответ.
Чтобы разрешить браузеру обработать ответ из запроса POST /greet, нам также нужно добавить заголовок CORS к конечной точке POST:
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
Добавляя к заголовку Access-Control-Allow-Origin заголовок ответа, мы сообщаем браузеру, что вкладка с открытой https://www.google.com
имеет доступ к содержимому ответа.
Если попытаться еще разок, то
POST работает!
Мы увидим, что POST /greet получил для нас ответ без каких-либо ошибок. Если посмотреть на панели Network, то мы увидим, что оба запроса зеленые.
OPTIONS & POST в деле!
Используя надлежащие заголовки ответа в нашем конечной точке OPTIONS /greet, выполнявшей предварительный запрос, мы разблокировали конечную точку POST /greet нашего сервера, так, чтобы он имел доступ к информации из разных источников. Вдобавок, предоставляя правильный CORS заголовок ответа в ответе конечной POST /greet, мы позволили браузеру обрабатывать ответы без возникновения блокировок.
Считывание из разных источников
Как мы отмечали ранее, считывание из разных источников блокируются по умолчанию. Это делается намеренно: мы не хотели бы загружать другие ресурсы из других источников в пределах нашего источника.
Скажем, в нашем Crystal сервере есть действие GET /greet.
get "/greet" do
"Hey!"
end
Из нашей вкладки, что передала www.google.com если мы попробуем запросить эндпоинт GET /greet, то CORS нас заблокирует:
CORS блокирует
Если посмотрим поглубже в запрос, то мы найдем кое-что интересное:
На самом деле, как и прежде, наш браузер разрешил запрос: мы получили код состояния HTTP 200. Однако он не показал нашу открытую страницу/вкладку в ответ на этот запрос. Еще раз, в данном случае CORS не заблокировал запрос, он заблокировал ответ.
Так же, как и в случае с записью из разных источников, мы можем освободить CORS и обеспечить считывание из разных источников, добавляя заголовок Access-Control-Allow-Origin:
get "/greet" do |env|
env.response.headers["Access-Control-Allow-Origin"] = "https://www.google.com"
"Hey!"
end
Когда браузер получит ответ от сервера, он проверит заголовок Access-Control-Allow-Origin и исходя из его значения решит, сможет ли он позволить странице прочитать ответ. Учитывая, что в данном случае значением является https://www.google.com
, итог будет успешным:
Успешный запрос GET между разными источниками
Вот как наш браузер защищает нас от считывания из разных источников и соблюдает директивы server, сообщенные через заголовки.
Тонкая настройка CORS
Как мы уже видели в предыдущих примерах, чтобы смягчить политику CORS нашего сайта мы можем присвоить опцию Access-Control-Allow-Origin для нашего действия /greet значению https://www.google.com
:
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
Это разрешит нашему источнику https://www.google.com
запросить наш сервер, и наш браузер свободно сделает это. Имея Access-Control-Allow-Origin мы можем попробовать снова выполнить вызов fetch:
Сработало!
И это работает! С новой политикой CORS мы можем вызвать действие /greet из нашей вкладки, в которой загружена страница https://www.google.com
. Или, мы могли бы присвоить заголовку значение *
, которое сообщило бы браузеру, что сервер может быть вызван из любого источника.
Устанавливая такую конфигурацию, нужно тщательно взвесить все риски. Тем не менее, вставка заголовков с нестрогими требованиями к CORS почти всегда безопасна. Есть эмпирическое правило: если вы открываете URL в приватной вкладке и вас устраивает информация, которая там отображается, то вы можете установить разрешающую CORS политику (*
) для данного URL.
Другой способ настройки CORS на нашем сайте — это использование заголовка запроса Access-Control-Allow-Credentials
. Access-Control-Allow-Credentials
запрашивает браузер, показывать ли ответ JavaScript коду клиентской части, когда в качестве режима учетных данных запроса используется include.
Учетный режим запросов исходит из внедрения Fetch API, который в свою очередь корнями идет к объектам XMLHttpRequest:
var client = new XMLHttpRequest()
client.open("GET", "./")
client.withCredentials = true
С вводом fetch, метод withCredentials превратился в опциональный аргумент fetch запроса:
fetch("./", { credentials: "include" }).then(/* ... */)
Доступными опциями для обработки учетных данных являются omit, same-origin и include. Доступны разные режимы, так что программист может настроить отправляемый запрос, пока ответ от сервера сообщает браузеру как вести себя, когда учетные данные отправлены с запросом (через заголовок Access-Control-Allow-Credential).
Спецификация Fetch API содержит подробно расписанный и детально разобранный функционал взаимодействия CORS и Web API fetch, а также характеризует механизмы безопасности, используемые браузерами.
Несколько правильных решений
В завершении, давайте рассмотрим некоторые из рекомендуемых методов, касающихся совместного использования ресурсов между разными источниками (CORS).
Свободный доступ для всех
Как правило, это тот случай, когда у вас есть сайт с открытым контентом, не ограниченный платным доступом или сайт, требующий аутентификацию или авторизацию. Тогда вы должны установить Access-Control-Allow-Origin: * для ресурсов сайта.
Значение * хорошо подойдет в случаях, когда
- Не требуется ни аутентификация, ни авторизация
- Ресурс доступен широкой аудитории пользователей без ограничений
- Источников и клиентов, у которых будет доступ к ресурсам великое множество, и вы не знаете о них или вам все равно, кто они.
Опасные последствия от применения такой конфигурации наступают, когда контент подается на частном сервере (то есть за брандмауэрами или VPN). Когда вы подключены через VPN, у вас есть доступ к файлам в сети компании:
Сверхупрощение VPN
Теперь, когда взломщик захостит dangerous.com
, который содержит ссылку файла с VPN, то (в теории) он может создать скрипт в их сайте, который сможет иметь доступ к этому файлу:
Утечка файла
В то время как атаку такого типа сложно устроить и это требует широких знаний о конкретном VPN и файлах, хранящихся в нем, это потенциальный вектор атаки, о которым мы должны знать.
Всё в семью
Продолжая этот пример, представим, что вы хотите провести аналитику нашего сайта. Мы хотели бы, чтобы браузеры наших пользователей отправляли нам данные о том, как пользователи взаимодействуют с сайтом и о поведении наших пользователей на нашем сайте.
Проще всего это сделать, периодически отправляя данные, реализовав в браузере асинхронные запросы с помощью JavaScript. На машинном интерфейсе у нас есть простой API, который берет эти запросы из браузеров наших пользователей и хранит данные на машине для последующей обработки.
В таких случаях наш API общедоступен, но мы не хотим, чтобы какой-либо сайт прислал данные в наш аналитический API. На самом деле мы заинтересованы только в запросах, исходящих из браузеров, открывавших наш сайт, вот и все.
В данных случаях мы хотим, чтобы наш API установил заголовок Access-Control-Allow-Origin к URL нашего сайта. Это обеспечит нас тем, что браузеры никогда не отправят запросы нашему API с других страниц.
Если пользователи или другие сайты попробуют взломать данные нашего аналитического API, то набор заголовков Access-Control-Allow-Origin, установленный на нашем API, не пропустит запрос.
Null источник
Другим интересным случаем является null
источник. Это происходит, когда ресурс получает доступ от браузера, который отображает локальный файл. Например, запросы, исходящие из определенного JavaScript, работающего в статическом файле на вашем ПК, имеют заголовок Origin
со значением null
.
В таких случаях, если ваш сервер разрешает доступ к ресурсам для null
источников, то это может мешать продуктивности разработчика. Разрешение null
источников в политике CORS должно быть сделано намеренно, и только если пользователями вашего сайта/продукта пока являются только его разработчики.
Пропускай куки, если возможно
Как мы уже видели с Access-Control-Allow-Credentials
, куки не включены по умолчанию. Чтобы разрешить отправку куки с разных источников, нужно просто вернуть Access-Control-Allow-Credentials: true
. Этот заголовок сообщит браузерам, что им разрешается пересылать удостоверяющие данные (то есть куки) в запросах между разными источниками.
Разрешение куки между разными источниками – часто ненадежная практика. Вы можете подставиться под потенциальные атаки, так что включайте куки их только когда это абсолютно необходимо.
Куки между разными источниками полезнее всего в ситуациях, когда вы точно знаете какие именно клиенты будут иметь доступ к вашему серверу. Именно поэтому семантика CORS не позволяет нам установить Access-Control-Allow-Origin: *
, когда удостоверяющие данные между разными источниками разрешены.
В то время как комбинация из Access-Control-Allow-Origin: *
и Access-Control-Allow-Credentials: true
технически разрешается, она является анти-паттерном и ее следует безусловно избегать.
Если вы хотите, чтобы к вашим серверам имели доступ разные клиенты и источники, то вам стоит рассмотреть возможность создания API с аутентификацией через пароль вместо использования куков. Но если вариант с API не является оптимальным, то обеспечьте себя защитой от фальсификатора межсайтовых запросов (CSRF).
Дополнительная литература
Надеюсь, что этот длинный текст помог вам подробно разобраться в CORS, как появилась эта штука и почему она необходима. Вот ссылки, которые я использовал при написания своей статьи.
- 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
Наши серверы можно использовать для разработки и хостинга сайтов любой сложности.
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!
What is CORS?
Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any other origins (domain, scheme, or port) than its own from which a browser should permit loading of resources — MDN
This definition might seem confusing so let me try to explain it in simpler terms.
CORS is a browser policy that allows you to make requests from one website to another which by default is not allowed by another browser policy called Same Origin Policy.
This is an error that is mostly addressed from the backend of an API.
The problem here is when you are trying to call a public API without the CORS error being fixed and you can’t reach the developers that developed the API.
In this tutorial, I’ll be showing you how to by-pass CORS errors using Vanilla Javascript when you are in such a situation.
The API we are going to be using is a Quote Generator
API.
http://api.forismatic.com/api/1.0/
In other to get list of Quotes, we need to append this to the base URL
?method=getQuote&lang=en&format=json
.
So the full URL becomes;
http://api.forismatic.com/api/1.0/?method=getQuote&lang=en&format=json
In other to make the API call, we need to create a Javascript file and call the endpoint.We would be using the fetch
API.
This would look like this;
// Get quote from API
async function getQuote() {
const apiUrl = 'http://api.forismatic.com/api/1.0/?method=getQuote&lang=en&format=json';
try {
const response = await fetch(apiUrl);
const data = await response.json();
console.log({data});
} catch (error) {
console.log(error);
}
}
// On load
getQuote();
Enter fullscreen mode
Exit fullscreen mode
If you run this code in your browser and open up your console, you should see the error below;
In other to fix this error, add the following lines of code;
// Get quote from API
async function getQuote() {
const proxyUrl = 'https://cors-anywhere.herokuapp.com/' -> this line;
const apiUrl = 'http://api.forismatic.com/api/1.0/?method=getQuote&lang=en&format=json';
try {
const response = await fetch(proxyUrl + apiUrl) -> this line;
const data = await response.json();
console.log({data});
} catch (error) {
console.log(error);
}
}
// On load
getQuote();
Enter fullscreen mode
Exit fullscreen mode
This URL https://cors-anywhere.herokuapp.com/
is also a public API that was created by someone to fix the CORS error.
N.B: You might still get some errors on your console even after following the steps I just showed.If this happens, go to this URL
`https://cors-anywhere.herokuapp.com/corsdemo`
Enter fullscreen mode
Exit fullscreen mode
and follow the instructions there.
Thanks for taking your time to read this Article. Your feedback and comment is highly welcomed.
6 min read
You’ve created an API with Express and you’re busy adding some JavaScript to your front end which will make requests to it. Everything is going great until you load up the front end in your browser and you see a weird error like this in the console:
Access to fetch at 'https://your-api.com/user/1234' from origin 'https://your-website.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.
Perhaps you’ve then tried setting the request’s mode to no-cors
as the error message suggests, but the request to the API still doesn’t work. Even after a bunch of Googling, it’s hard to wrap your head around why this is happening or how to get around it.
The good news is that there’s a library for Express which you can use to help fix these CORS errors, but before we look at fixing them, what do they actually mean? In order to understand these errors, let’s take a look at what CORS is.
What is CORS and why is it ruining your day?
CORS stands for Cross-Origin Resource Sharing, and it’s something which is supported by all modern browsers. It’s a bit of a mouthful, so we’re going to break it down first, and then we can learn about what it actually does.
What is a «resource»?
A resource is the content which is available at a specific URL e.g. an HTML web page, an image, or a JSON API response. It’s effectively the «stuff» which makes up the world wide web.
What is an «origin»?
The origin for a resource is the protocol + domain + port e.g. for the URL https://your-api.com:8080/user/1234
the origin is https://your-api.com:8080
. If the URL doesn’t contain a port, then the origin will just be the protocol + domain.
What does Cross-Origin Resource Sharing actually do?
Cross-Origin Resource Sharing is the way in which a web browser ensures that the front end JavaScript of a website (origin A) can only access resources from another origin (origin B) if that origin explicitly allows it to. If it does allow it, then the resource is shared – you guessed it – cross-origin! Phew, we got there in the end.
CORS can help prevent malicious websites from accessing and using data from places that they shouldn’t be. When you see those annoying CORS errors in your browser, it’s actually your web browser doing its best to protect you from what it has identified as a potentially malicious request.
How does CORS work?
The way in which a web browser figures out whether a resource is allowed to be shared cross-origin is by setting an Origin
header on requests made by front end JavaScript. The browser then checks for CORS headers set on the resource response. The headers it will check for on the response depend on the type of request which the browser has made, but the response must at least have the Access-Control-Allow-Origin
header set to an allowed value for the web browser to make the response available to the front end JavaScript which requested it.
An example CORS request
An example of a cross-origin request would be a GET
request made with fetch from the front end JavaScript on your web page – which is hosted on one domain (origin A) – to an API endpoint which you host on a different domain (origin B).
The request made by the browser from the JavaScript on your web page at https://your-website.com/user-profile
would contain this information:
> GET /user/1234 HTTP/1.1
> Host: your-api.com
> Origin: https://your-website.com
The Origin
request header is automatically set by the web browser – for security reasons you are not able to set its value when you make the request with fetch
.
In order for the example CORS request above to work correctly, the response from your API would need to look like this:
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: https://your-website.com
< Vary: Origin
< Content-Type: application/json; charset=utf-8
<
{"name":"Existing Person"}
Notice how the value of the Access-Control-Allow-Origin
response header matches the value of the Origin
response: https://your-website.com
. The web browser will see this CORS response header and determine that it has permission to share the response content with the front end JavaScript on your web page.
Now we have a better idea of what CORS is and what it does, it’s time to set some CORS headers and fix the errors you’re getting on your web page.
How to set CORS headers and get rid of those annoying errors
As you saw in the example above, it’s important for the web browser to send the Origin
header in the request that it makes to your API, but it’s your API which needs to send the all important Access-Control-*
headers in the response. These CORS headers are what will tell the web browser whether or not it is allowed to make the response from your API available to your front end JavaScript.
The library you’re going to use to help fix the CORS errors you’ve been battling is the cors middleware package. Head to the directory containing your Express application in your terminal, and let’s get it installed:
npm install cors
Note: In this blog post I’m linking to the
cors
package on GitHub instead of npm as at the time of writing the documentation for this package is out-of-date on on npm.
Once it’s installed, you need to require it in your application (directly after you require express
is fine):
const cors = require("cors");
If you call the cors
middleware in your Express application without passing any configuration options, by default it will add the CORS response header Access-Control-Allow-Origin: *
to your API’s responses. This means that any origin — i.e. a web page on any domain — can make requests to your API. Unless you’re building an API for the general public to use, this is not the behaviour you want, so let’s jump right in to configuring the cors
middleware so that only your website can make CORS requests to your API:
/**
* These options will be used to configure the cors middleware to add
* these headers to the response:
*
* Access-Control-Allow-Origin: https://your-website.com
* Vary: Origin
*/
const corsOptions = {
origin: "https://your-website.com"
};
/**
* This configures all of the following routes to use the cors middleware
* with the options defined above.
*/
app.use(cors(corsOptions));
app.get("/user/:id", (request, response) => {
response.json({ name: "Existing Person" });
});
app.get("/country/:id", (request, response) => {
response.json({ name: "Oceania" });
});
app.get("/address/:id", (request, response) => {
response.json({ street: "Gresham Lane", city: "Lakeville" });
});
Typically you’ll want to enable CORS for all of the routes in your Express application as in the example above, but if you only want to enable CORS for specific routes you can configure the cors middleware like this:
/**
* These options will be used to configure the cors middleware to add
* these headers to the response:
*
* Access-Control-Allow-Origin: https://your-website.com
* Vary: Origin
*/
const corsOptions = {
origin: "https://your-website.com"
};
// This route is using the cors middleware with the options defined above.
app.get("/user/:id", cors(corsOptions), (request, response) => {
response.json({ name: "Existing Person" });
});
// This route is using the cors middleware with the options defined above.
app.get("/country/:id", cors(corsOptions), (request, response) => {
response.json({ name: "Oceania" });
});
/**
* We never want this API route to be requested from a browser,
* so we don't configure the route to use the cors middleware.
*/
app.get("/address/:id", (request, response) => {
response.json({ street: "Gresham Lane", city: "Lakeville" });
});
Enabling «complex» CORS requests
The examples above configure CORS for simple GET requests. For many other types of CORS requests, a CORS «preflight» request will be made by web browsers before the actual CORS request. This preflght request uses the OPTIONS
HTTP method and it helps the browser determine whether it will be allowed to make the CORS request.
The cors
middleware provides instructions for Enabling CORS Pre-Flight, and allows you to configure the headers that you want to send in the response to a preflight request.
Fear CORS no more
Hopefully this article has helped you understand what CORS is all about, but there will always be times where it’s difficult to figure out how you need to configure things for a CORS request to work. Here are some things that have helped me out along the way:
- Will it CORS? — This fantastic tool will ask you about what you want to do, and then tell you the exact CORS response headers that you need to send for the CORS request to work correctly.
- CORS HTTP headers — A handy reference which lists all of the CORS headers which you can use.
- Simple requests and Preflighted requests — The CORS documentation on the Mozilla Developer Network has great explanations of the different types of CORS requests.
- Path of an XMLHttpRequest(XHR) through CORS — This flowchart on Wikipedia is a helpful visual tool for understanding when a CORS request is considered «complex».
- Fetch standard: HTTP extensions — This documentation covers the nitty gritty details of how CORS is implemented in the browser Fetch API.
Happy cross-origin resource sharing!