CORS and preflight request
CORS (Cross-Origin Resource Sharing) — is a security standard that allows clients (browsers) to use resources (URLs) from other origins (domains). It was created to improve SOP (same-origin policy). This policy is used by browsers to restrict access to resources (URLs) located on other domains (which they do not control).
Examples of CORS errors (which can be seen in the browser console):
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.
💡 Example
Imagine you are creating a web service where users can save their posts online. Your site (for example, myapp.com) sends data to an external server (for example, noteapi.com). For this, you use the POST method.
Example of JavaScript code that sends a post:
const apiUrl = "https://noteapi.com/api/notes"; const userToken = "authorization_token"; const payload = { title: "Post of the day", content: "Today I learned how to send data to another server" }; fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${userToken}` }, body: JSON.stringify(payload) });
How does the browser make the request?
-
Preflight Request: Since
Content-Type = application/json
, before sending the main POST request, the browser automatically makes a preflight request using theOPTIONS
method to the server noteapi.com. This is needed to find out if such a request is allowed. -
Headers in Preflight: The browser sends headers like:
Access-Control-Request-Method: POST Access-Control-Request-Headers: Content-Type, Authorization Origin: https://myapp.com
-
Server Response to Preflight: If the server is configured correctly, it responds that the request with the specified headers and method is allowed from the origin https://myapp.com.
- Main Request: Only after a positive response to the preflight request does the browser send the main POST request.
⚠️ Cross-origin request
In simple terms - it is a request to another domain.
For example, when we embed an image on our site - the <img>
tag, in the src
attribute of which we specify a link to an image from another site, the browser will ultimately make a Cross-origin request. But we usually do not experience problems with the <img>
tag because the policy for this tag is specific.
For a request to be considered cross-origin, not only the domain can differ, but also the "scheme" (for example, https) or "port" (for example, 80).
For example: http://example.com
and https://example.com
- are different origins because the first uses the "http" scheme, while the second uses "https". Also, there are different ports: for http — 80, and for https — 443. Thus, these origins differ by two parameters: scheme and port.
The path to a specific resource does not matter. For example, http://example.com/api/post/25
and http://example.com/some-page
will be considered the same origin.
Popular tags that can make cross-origin requests:
<img>
<script>
- if the script executed inside makes a request, it must comply with CORS.<frame>
<video>
<audio>
<iframe>
- may be restricted by SOP. See X-Frame-Options<link>
<form>
🔒 What is SOP (same-origin policy)?
SOP (same-origin policy) is a browser security feature that restricts access to web application resources. It requires that the resource be from the same origin as the web application that is requesting the resource.
SOP was introduced in Netscape Navigator 2.02 in 1995.
All modern browsers follow this policy to some extent. The principles are described in RFC6454.
For two origins to be considered the same, they must have the same host
, scheme
, and port
. For example, consider the URL https://blog.postman.com:3000
:
- host - blog.postman.com
- scheme - https
- port - 3000 - usually excluded and equals 80 (http) or 443 (https).
SOP is more flexible for the following types of resources:
-
Images: Web pages can embed images from any origin without restrictions.
-
CSS styles: External CSS can be loaded onto a web page using the
<link>
tag directly in HTML or@import
rules in another stylesheet. -
Scripts: JavaScript files can be loaded and executed from any origin, but they can only access and modify DOM elements from the origin where they were loaded. They are subject to SOP restrictions and cannot make cross-origin calls to external APIs.
-
Media files: Audio and video files can be embedded and played from various sources using the
<audio>
or<video>
tags. - iframe: Embedded
<iframe>
can load content from any origin, but SOP isolates the content inside the iframe from the parent page's content. An iframe can communicate with the parent page using the interface provided by JavaScript, but it does not have direct access to the parent page's resources because they are not from the same origin.
🔓 What is CORS (cross-origin resource sharing)?
CORS (Cross-Origin Resource Sharing) is a mechanism that allows a server to grant access to its resources from other domains through special HTTP headers, bypassing SOP restrictions.
When a cross-origin request is made, the browser automatically applies the CORS policy. If the server to which the request is made does not allow the resource to be used, the browser blocks it.
SOP (Same-Origin Policy) by default restricts access to resources between different origins on the client side. CORS (Cross-Origin Resource Sharing) allows opening a resource explicitly by indicating, through HTTP headers, who can access their resources. Thus, CORS allows bypassing the restrictions imposed by SOP.
‼️ Preflight requests (предварительный запрос)
A preflight request is a special request sent before the main request to the server. It uses the OPTIONS
method and contains headers that inform the server about the type of upcoming request.
If the preflight request fails, the browser will not make the main request and instead will display a CORS error.
Important headers of the preflight request and server response
Headers in the preflight request:
-
Access-Control-Request-Method
: informs the server which HTTP method will be used in the main request. -
Access-Control-Request-Headers
: lists the headers that will be used in the main request. Origin
: specifies the domain from which the main request will be made.
Headers in the response to the preflight request:
-
Access-Control-Allow-Origin
: indicates which domains can receive responses. -
Access-Control-Allow-Methods
: lists the methods allowed for cross-origin requests. -
Access-Control-Allow-Headers
: specifies which headers are allowed in the request. Access-Control-Max-Age
: indicates how long the result of the preflight request can be cached.
When is a preflight request made?
The browser sends a "preflight request" to the server for any action that modifies data. This is necessary because requests that modify data are not considered safe, so the browser first ensures that the server allows CORS before making the actual request.
A preflight request is made if any of these conditions are met:
-
Method is not "simple":
Not GET, HEAD, POST. For example:DELETE
andPUT
. -
POST with "non-simple" Content-Type:
Any other than:- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
-
There are custom headers:
For example:- Authorization
- X-Requested-With
- Content-Type: application/json
- Any X-*
-
The response requires credentials (withCredentials: true in JS)
And the server does not specify Access-Control-Allow-Credentials: true. -
The request contains custom headers or methods not included in the standard.
- The request uses fetch() with options affecting CORS
For example, mode: 'cors' + custom header or method.
Caching preflight responses
Caching preflight responses can improve performance. If the server specifies in the response to the preflight request the header Access-Control-Max-Age
, the browser can cache the permission to perform requests for the specified period. Thus, for repeated requests to the same server, the preflight request will not be made.
❗️ Simple requests (основной запрос)
These are CORS requests that are sent as part of the main HTTP request. For a simple request to be sent to the server, the following criteria must be met:
-
The request must use one of the following HTTP methods:
GET
,HEAD
orPOST
. -
The request headers must be basic, which are usually automatically set by the client. Simple headers include:
Accept
Accept-Language
Content-Language
Content-Type
- If the request uses the
POST
method, theContent-Type
header must have one of the values:application/x-www-form-urlencoded
multipart/form-data
text/plain
🟢 CORS Response Headers
The server uses response headers to provide information about CORS to the browser. A response to a CORS request typically contains the following headers:
-
Access-Control-Allow-Origin
- informs the browser which origins can access the resource. It can contain the name of the origin itself (for example,https://postman.com
), or a wildcard can be used -*
. -
Access-Control-Allow-Credentials
- is a boolean value that indicates whether the browser should include credentials (cookies or HTTP authentication credentials) when making a cross-origin request.- If
false
is set in the response to the preflight request, then credentials should not be included in the actual request. - If a simple request is made with this header set to false, then the browser will not include these data in the main request.
- If
-
Access-Control-Allow-Methods
- used in the response to the preflight request to specify the allowed request methods in the main request. Example:GET, POST, OPTIONS
-
Access-Control-Allow-Headers
- used in the response to the preflight request to specify the allowed headers in the main request. Example:DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range
-
Access-Control-Expose-Headers
- indicates which headers can be accessible to the browser. Any header not specified here will not be accessible to client-side JavaScript code. Example:Content-Length,Content-Range
Access-Control-Max-Age
- indicates the maximum time (in seconds) that the response to the preflight request can be cached.
A successful response to a CORS request may include any HTTP status code, as long as it contains one or more of the headers mentioned above. However, a successful response to a CORS preflight request must indicate an OK status (i.e., 200 or 204).
🛡 Advantages of working with CORS
CORS enhances the overall security of the application while improving flexibility and interoperability. Key benefits:
-
Enhanced API security: CORS protects against unauthorized requests to sensitive resources on another domain. By relaxing the same-origin policy (SOP) and requiring explicit permission through CORS headers, it reduces the risk of cross-site request forgery (CSRF) attacks and unauthorized access to data.
-
Cross-origin authentication: CORS ensures secure authentication by controlling the transmission of credentials (such as cookies or authentication tokens) in cross-origin requests. This is important when using Single Sign-On (SSO) technology and other authentication mechanisms.
-
Standardization: CORS is a standardized mechanism supported by all major web browsers. This unified implementation across all browsers provides a consistent and reliable approach to handling cross-origin requests.
- API integration: CORS is essential for integrating web applications with external APIs, allowing developers to leverage the functionality of third-party services within their own services.
👍 Best practices for implementing CORS
While CORS was designed to safely modify the same-origin policy, it still needs to be implemented correctly. Improper implementation of CORS can compromise data security. Therefore, it is important to follow industry best practices such as:
-
Avoid cache poisoning: If the server's
Access-Control-Max-Age
header is set for a long duration, outdated permissions may be used for subsequent requests, leading to potential security issues.Ensure that
Access-Control-Max-Age
is set for a moderate period (the default and recommended duration is five seconds). -
Be explicit: It is not recommended to use wildcard symbols
*
. Instead, explicitly specify the origins that are allowed to access the server's resources using theAccess-Control-Allow-Origin
header. Also, explicitly specify the allowed methods and headers usingAccess-Control-Allow-Methods
andAccess-Control-Allow-Headers
.Use wildcard symbols only when you are sure that the API should be accessible from any origin.
-
Add important CORS headers: Ensure that the server includes all necessary headers in the response. For example, the absence of the
Access-Control-Allow-Credentials
header may cause issues if the browser does not send credentials when needed. If it is set to an incorrect value, the browser may send credentials to an unwanted origin. -
Document everything: It is important to properly document CORS policies and make them understandable for developers who may integrate your APIs into their applications. This practice fosters greater trust from users of your API.
- Credential verification: By default, CORS ignores credentials. If you need to include credentials (such as cookies or HTTP authentication) in cross-origin requests, set
Access-Control-Allow-Credentials
totrue
. However, you should also ensure that the server verifies and handles credentials securely.