A clinic's cancel-appointment endpoint acts on GET and the attacker never sees the response — why is the forgery still complete?
answer
- the damage is done server-side
- write-only, so reads do not matter
- safe is a contract, not enforcement
- GET, HEAD, OPTIONS, TRACE
- method change narrows reach only
basics
~20 sThe cancellation happens when the server handles the request, so reading the response adds nothing for the attacker. Safety in RFC 9110 is a contract about what a client may assume, not enforcement — an implementation is free to change state on GET, and this one has.
solid answer
~40 sTwo separate points meet here. First, the attack is **write-only**: the state change is complete at the moment the server processes the request, so the fact that the hostile page can never read the clinic's response costs the attacker nothing. "The browser stops cross-site reads" is therefore the wrong answer to a forgery question. Second, RFC 9110 defines `GET`, `HEAD`, `OPTIONS` and `TRACE` as **safe** methods, but safety is a statement about what the client is requesting and may be held responsible for — nothing prevents an implementation from attaching side effects, and this endpoint has. The practical cost is that a state-changing GET is reachable from a subresource load, which needs no interaction at all; moving it to an unsafe method removes that path and nothing more.
code
http · 5 linesGET /appointments/8411/cancel HTTP/1.1
Host: booking.vetclinic.example
Accept: */*
Referer: https://pet-deals.example/
Cookie: session=8f2c1ab94d7ego deeper
Remember that the change happens on the server when the request arrives, so an attacker who never sees the answer has still succeeded.
Explain that safety is a semantic contract over GET, HEAD, OPTIONS and TRACE rather than something the protocol enforces, and name the extra reach a state-changing GET hands an attacker.
Show you can separate a confidentiality argument from an integrity one under pressure, and that you know a method change reduces reach without defending the endpoint.
Frame broken method semantics as an estate-wide correctness problem: every caching, prefetching and retrying intermediary was built on that contract, so breaking it costs you far more than one forgeable endpoint.
## Write-only is the whole point The most common wrong answer in this area is some version of "but the attacker can't read anything back". It is true and it is beside the point. The appointment is cancelled inside the clinic's own request handling: the row is updated, the confirmation email is queued, the slot is released. All of that has happened by the time the response exists. Whether the response reaches the hostile page, or is discarded by the browser as a failed image decode, changes nothing about the damage. This is why read-oriented reasoning misses the attack entirely. A candidate who says "the browser blocks cross-origin reads, so we're fine" has answered a confidentiality question when the question was about integrity. The two useful framings to carry: - **Reads need the response; writes do not.** An attack that only needs a write is unaffected by anything that governs reading. - **The browser's read restriction is why forgery exists as a distinct problem.** If hostile pages could simply read your data, nobody would bother forging writes. ## What "safe" actually promises RFC 9110 designates `GET`, `HEAD`, `OPTIONS` and `TRACE` as safe methods. The designation says that a client issuing one of them is requesting a read-only operation and should not be held accountable for any side effect that results. It is a statement about **intent and responsibility**, addressed to clients and to the semantics of the method — not a capability the protocol enforces on servers. Nothing in the protocol stack inspects your handler. If a route mapped to GET deletes a row, it deletes the row. The safe/unsafe split is a contract the application may simply have broken, and when it is broken every intermediary that assumed it — anything that prefetches, revalidates, retries or crawls — becomes a way to trigger the side effect, quite apart from any attacker. ## What changes between a GET endpoint and a POST endpoint | | Cancel on GET | Cancel on POST | |---|---|---| | Reachable from an image or frame source | Yes, on page load | No | | Needs interaction from the victim | No | Usually one click, or a hidden frame | | Reachable from a cross-site form | Yes | Yes | | Triggered accidentally by a prefetch or crawler | Possible | No | | Protected by the method change alone | — | **No** | That last row is the one people get wrong. Moving a state change from GET to POST is worth doing — it closes the zero-interaction path and it restores the contract intermediaries rely on — but it is a reduction in reach, not a defence. A form on a hostile page emits a POST just as happily, carrying the same session cookie. ## How this shows up in production State-changing GETs are rarely designed; they accumulate: 1. A convenience link — "cancel" as an anchor in a reminder email, because an anchor is a GET and a form is not. 2. An administrative shortcut — a URL that toggles a flag, pasted into a runbook. 3. A legacy client that could only issue GET and never got revisited. Each one is discoverable from the clinic's own pages, its emails or its documentation. And because a GET's parameters live in the URL, the same endpoints are the ones that end up in browser history, in referrer information and in access logs, which is a separate problem with the same root cause. ## Safe is not the same as idempotent Candidates blur the two and interviewers listen for it. **Safe** means the client is requesting no state change and is not accountable for one. **Idempotent** means that issuing the same request several times leaves the resource in the same state as issuing it once. RFC 9110 makes every safe method idempotent, but the reverse does not hold: a deletion or a full replacement changes state, so it is not safe, while repeating it changes nothing further, so it is idempotent. The cancel-on-GET endpoint here is idempotent — the slot frees only once — and still completely unsafe. ## The answer an interviewer wants Two sentences, in this order. The forgery is complete because the write happened at the server and the attacker never needed the response — read restrictions are irrelevant to a write-only attack. And the method's safety designation is a semantic contract about what a client is asking for, which this endpoint has broken; the protocol never enforced it, so the GET both changes state and is reachable from markup that requires nothing at all from the victim.
- If read restrictions are irrelevant here, when do they matter at all?When the attacker's goal is the data rather than the change — reading the owner's pet records, an account balance or a token out of a response. Those attacks need the response body, so what the browser will and will not hand to a script is decisive. Forgery deliberately targets the case where it is not.
- Does moving every state change off GET fix the problem?No. It closes the paths that need no interaction — an image or frame source loading on page open — and it restores the assumption intermediaries make. A cross-site form still emits a POST with the session cookie attached, so the endpoint still needs a real provenance check.
- Why do intermediaries make a state-changing GET worse independently of any attacker?Because the safe designation is what they were built on. Anything that speculatively fetches, retries or revalidates a URL assumes doing so is free of consequence. A GET that cancels an appointment can therefore be triggered by a prefetch, a link checker or a crawler, with no hostile party present at all.
saying these in an interview costs you the question
- Says the attack fails because the hostile page cannot read the response
- Believes the protocol prevents a GET from changing server state
- Treats switching the endpoint to POST as a complete defence
- Confuses a safe method with an idempotent one
- Thinks a request that produces no visible page did not reach the server