Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~16 min· topic 6 of 7

Topic 7.6

CORS, HSTS & Browser Security Headers

In one line

Browsers enforce the same-origin policy: a page from one origin (scheme + host + port) can't read responses from another unless that server allows it with CORS headers like Access-Control-Allow-Origin. Some requests first send an OPTIONS 'preflight'. CORS is enforced by the browser, not the server, so it protects users, not APIs. Other response headers harden the browser too: HSTS forces HTTPS, CSP limits where scripts load from, and X-Content-Type-Options, frame-ancestors, and Referrer-Policy close common gaps.

0/7 · 0%

Think of it like this

A school that only lets a parent collect a child if the child's own class teacher has written that parent's name on an approved list. The rule is enforced at the school gate (the browser), not by the parent (the server being asked).

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Origin
The scheme, host, and port of a URL, the unit browsers use for isolation.
Same-origin policy
The browser rule that scripts can only read responses from their own origin by default.
CORS
Cross-Origin Resource Sharing: headers that let a server allow specific other origins to read its responses.
Preflight
An OPTIONS request the browser sends first to ask whether a cross-origin request is allowed.
HSTS
HTTP Strict Transport Security: tells browsers to use only HTTPS for a site for a set time.
CSP
Content Security Policy: a header restricting where a page may load scripts and other resources from.

Step by step

01The console error

Tiffin moves its API to https://api.tiffin.in, while the website stays at https://tiffin.in. The menu loads in curl, but in the browser Meera sees an empty page and the console shows a CORS error.

browser consolewhole filetext
Access to fetch at 'https://api.tiffin.in/menu' from origin 'https://tiffin.in'
has been blocked by CORS policy: Response to preflight request doesn't pass
access control check: No 'Access-Control-Allow-Origin' header is present on
the requested resource.
The console errordiagram
Rendering diagram…

02Testing the preflight by hand

Arjun simulates the browser's preflight with curl, sending the same headers. Before the fix, the API returns no CORS headers. After adding an allow-list, the preflight answers correctly.

src/cors.jswhole filejavascript
const ALLOWED = new Set(["https://tiffin.in", "https://admin.tiffin.in"]);

export function cors(req, res, next) {
  const origin = req.headers.origin;
  if (ALLOWED.has(origin)) {                       // allow-list, never "reflect anything"
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Vary", "Origin");                 // caches must keep per-origin copies
    res.setHeader("Access-Control-Allow-Credentials", "true");
  }
  if (req.method === "OPTIONS") {
    res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
    res.setHeader("Access-Control-Allow-Headers", "authorization, content-type");
    res.setHeader("Access-Control-Max-Age", "600");  // browser caches the preflight 10 min
    return res.status(204).end();
  }
  next();
}
terminal
$ curl -si -X OPTIONS https://api.tiffin.in/menu \
-H 'Origin: https://tiffin.in' \
-H 'Access-Control-Request-Method: GET' \
-H 'Access-Control-Request-Headers: authorization' | grep -i '^access-control\|^HTTP'
── expected output ──
HTTP/2 204
access-control-allow-origin: https://tiffin.in
access-control-allow-methods: GET, POST, PUT, DELETE
access-control-allow-headers: authorization, content-type
access-control-allow-credentials: true
access-control-max-age: 600

03Hardening headers at the edge

Priya adds security headers in Nginx so every response gets them. HSTS starts with a short max-age and grows once everything works over HTTPS. The CSP allows scripts only from Tiffin itself and the payment provider's checkout script.

/etc/nginx/snippets/security-headers.confwhole filenginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://checkout.razorpay.com; frame-ancestors 'none'; base-uri 'self'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
terminal
$ curl -sI https://tiffin.in | grep -iE 'strict-transport|content-security|x-content-type|referrer-policy'
── expected output ──
strict-transport-security: max-age=31536000; includeSubDomains
content-security-policy: default-src 'self'; script-src 'self' https://checkout.razorpay.com; frame-ancestors 'none'; base-uri 'self'
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Reflecting any Origin with credentials

To silence CORS errors quickly, the API echoes back whatever Origin header arrives and sets Allow-Credentials: true.

terminal
$ curl -si https://api.tiffin.in/me -H 'Origin: https://evil.example' | grep -i '^access-control'
── what you'll see ──
access-control-allow-origin: https://evil.example
access-control-allow-credentials: true
Any website a logged-in customer visits can now read their profile and orders through their browser.

Myth vs fact

Myth

CORS protects my API from other servers.

Fact

Only browsers enforce CORS. Scripts, curl, and servers ignore it. Protect APIs with authentication and authorization.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Roll out CSP with Content-Security-Policy-Report-Only first, collecting violation reports, then switch to enforcing. Before submitting a domain to the HSTS preload list, be sure every subdomain supports HTTPS, because removal takes months.

Remember this

  1. 1

    An origin is scheme + host + port: https://tiffin.in and https://api.tiffin.in are different origins, and so are http:// vs https:// and :3000 vs :443.

  2. 2

    By default, JavaScript on https://tiffin.in can *send* a request to https://api.tiffin.in but can't *read* the response unless the API replies with Access-Control-Allow-Origin: https://tiffin.in.

  3. 3

    Preflight: for non-simple requests (methods like PUT/DELETE, JSON content type, custom headers like Authorization), the browser first sends OPTIONS with Access-Control-Request-Method/Headers. The server must answer with matching Allow-* headers, or the real request is never sent.

  4. 4

    Credentials (cookies) need Access-Control-Allow-Credentials: true *and* a specific origin. * isn't allowed with credentials. Never reflect any incoming Origin back blindly.

  5. 5

    CORS errors appear only in the browser console. curl ignores CORS completely, which is why 'it works with curl' doesn't mean the browser will work, and why CORS isn't access control for the API.

  6. 6

    Hardening headers: Strict-Transport-Security (HSTS: always use HTTPS), Content-Security-Policy (allowed script, style, and frame sources), X-Content-Type-Options: nosniff, frame-ancestors in CSP (clickjacking protection), Referrer-Policy.

Explain it without notes

01

Why does the API work in curl but fail in the browser?

02

What does HSTS protect against?

Practice

01

Check which security headers a site sends.

02

Write the CORS headers for an API used by https://tiffin.in with cookies.

Trade-offs

  • ↔

    Strict CSP and HSTS raise security but can break third-party widgets or HTTP-only subdomains, so test in report-only mode first. Long preflight caching reduces extra requests but delays CORS changes.

Done when you can

  • I can explain origins and the same-origin policy.

  • I configure CORS with an allow-list and handle preflights.

  • I set HSTS, CSP, and nosniff headers.