CORS
Table of Contents
Why CORS Exists
When your browser loads a web page, that page can run JavaScript. That JavaScript can send HTTP requests, for example with fetch or XMLHttpRequest.
Without any protection, any website could:
- Call any API where you are logged in.
- Read the response data.
- Perform actions as you.
Browsers implement a same origin policy to reduce this risk.
An origin is:
scheme(protocol) +host(domain) +port
For example:
| URL | Origin |
|---|---|
https://example.com/index.html | https://example.com |
https://api.example.com/users | https://api.example.com |
http://example.com:8080/hello | http://example.com:8080 |
JavaScript in a page from https://example.com is normally not allowed to read responses from https://api.example.com or http://example.com:8080.
But in modern web development, frontend and backend often live on different origins:
- Frontend during development:
http://localhost:3000 - Backend API:
http://localhost:8000
We need a controlled way to say:
"This backend allows that origin to access it."
This mechanism is CORS, Cross Origin Resource Sharing.
CORS is a browser security feature. The server participates by sending special HTTP headers. Other clients, such as Python scripts, Postman, or curl, completely ignore CORS. They can call your API without any CORS issues.
CORS is enforced only by browsers. It is a client-side security restriction that the server can relax in a controlled way by sending CORS headers.
Basic CORS Flow
Imagine this situation:
- Frontend origin:
http://localhost:3000 - Backend API:
http://localhost:8000
Frontend code:
fetch('http://localhost:8000/api/items')
.then(r => r.json())
.then(console.log);From the browser's view, this is a cross origin request. The browser:
- Adds an
Originheader to the request:
Origin: http://localhost:3000- Sends the request to
http://localhost:8000. - Waits for the server response.
- Checks the server's CORS headers.
- If allowed, the browser gives the JavaScript access to the response data.
- If not allowed, the browser blocks JavaScript from reading the response and shows a CORS error in the console.
The server decides whether to allow this by returning headers such as:
Access-Control-Allow-Origin: http://localhost:3000
If the Access-Control-Allow-Origin value matches the Origin of the request, the browser allows access. If not, it blocks it.
Important: The request is usually still received by the server. CORS only affects whether the browser exposes the response to JavaScript.
Important CORS Headers
Request Headers
When a browser makes a cross origin request, it adds at least:
Origin- Sometimes other CORS related headers for preflight, which we will cover later.
Example:
GET /api/items HTTP/1.1
Host: localhost:8000
Origin: http://localhost:3000Response Headers
The server responds with CORS headers to indicate what is allowed.
Access-Control-Allow-Origin
This is the core header.
Examples:
Access-Control-Allow-Origin: http://localhost:3000or
Access-Control-Allow-Origin: *Rules:
- Must either be:
- A single origin value, for example
https://myfrontend.com, or *to allow every origin.- You cannot put multiple origins separated by commas. If you want to support multiple specific origins, you must decide per request which single origin to return.
Rule: Access-Control-Allow-Origin must be either exactly one origin or *. Never try Access-Control-Allow-Origin: https://a.com, https://b.com.
Access-Control-Allow-Credentials
Controls whether the browser is allowed to send credentials such as:
- Cookies
- HTTP auth
- TLS client certificates
Example:
Access-Control-Allow-Credentials: trueImportant restrictions:
- If you allow credentials, you cannot use
Access-Control-Allow-Origin: *. - You must use a specific origin value.
Rule: If Access-Control-Allow-Credentials: true then you must not use * for Access-Control-Allow-Origin. Use a specific origin instead.
Access-Control-Expose-Headers
By default, JavaScript can only read a limited set of response headers in cross origin responses.
To allow access to custom headers, you specify them:
Access-Control-Expose-Headers: X-Total-Count, X-Request-IdThen frontend code can do:
const count = response.headers.get('X-Total-Count');This is useful for pagination metadata, request IDs, or other custom information.
Simple vs Preflight Requests
Browsers do not treat all cross origin requests the same. There are two categories:
- Simple requests
- Preflighted requests
Simple Requests
A cross origin request is "simple" if it satisfies strict conditions:
- Method is one of:
GETHEADPOST- Request headers are only a limited set of "simple headers":
AcceptAccept-LanguageContent-LanguageContent-Type, but only with:text/plainmultipart/form-dataapplication/x-www-form-urlencoded- No custom headers like:
X-Auth-TokenX-Requested-WithX-Custom
For simple requests, the browser directly sends the request, then checks the CORS headers on the response.
Example of a simple GET:
GET /api/items HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.comServer response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Content-Type: application/json
[{"id": 1, "name": "Item 1"}]If the origin matches, the browser gives the response to JavaScript.
Preflight Requests
If the request does not meet the "simple" rules, the browser sends an extra request before the real one. This is the preflight request.
The preflight uses the OPTIONS method and asks the server:
"If I later send a request like this, will you allow it?"
The server must reply and say which methods and headers are allowed.
When preflight is used
Preflight is triggered when you use:
- HTTP methods:
PUTPATCHDELETEOPTIONSwith custom requests- Custom headers:
AuthorizationX-Auth-Token- Any non simple header
- Non simple
Content-Type, for example: application/json
Example: Preflight Sequence
Frontend code:
fetch('https://api.example.com/items/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer abc123'
},
body: JSON.stringify({ name: 'New name' })
});Browser sends a preflight request:
OPTIONS /items/123 HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, AuthorizationServer responds:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
If the browser accepts this response, it then sends the actual PUT request.
Actual request:
PUT /items/123 HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Content-Type: application/json
Authorization: Bearer abc123
{"name": "New name"}The server should respond with CORS headers again.
Preflight Response Headers
Important headers in the preflight response:
Access-Control-Allow-Origin: as beforeAccess-Control-Allow-Methods: which methods are allowed
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers: which custom headers can be used
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-IdAccess-Control-Max-Age: how long (in seconds) the browser can cache this preflight result
Access-Control-Max-Age: 3600
Caching preflight responses reduces the number of extra OPTIONS requests.
Rule: For preflight to succeed, the server must include:
Access-Control-Allow-Originwith a matching origin or*(if no credentials), andAccess-Control-Allow-Methodsthat includes the requested method, andAccess-Control-Allow-Headersthat includes all requested headers.
CORS and Credentials
Many APIs require authentication or rely on cookies for sessions.
From the browser side, there are two separate questions:
- Is the browser allowed to send credentials with a cross origin request?
- Is JavaScript allowed to read the response when credentials are involved?
To send credentials:
- The frontend must set
credentialsinfetch. - The server must allow credentials.
Example frontend with cookies:
fetch('https://api.example.com/profile', {
credentials: 'include', // send cookies
});Server response:
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: trueIf anything is wrong, the browser will block the response.
Table of common fetch credentials options:
| Value | Meaning |
|---|---|
omit | Never send cookies or auth info |
same-origin | Send credentials only when origin is the same |
include | Always send credentials, even for cross origin calls |
Important combinations:
- If the response has
Access-Control-Allow-Origin: *andAccess-Control-Allow-Credentials: true, the browser will treat it as invalid. - For credentialed CORS, you must use a specific origin value.
Rule: For CORS requests with cookies or auth:
- Frontend:
credentials: 'include' - Server:
Access-Control-Allow-Origin: <exact frontend origin>
andAccess-Control-Allow-Credentials: true
Common CORS Misconfigurations
Allowing Everything in Production
It is tempting to "fix" CORS errors by adding:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: trueThis can silently disable important protections. Some of these combinations are invalid anyway.
Typical problems:
- Any website can make requests to your API from the user's browser.
- If you do not use cookies or rely only on token in header, other sites might still be able to trigger actions if users paste tokens or if there are other flows.
- You may think you are protected, but you are not.
Better approach:
- In development, you can use
*without credentials for convenience. - In production, always whitelist specific origins.
Example safe configuration for production:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: trueForgetting CORS on Error Responses
Some frameworks only add CORS headers for successful responses.
In that case, you see logs that show the server responded with 401 or 500, but the browser displays a CORS error, not the real error.
You must ensure:
- CORS headers are present on all responses, including errors handled by exception handlers.
For example, in a middleware that sets CORS headers on every response.
Not Handling Preflight Requests
Some backends:
- Reject
OPTIONSrequests with 404, 405, or similar. - Only handle GET, POST, etc.
For non simple cross origin requests, the browser will never even send the main request if preflight fails.
Solution:
- Add explicit handling for
OPTIONSrequests, or - Use a CORS middleware that auto responds to preflight.
Mismatched Origin Values
Common mistakes:
- Extra trailing slash:
- Correct:
https://app.example.com - Wrong:
https://app.example.com/ - Using
httpinstead ofhttpsin configuration. - Forgetting specific ports in development:
- Frontend:
http://localhost:3000 - Backend returns:
http://localhost(does not match)
Browsers compare origin exactly: scheme, host, and port must match.
CORS in Development vs Production
In development, typical setup:
http://localhost:3000React frontendhttp://localhost:8000FastAPI backend
You can be more permissive here, for example:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: *As long as you do not use cookies or credentialed sessions.
In production, typical setup:
- API at
https://api.example.com - Web app at
https://app.example.com
Better configuration:
- Allow only
https://app.example.com. - Allow credentials if needed.
- Limit methods and headers to what is actually used.
Example:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-Id
Access-Control-Max-Age: 3600It is also common to support more than one frontend, for example:
- Main app:
https://app.example.com - Admin app:
https://admin.example.com
The server then must:
- Inspect the
Originheader. - Check if it belongs to an allowed list.
- Echo back the same origin in
Access-Control-Allow-Origin.
Pseudo code logic:
ALLOWED_ORIGINS = {
"https://app.example.com",
"https://admin.example.com",
}
origin = request.headers.get("Origin")
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Vary"] = "Origin"
Vary: Origin tells caches that the response may change depending on the Origin request header.
CORS and Security Perspective
CORS does not protect your API from malicious scripts using curl or Postman.
CORS is about one question:
"Can JavaScript in a browser page from origin A read responses from origin B?"
From the backend security perspective:
- CORS controls who can read responses in a browser.
- Authentication and authorization control who can perform actions, regardless of CORS.
You still need:
- Authentication, tokens, sessions.
- Proper authorization checks.
- CSRF protection when you use cookies.
- Input validation.
But CORS is still important, because:
- Without CORS restrictions, your API could be easily used by any website as a free backend, especially if you do not require authentication.
- With cookie based login and weak CORS, a malicious website could act on behalf of logged in users if you also lack CSRF protection.
Useful rule of thumb:
Rule: CORS is not a substitute for authentication or CSRF protection. Treat CORS as an additional browser level safety belt, not as the main lock on your API.
Practical CORS Examples
Example 1: Public Read Only API
You have a public API for reading blog posts:
- No authentication.
- Safe methods only:
GET.
Goal:
- Allow all websites to fetch public posts.
- Do not worry about cookies.
Configuration example:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Since you do not accept credentials, * is acceptable here.
Example 2: Single Page App with Login Token in Header
Setup:
- SPA at
https://app.example.com - API at
https://api.example.com - Auth token is sent in
Authorizationheader:Bearer <token>
This triggers preflight for requests with Authorization.
You want:
- Only
https://app.example.comcan access your API from browser JavaScript. - No cookies, only tokens in headers.
Configuration example:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
No Access-Control-Allow-Credentials, since you do not use cookies.
Example 3: Cookie Based Session
Setup:
- Frontend:
https://app.example.com - Backend:
https://api.example.com - Login sets a cookie like
sessionid=...; Secure; HttpOnly; SameSite=None.
SameSite=None is required to allow cookies to be sent in cross site contexts. That also requires Secure.
In the frontend, you must always use:
fetch('https://api.example.com/profile', {
credentials: 'include',
});Server CORS headers:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-TypeYou must also implement CSRF protection, but that belongs to other security topics in the course.
Summary
- Browsers enforce same origin policy for security.
- CORS is a protocol that allows servers to explicitly relax this policy for selected origins.
- Key headers:
Originin requests.Access-Control-Allow-Originin responses.- Additional headers like
Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Access-Control-Max-Age. - Simple requests skip preflight. Others trigger a preflight
OPTIONSrequest. - For credentialed CORS:
- Use
credentials: 'include'on the frontend. - Return a specific origin and
Access-Control-Allow-Credentials: trueon the backend. - Do not rely on CORS as your only security control. It only affects browser JavaScript access, not direct HTTP clients.
Understanding CORS helps you:
- Fix common "CORS error" messages in browsers.
- Configure your backend safely so that only trusted frontends can use it from browsers.
- Keep development flexible while keeping production secure.
Views: 8
KAHIBARO