KAHIBARO
Discord Login Register

CORS

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:

Browsers implement a same origin policy to reduce this risk.

An origin is:

For example:

URLOrigin
https://example.com/index.htmlhttps://example.com
https://api.example.com/usershttps://api.example.com
http://example.com:8080/hellohttp://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:

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 code:

js
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:

  1. Adds an Origin header to the request:
http
   Origin: http://localhost:3000
  1. Sends the request to http://localhost:8000.
  2. Waits for the server response.
  3. Checks the server's CORS headers.
  4. If allowed, the browser gives the JavaScript access to the response data.
  5. 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:

http
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:

Example:

http
GET /api/items HTTP/1.1
Host: localhost:8000
Origin: http://localhost:3000

Response Headers

The server responds with CORS headers to indicate what is allowed.

Access-Control-Allow-Origin

This is the core header.

Examples:

http
Access-Control-Allow-Origin: http://localhost:3000

or

http
Access-Control-Allow-Origin: *

Rules:

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:

Example:

http
Access-Control-Allow-Credentials: true

Important restrictions:

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:

http
Access-Control-Expose-Headers: X-Total-Count, X-Request-Id

Then frontend code can do:

js
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

A cross origin request is "simple" if it satisfies strict conditions:

  1. Method is one of:
    • GET
    • HEAD
    • POST
  2. Request headers are only a limited set of "simple headers":
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type, but only with:
      • text/plain
      • multipart/form-data
      • application/x-www-form-urlencoded
  3. No custom headers like:
    • X-Auth-Token
    • X-Requested-With
    • X-Custom

For simple requests, the browser directly sends the request, then checks the CORS headers on the response.

Example of a simple GET:

http
GET /api/items HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com

Server response:

http
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:

Example: Preflight Sequence

Frontend code:

js
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:

http
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, Authorization

Server responds:

http
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:

http
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:

http
  Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
http
  Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-Id
http
  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-Origin with a matching origin or * (if no credentials), and
  • Access-Control-Allow-Methods that includes the requested method, and
  • Access-Control-Allow-Headers that 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:

  1. Is the browser allowed to send credentials with a cross origin request?
  2. Is JavaScript allowed to read the response when credentials are involved?

To send credentials:

Example frontend with cookies:

js
fetch('https://api.example.com/profile', {
  credentials: 'include', // send cookies
});

Server response:

http
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Credentials: true

If anything is wrong, the browser will block the response.

Table of common fetch credentials options:

ValueMeaning
omitNever send cookies or auth info
same-originSend credentials only when origin is the same
includeAlways send credentials, even for cross origin calls

Important combinations:

Rule: For CORS requests with cookies or auth:

  • Frontend: credentials: 'include'
  • Server: Access-Control-Allow-Origin: <exact frontend origin>
    and Access-Control-Allow-Credentials: true

Common CORS Misconfigurations

Allowing Everything in Production

It is tempting to "fix" CORS errors by adding:

http
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true

This can silently disable important protections. Some of these combinations are invalid anyway.

Typical problems:

Better approach:

Example safe configuration for production:

http
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: true

Forgetting 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:

For example, in a middleware that sets CORS headers on every response.

Not Handling Preflight Requests

Some backends:

For non simple cross origin requests, the browser will never even send the main request if preflight fails.

Solution:

Mismatched Origin Values

Common mistakes:

Browsers compare origin exactly: scheme, host, and port must match.

CORS in Development vs Production

In development, typical setup:

You can be more permissive here, for example:

http
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:

Better configuration:

Example:

http
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: 3600

It is also common to support more than one frontend, for example:

The server then must:

Pseudo code logic:

python
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:

You still need:

But CORS is still important, because:

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:

Goal:

Configuration example:

http
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:

This triggers preflight for requests with Authorization.

You want:

Configuration example:

http
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:

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:

js
fetch('https://api.example.com/profile', {
  credentials: 'include',
});

Server CORS headers:

http
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

You must also implement CSRF protection, but that belongs to other security topics in the course.

Summary

Understanding CORS helps you:

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!