82 lines
4.3 KiB
Markdown
82 lines
4.3 KiB
Markdown
|
|
# 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.
|