Skip to content

All lessons

Area 08 // BackendLesson 131 / 14011 min read

CORS

CORS (cross-origin resource sharing) is the browser rule that decides whether a web page from one origin may read responses from an API on another origin, and your API opts in by sending headers that list exactly which origins, methods and headers it allows.

Think of it like this

Analogy

A building's reception desk (the browser) has a rule: visitors from Company A can't collect parcels addressed to Company B unless Company B has left a note saying "Company A may collect our parcels". The note is the CORS header. It protects tenants from strangers walking off with their mail, but it does nothing to stop a courier going straight to Company B's back door: that's what locks (authentication) are for.

The problem it solves

You deploy your chat frontend at https://app.example.com and your API at https://api.example.com (or, locally, localhost:3000 and localhost:8000). The first request fails in the browser with a "blocked by CORS policy" error, even though the API works perfectly from curl. The tempting fix, allowing every origin, can let any website use your users' logged-in sessions to read their data.

How it works, step by step

  1. Same-origin policy: browsers let a page freely read responses only from its own origin (scheme + host + port).
  2. Cross-origin requests are allowed to be sent, but the page can only read the response if the API says so with Access-Control-Allow-Origin matching the page's origin.
  3. Preflight: for "non-simple" requests (JSON Content-Type, an Authorization header, methods like PUT or DELETE) the browser first sends OPTIONS with Origin, Access-Control-Request-Method and Access-Control-Request-Headers; the API replies with what it allows.
  4. Credentials (cookies) need Access-Control-Allow-Credentials: true and an exact origin, never *.
  5. Configure it in one place: FastAPI's CORSMiddleware with exact allowed origins per environment (from config), the methods and headers you use.
  6. Remember what it isn't: CORS doesn't authenticate anyone or stop non-browser clients; you still need authentication and rate limiting.
  7. Alternative: serve the frontend and API from the same origin (e.g. /api behind the same domain via a proxy) and CORS isn't needed at all.

See it in code

Install
pip install fastapi httpx
Needs
Python 3.10+. TestClient sends the same headers a browser would.

A preflight from the allowed frontend vs another site, and why CORS isn't a security wall for the server.

cors.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient
 
app = FastAPI()
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],      # exact origins of YOUR frontend, not "*"
    allow_methods=["GET", "POST"],
    allow_headers=["Authorization", "Content-Type"],
    allow_credentials=True,
)
 
@app.post("/ask")
def ask():
    return {"answer": "..."}
 
c = TestClient(app)
for origin in ["https://app.example.com", "https://evil.example.net"]:
    # The browser sends this "preflight" before a cross-origin POST with JSON and an auth header.
    r = c.options("/ask", headers={"Origin": origin, "Access-Control-Request-Method": "POST",
                                   "Access-Control-Request-Headers": "authorization,content-type"})
    print(f"preflight from {origin:26} -> {r.status_code}, allow-origin: {r.headers.get('access-control-allow-origin')}")
 
r = c.post("/ask", headers={"Origin": "https://evil.example.net"})
print("actual POST from evil origin ->", r.status_code, "| allow-origin header:", r.headers.get("access-control-allow-origin"))
print("note: the server still answered; CORS only tells BROWSERS whether the page may read the response")
Output
preflight from https://app.example.com    -> 200, allow-origin: https://app.example.com
preflight from https://evil.example.net   -> 400, allow-origin: None
actual POST from evil origin -> 200 | allow-origin header: None
note: the server still answered; CORS only tells BROWSERS whether the page may read the response

The last two lines are the key lesson: the server processed the request anyway. A browser would hide the response from the evil page, but a script would read it, so authentication is what really protects the API.

A real example

Example

The wildcard that leaked chat history

A startup's API used cookie sessions and, to fix a CORS error during a demo, reflected any Origin back with credentials allowed. A security researcher showed that any website a logged-in user visited could fetch /conversations in the background and read their chat history. The fix was an explicit allow-list (https://app.<their-domain> in production, localhost ports in development, loaded from config), SameSite cookies, and moving the frontend and API under one domain so most requests weren't cross-origin at all.

The easy way

Start here

Add CORSMiddleware with the exact origin of your frontend (and your local dev URL in development), the methods and headers you actually use, and nothing more. Or serve the frontend and API from the same domain and skip CORS.

When you grow: the production setup

Teams keep allowed origins per environment in config, avoid credentialed cross-origin requests where possible (bearer tokens or same-site setups), set SameSite and Secure cookies, cache preflights with max-age, test CORS in CI, and never treat CORS as an access-control mechanism.

Trade-offs

What you gain

  • Protects users' browsers from malicious sites reading their data.
  • Simple to configure in one middleware.
  • Lets frontends and APIs live on separate domains.

What it costs you

  • Confusing errors that only appear in browsers.
  • Preflights add a round trip.
  • Misconfiguration can silently open data to any site.

When not to use it

  • APIs called only server-to-server don't need CORS headers at all.

Common mistakes

  • allow_origins=["*"] (or reflecting any origin) with credentials.
  • Thinking CORS protects the API from scripts or bots.
  • Forgetting the dev origin's port is part of the origin.
  • Adding CORS headers in several places (proxy and app) that conflict.
  • Allowing every method and header "to make it work".

Interview questions

Try answering out loud first, then open the model answer.

What is CORS and why does it exist?

Model answer

A browser mechanism that relaxes the same-origin policy in a controlled way: a page from one origin may read responses from another origin only if that server opts in with Access-Control-Allow-* headers. It exists to stop malicious sites from using a user's browser (and their cookies) to read data from other sites.

What triggers a preflight request?

Model answer

Cross-origin requests that aren't "simple": methods other than GET, HEAD or POST; custom headers such as Authorization; or a Content-Type other than form or plain text (e.g. application/json). The browser sends OPTIONS first and only sends the real request if the response allows it.

Does a correct CORS setup secure your API?

Model answer

No. CORS is enforced by browsers only; any script, server or tool can call your API regardless. Security comes from authentication, authorization, rate limiting and input validation; CORS just prevents other websites from reading responses in your users' browsers.

Quiz

Pick an answer to see whether it’s right and why.

  1. 01What does CORS control?
  2. 02Are http://localhost:3000 and http://localhost:8000 the same origin?
  3. 03What is a preflight request?
  4. 04Does CORS protect your API from curl or a Python script?
  5. 05Why not use allow_origins=["*"]?

0 of 5 answered

Key terms

CORS (cross-origin resource sharing)
The browser mechanism that lets a page from one origin read responses from another origin only if that server allows it with special headers.
Origin
The combination of scheme, host and port of a web page or API, such as https://app.example.com. Different ports mean different origins.
Preflight request
An OPTIONS request a browser sends before certain cross-origin requests to ask the server whether the real request is allowed.
Same-origin policy
The browser rule that pages can freely read responses only from their own origin; CORS relaxes it in a controlled way.
HTTP header
A name and value sent with a request or response to carry metadata, such as Authorization, Content-Type or Retry-After.

Before and after

Progress is saved in this browser only.