DevOpsIntermediate 13 min Lesson 11 of 15

CORS

Understand what CORS actually protects, why preflight requests happen, and how to configure it without opening everything.

DevOps · Lesson 11 of 15
0/15 done(0%)

What is it? #

CORS is a browser rule. A page on one origin cannot read a response from a different origin unless that server explicitly allows it.

It is not a server security feature. It protects users from one site reading another site's data using their credentials, and it does nothing against a direct request from curl or a server.

That distinction matters, because "I disabled CORS to fix it" usually means allowing any origin to read authenticated responses — which is the thing CORS exists to prevent.

The rules are unintuitive largely because of preflight requests: for anything beyond a simple request, the browser asks permission before sending.

Think of it like this #

A receptionist who will pass a message to a visitor only if the department has listed that visitor's company in advance.

The rule protects the department's clients, not the building. Someone phoning directly from outside is not affected at all.

Simple example #

A frontend on app.example.com calls an API on api.example.com. The browser blocks it until the API responds with headers naming that origin as allowed.

Code #

TEXT
Simple request — sent immediately, response blocked if not allowed

  GET /orders
  Origin: https://app.example.com

  Access-Control-Allow-Origin: https://app.example.com
  → the browser lets the page read the response


Preflight — an OPTIONS request first, before the real one

  OPTIONS /orders
  Origin: https://app.example.com
  Access-Control-Request-Method: POST
  Access-Control-Request-Headers: content-type, authorization

  Access-Control-Allow-Origin: https://app.example.com
  Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  Access-Control-Allow-Headers: Content-Type, Authorization
  Access-Control-Max-Age: 86400
  → only then is the POST sent
TEXT
What triggers a preflight

any method other than GET, HEAD or POST
Content-Type other than form-encoded, multipart or text/plain
  → application/json always triggers one
any custom header, including Authorization

Most real API calls trigger a preflight. Access-Control-Max-Age caches
the permission so it is not repeated for every request.
PYTHON
# Correct configuration: specific origins, not a wildcard
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://app.example.com",
        "https://admin.example.com",
    ],
    allow_credentials=True,              # cookies or Authorization headers
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Content-Type", "Authorization"],
    max_age=86400,
)

# NEVER: allow_origins=["*"] with allow_credentials=True
# The specification forbids it, and browsers reject the combination —
# because it would let any site read authenticated responses.
TEXT
Reading the error

"No 'Access-Control-Allow-Origin' header is present"
    the server did not allow this origin — or returned an error before
    the CORS middleware ran

"The value of 'Access-Control-Allow-Origin' must not be '*' when
 credentials mode is 'include'"
    exactly the unsafe combination described above

"Method PUT is not allowed by Access-Control-Allow-Methods"
    the preflight response did not list that method

"Request header authorization is not allowed"
    the preflight response did not list that header
TEXT
CORS is not a substitute for authorisation

Allowing an origin lets a browser page READ the response. It says
nothing about who may access the data. A public API with permissive
CORS and no authentication is open to everyone either way.

Authorisation is enforced server-side, per request, regardless of origin.

How it works #

The browser attaches an Origin header to cross-origin requests and inspects the response headers before letting the page read it. The request usually reaches the server either way — CORS controls reading the response, not sending the request.

That is why a cross-origin POST can have side effects even when the response is blocked, and why CSRF protection is a separate concern.

Preflight exists so that a request with side effects is not sent at all unless the server has agreed to the method and headers. The browser asks first with OPTIONS.

Since application/json and Authorization both trigger preflight, essentially every authenticated API call performs one. Access-Control-Max-Age caches the answer, avoiding a doubled request count.

The wildcard-with-credentials prohibition is the important rule. Allowing any origin while permitting credentials would let any website make authenticated requests on a logged-in user's behalf and read the results. Browsers reject the combination outright.

CORS errors originating from a server error are a common confusion: if the application throws before the CORS middleware adds headers, the browser reports a CORS failure while the real problem is a 500.

The final point is the one most worth internalising. CORS is a browser-enforced rule about reading responses, and it is not authorisation.

Real-world use #

Nearly every separated frontend and backend hits CORS on the first day. The correct fix is listing the frontend origins; the common wrong fix is a wildcard.

Credentials mode matters. Cookie-based sessions across origins need allow_credentials plus SameSite cookie settings that permit cross-site sending, which is increasingly restricted by browsers.

Behind a CDN, Vary: Origin is necessary when the allowed origin varies, or a cached response for one origin can be served to another.

Development often uses a proxy so the frontend and backend share an origin, which removes CORS locally and means it is discovered in staging instead.

The reliable configuration is a short explicit list of origins, the methods and headers actually used, and credentials enabled only when needed.

Common mistakes #

  • Using a wildcard origin with credentials, which browsers reject and which would be unsafe.
  • Treating CORS as authorisation rather than as a browser reading rule.
  • Not handling OPTIONS, so every preflight fails.
  • Missing Vary: Origin behind a cache, leaking one origin’s response to another.
  • Diagnosing a CORS error when the real problem is a server error before the middleware.

Practice #

Configure CORS for an API with two specific frontend origins, credentials enabled, and only the methods and headers actually used. Then make a cross-origin JSON request and observe the preflight in the browser network tab, including how Max-Age affects repeat requests.

Quick quiz

  1. 1. What does CORS actually protect?

  2. 2. What triggers a preflight request?

  3. 3. Why is a wildcard origin with credentials forbidden?

  4. 4. Does CORS prevent a cross-origin request from reaching the server?

  5. 5. What does Access-Control-Max-Age do?

Summary

  • CORS is a browser rule about reading cross-origin responses.
  • Preflight requests happen for anything beyond simple GETs — including JSON.
  • List specific origins; never combine a wildcard with credentials.
  • Cache preflight results with Max-Age and set Vary: Origin behind caches.
  • CORS is not authorisation — enforce that server-side regardless of origin.