cors-demo/README.md

82 lines
4.3 KiB
Markdown
Raw Permalink Normal View History

2026-10-01 13:02:44 -03:00
# CORS classroom
A live playground for teaching CORS. One zero-dependency Node file starts three
origins; the page fires real cross-origin requests and shows the browser's verdict
next to the server's own log of what it actually received.
```bash
node server.js # or: npm start
open http://localhost:3000
```
Then, in this order:
1. Open DevTools → Network and leave it visible. Every "blocked" response is still
there, which is the point of the first scenario.
2. Click **Set cookies on API** once, so the credentials scenarios have cookies to send.
3. Work down the six groups, or hit **Run all** and talk through the results.
## What's running
| Origin | Role |
| --- | --- |
| `http://localhost:3000` | the page. Also serves `/same-origin/api` and the log stream — both same-origin, so neither is affected by CORS. |
| `http://localhost:3001` | the API. A different **origin** from the page, but the same **site**, so cookies still travel there. |
| `http://127.0.0.1:3001` | the same listener reached as a different **site**, which is how the SameSite demo works. |
The API builds its CORS headers from query parameters on every request, so each
scenario reconfigures the server as it runs. Nothing needs restarting, and you can
hand-edit any URL live in front of the class.
| Param | Effect |
| --- | --- |
| `acao=*` / `echo` / literal | `Access-Control-Allow-Origin` (omit the param to send no header at all) |
| `acac=true` | `Access-Control-Allow-Credentials` |
| `acam=` / `acah=` | `Allow-Methods` / `Allow-Headers`; `echo` reflects what the preflight asked for |
| `aceh=` | `Access-Control-Expose-Headers` |
| `acma=60` | `Access-Control-Max-Age` (preflight cache) |
| `vary=1` | adds `Vary: Origin` |
| `pfstatus=401` | status code for the OPTIONS preflight |
| `pfcors=0` | answer the preflight with no CORS headers |
| `pfacao=0` / `pfacac=0` | omit that header on the preflight only |
| `pfredirect=1` | make the preflight respond 302 |
| `status=500` | status of the actual response |
| `errnocors=1` | drop CORS headers when status ≥ 400 — the classic middleware-order bug |
| `delay=400` | artificial latency, to show what a preflight costs |
## The six groups
1. **The basic rule** — CORS only applies cross-origin, and it gates reading the
response, not sending the request. Includes the ways `Allow-Origin` is commonly
written wrong: a comma-separated list, a trailing slash, unconditional reflection.
2. **Preflight mechanics** — what makes a request simple, what triggers OPTIONS,
why a failed preflight means the real request never reaches your server, and
`Max-Age`. The `Max-Age` card is worth running twice: the second run has no OPTIONS.
3. **Credentials and wildcards** — the `*`-with-credentials rule, `Allow-Credentials`
on the preflight as well as the response, and a cross-site scenario where CORS
passes but the cookie still isn't sent, to separate CORS from SameSite.
4. **Reading the response** — a successful request still hides every response header
outside the seven-header safelist, and `Expose-Headers: *` silently stops working
once credentials are involved.
5. **How this breaks in real life** — a 500 that reports itself as a CORS error, auth
middleware answering the preflight with 401, `mode: 'no-cors'` and what it really
does, and a cross-origin form POST that succeeds — the CORS-is-not-CSRF-protection
point.
6. **Free playground** — every knob as a form, with both preview panels updating as
you type, for when someone asks "but what if…".
## Files
- `server.js` — the whole backend, ~330 lines of plain Node, written to be read out loud.
- `public/scenarios.js` — the scenario catalogue as data: config, expectation, and the
teaching note for each. Add your own here.
- `public/app.js` — the runner: predicts whether a request will be preflighted, fires it,
and renders the browser verdict against the server log.
- `public/_selftest.html` — not linked from the page. Open
`http://localhost:3000/_selftest.html` to run every scenario and check that the
browser still agrees with the expectation written in the catalogue. Useful after
you edit scenarios, and for confirming behaviour in a different browser.
All 37 fetch scenarios were verified against Chrome: each one behaves exactly as its
note describes.