skip to content

Placing CSRF Defence

Cookie credentials are attached by the browser automatically, so every cookie-authenticated endpoint needs a CSRF check or a stated reason it does not. Interviewers ask which honestly need none.

on this pageshow

questions

2

Your appointment desk authenticates front-desk staff with a session cookie — which endpoints need a CSRF check, and where does it run?

level: middleimportance: must knowfreq 70%

answer

  1. arrival is not intent
  2. the browser attaches it, not the user
  3. audit the route table row by row
  4. one choke point, exemptions named
  5. safe methods only if truly read-only

basics

~20 s

Every state-changing endpoint an automatically-attached session cookie can reach needs an anti-forgery check or a written reason it does not. Run it at one choke point in front of the handlers, so protection is the default and exemptions are named.

solid answer

~50 s

The audit turns on one fact: the browser attaches the session cookie by itself, so a request arriving authenticated says nothing about whether the signed-in staff member intended it. Walk the route table and give every row a verdict. Any state-changing method on a route the cookie can reach needs an anti-forgery check. `GET`, `HEAD`, `OPTIONS` and `TRACE` are exempt only while those handlers are genuinely read-only — that is an assertion about your own routing rather than something the method name guarantees, and RFC 9110 Section 9.2.1 puts the duty to disable an action under a safe method on the resource owner. Routes no browser-attached credential reaches are exempt with a stated reason. Run the check once in the pipeline in front of every handler, so a route added next quarter is protected by default; a check written inside individual handlers fails silently the day someone forgets one.

code

pseudocode · 17 lines
pseudocode
# Startup: 'we exempt safe methods' is a claim about YOUR handlers. Prove it once.
for route in route_table:
    if route.method in ("GET", "HEAD", "OPTIONS", "TRACE") and route.mutates_state:
        refuse_to_start("safe method selects an action: " + route.path)

# Routes no user agent attaches a credential to. Each one rejects the session cookie.
EXEMPT = {"/receivers/lab-results", "/registry/microchips"}

# Per request: one choke point, in front of every handler.
def on_request(req):
    if req.path in EXEMPT:
        return next(req)
    if req.method in ("GET", "HEAD", "OPTIONS", "TRACE"):
        return next(req)          # sound only because startup proved these are read-only
    if not anti_forgery_value_valid(req):
        return reject(403)        # caller is identified; intent is not evidenced
    return next(req)

go deeper

for a junior

Remember the one fact everything follows from: the browser sends the session cookie by itself, so an endpoint cannot read 'this arrived authenticated' as 'the user asked for this'.

for a middle

Be able to walk a route table out loud, giving each row a verdict and a reason, and to say why the check belongs in the pipeline in front of the handlers rather than inside them.

for a senior

Show that you treat the exemption list as an artefact that decays: a test enumerating routes, a startup check that no safe-method handler mutates state, and a re-audit whenever a route moves.

for a principal

The tradeoff worth arguing is default-protect against default-exempt across a service with several kinds of caller: default-protect costs you a value handoff for client-rendered front ends, default-exempt costs one silent hole per forgotten route.

## Why an authenticated request proves nothing about intent A session cookie sits in the browser's cookie jar and is attached by the **user agent** to any request matching the cookie's host and path — whatever page emitted that request, and whether or not the person at the keyboard asked for it. The credential travels because of where the request is **going**, not because of where it came **from**. On a cookie-authenticated service that pulls apart two facts which handlers routinely treat as one: - **This request is authenticated.** True, and the server can act on it. - **The signed-in staff member intended this request.** Unproven, and nothing else in the request supplies it. An anti-forgery check is the machinery that supplies the second fact. The placement question is therefore a question about **which endpoints an automatically-attached credential can reach**. Nothing in the standards decides this for you. RFC 9110 defines HTTP's own challenge-response authentication framework — the `WWW-Authenticate` and `Authorization` pair — and treats cookie-carried authentication as a custom mechanism outside it. There is no specification to point at when a reviewer asks why this route carries a check and that one does not. The reason has to be yours, and it has to be written down. ## The audit is a table, not an instinct Walk the route table and give every row two columns: a verdict and a reason. The reason is never blank, **including for the exemptions** — an exemption with no stated reason is indistinguishable from an oversight six months later. | Route's credential | Who attaches it | Method | Verdict | |---|---|---|---| | Session cookie | the user agent, automatically | state-changing | check required | | Session cookie | the user agent, automatically | safe | exempt, if the handler is genuinely read-only | | Signature over the request body | the calling program, deliberately | any | exempt — no ambient credential | | Machine secret set by the client | the calling program, deliberately | any | exempt — no ambient credential | Two things make this an audit rather than a habit. First, the practice cannot answer it globally: front-desk staff in a browser exist, so the check cannot be off everywhere, and the laboratory's result receiver exists, so it cannot be on everywhere either. Second, the table is a claim about the service **as it is today**, so it has to be re-derived when routes move. ## Where the check runs Put it at one choke point in the request pipeline, in front of every handler, and express exemptions as an explicit list of routes: 1. **The default becomes protected.** A route added next quarter is covered before anyone remembers to think about it. 2. **Every exemption is enumerable.** You can print the list, review it in a change, and test it. 3. **The failure mode becomes loud.** A missing exemption breaks a working caller immediately; a missing check breaks nothing and is found later by someone else. A check written inside individual handlers inverts all three. The default is unprotected, the exemptions are invisible because they are absences rather than entries, and the day someone adds a handler and forgets the call nothing reports it. That is the whole argument for placement, and it holds whatever pipeline the service happens to be built on. ## The safe-method exemption is a claim about your own handlers `GET`, `HEAD`, `OPTIONS` and `TRACE` are exempt because they are supposed to be read-only, and a read-only handler has no state for a forged request to change. But safety is a property the **resource owner** upholds, not one the method name enforces. RFC 9110 Section 9.2.1 is explicit: where a parameter in the target URI selects an action, the resource owner MUST disable that action when it is reached with a safe request method. So *"we exempt GET"* is an assertion about your routing, and it is the assertion most likely to be false in an application that grew. Prove it rather than repeat it: enumerate routes at startup and refuse to start if a safe-method route is flagged as mutating, and treat a request to put a state-changing action behind a safe method as a change to the audit rather than a routing detail. ## Getting a token to a client-rendered front end When the desk's front end renders in the browser rather than on the server there is no server-rendered form to embed the value in, so the handoff has to be deliberate. The server issues the anti-forgery value into the document or into a bootstrap response the client reads when it starts; the client holds it for the life of the page and sends it on every state-changing call; and the client refetches it whenever the session identifier changes, because the value is bound to a session and at login it is a different session. A front end that caches it across a sign-in starts failing every write, and the cause is the rotation rather than the check. ## What this decision does not settle The audit says which endpoints need a check and where it runs. Which scheme supplies the evidence, what a cookie's own attributes do about cross-site sending, and how a particular pipeline behaves when a value is missing or stale are separate questions with separate owners — and none of their answers change a row in the table above.

  • The desk's front end is rendered in the browser and calls the same endpoints — how does it get an anti-forgery value to send?
    There is no server-rendered form to embed it in, so the server issues it into the document or a bootstrap response the client reads at startup, and the client sends it on every state-changing call. It must be refetched when the session identifier changes at login: the value is bound to a session, and a cached one starts failing every write afterwards.
  • Your session cookie already carries a cross-site sending attribute such as SameSite — does that let you drop the check on any endpoint?
    No. That attribute is a property of the credential, enforced on the client side, not a property of the endpoint, so it never converts a 'check required' row into an exemption. The audit still asks whether an automatically-attached credential can reach that handler. What the attribute does and where it falls short is cookie-attribute material with its own owner.
  • Why does a check written inside each state-changing handler fail in a way a pipeline check does not?
    Because the default inverts. In front of the handlers, a new route is protected until someone exempts it, and the exemptions are a list you can review. Inside handlers, a new route is unprotected until someone remembers the call, and the omission is an absence rather than an entry — so nothing reports it and nothing breaks until it is found from outside.

The practice's back-office door unlocks for any staff fob within range of the reader, so it opens just as readily when someone leans against it walking past. The lock proves a fob was nearby; it never proves the holder meant to open that door. The anti-forgery check is the deliberate press that supplies the missing fact.

saying these in an interview costs you the question

  • Says the check is unnecessary because the endpoint already requires a logged-in user.
  • Exempts every GET without checking whether any GET handler changes state.
  • Turns the check off service-wide because one endpoint could not carry a value.
  • Treats the anti-forgery check as authorizing the action rather than evidencing intent.
  • Adds a check to read-only endpoints hoping to stop cross-origin reading of responses.
  • Checks a few handlers by hand and calls the route audit finished.
open as a page

An external laboratory posts signed result notifications to your practice's receiver endpoint — why does it need no CSRF check, and what keeps that exemption safe?

level: seniorimportance: should knowfreq 55%

basics

~20 s

A receiver endpoint authenticated by a signature over the request body needs no anti-forgery check because no user agent attaches that credential by itself, so a hostile page cannot make an authenticated request arrive. Scope the exemption to that path.

open as a page