skip to content

A booking form's server-issued anti-forgery token can ride in a hidden field or an author-set request header - what does each carriage assume?

level: middleimportance: should knowfreq 55%

answer

  1. carriage, not the check itself
  2. a form can emit any body
  3. only code sets an author header
  4. the header's presence is itself evidence
  5. never in the query string

basics

~20 s

A hidden field assumes the server rendered the form that carries it; an author-set header assumes your own code composed the request. The second is stronger, because a document from elsewhere can emit a request body but cannot set an author-defined header on it.

solid answer

~50 s

Both carriages deliver the same value to the same check - submitted against stored - so the difference is in what each one presupposes. A **hidden field** rides in the request body, which means the server must render the value into every form, and it works with no script at all. An **author-defined header field** must be set by code composing the request, so it cannot be used by a plain form submission. The second buys something extra: a document your site never served can emit a form POST with any body it likes, but it cannot attach an author-defined header to a request aimed at your site - the browser would first have to ask your server's permission, and your server does not grant it. So the header's mere presence is already evidence about who composed the request, independently of the value it carries.

code

http · 7 lines
http
POST /bookings/confirm HTTP/1.1
Host: hall.example.org
Cookie: session=Q1s7uT4kR0
Content-Type: application/x-www-form-urlencoded
Content-Length: 73

csrf_token=8f4a1c3e9b2d7f60a5c81e4b93d2f7a1&slotId=hall-a-2026-05-04-1900

go deeper

for a junior

Know the two places the value can ride - a hidden input in the form, or a header field set by code - and that the server checks the same stored value either way.

for a middle

Explain the asymmetry: a document from another host can emit a request body of its choosing but cannot attach an author-defined header field to a request aimed at your site. That is why the header carriage is evidence in itself.

for a senior

Bring up the operational consequences: no value in URLs, pages carrying a session-bound value must not be stored by a shared cache, and supporting both carriages behind one check is the normal arrangement.

for a principal

Weigh the coupling each carriage creates. The hidden field ties the defence to server-rendered markup; the header ties it to every unsafe request being issued by code that already holds the value. Decide which constraint your delivery model can actually honour.

## Two carriages, one check The check never changes: read the value the request supplied, look up the value stored against the session, compare them in constant time, refuse on a mismatch. What changes is **where the request carries the value**, and there are two places worth using. - **A hidden input in the form body.** The server writes the value into the markup when it renders the page, and the browser submits it along with the rest of the fields. - **An author-defined request header field.** Code composing the request reads the value from somewhere in the page and sets a header field on the request it issues. The field name is the application's own choice; nothing about it is registered or standard. ## What a document from another site can emit This is the asymmetry the header carriage exploits. A hostile document, with no script at all, can cause a form submission to your site. It controls the method, the action, every field name and every field value, and it can use any of the three encodings a form element is able to produce: `application/x-www-form-urlencoded`, `multipart/form-data` or `text/plain`. What it cannot control is the *value* of your session-bound token, because it cannot read your page. Header fields are different in kind. A form submission, an image load, a frame load - the things a hostile document can emit without script - carry only the header fields the **user agent** itself decides to attach. There is no markup that adds an author-defined one. Script can ask for one, but when script on another origin asks to add an author-defined header field to a request aimed at your site, the browser first checks with your server in a separate exchange, and your server has no reason to permit it. That gives the header carriage a second, independent property: **a request that merely arrives carrying the header at all was composed by code the browser was willing to let compose it.** The value still does the real work, but the carriage corroborates it. ## What each carriage assumes | | hidden field in the body | author-defined request header | |---|---|---| | who puts it there | the server, when it renders the page | code in the page, when it composes the request | | works with no script | yes - this is the whole point of it | no | | what a cross-site document can produce | the field name and a guessed value | not the header field at all | | what the server reads it from | the parsed request body | a header field on the request | | extra evidence beyond the value | none | presence of the header is itself provenance | A server-rendered booking flow - pick a room, pick a slot, confirm - is the hidden field's natural home, because every step is a form the server rendered anyway. A step submitted by code that composed the request itself is the header's natural home, because that code can set a header and a form cannot. ## Failure modes worth naming - **Putting the value in the query string.** It is neither of these two carriages. A URL travels into access logs, into browsing history, into anything a user copies and sends to a colleague, and it survives long after the session does. - **Treating the header name as the security property.** Any name works. What matters is that a header field had to be set by code at all; a clever name adds nothing, and a predictable one costs nothing. - **Letting a page that carries the value be stored by a shared cache.** The markup contains a value bound to one session; a cache that hands the same bytes to a different user has handed out that session's value. - **Assuming the header carriage removes the need for stored state.** It does not. The check is still submitted-versus-stored. Replacing the stored copy with a value echoed back out of a cookie is a different scheme with different assumptions, not a variation on this one. ## The judgment Prefer the hidden field where the server renders the form, because it depends on nothing but markup the server already emits. Prefer the header where code composes the request anyway, because the carriage itself tightens the check. Supporting both at once is normal: the server looks in the header first, falls back to the body field, and compares whichever it found against the same stored value.

  • Could the value be carried in the URL query string instead?
    It works and it is a bad idea. A URL is written into access logs, kept in browsing history, and pasted into messages by users who think they are sharing a link. A session-bound secret should travel in the request body or in a header field, both of which stay out of the URL and out of the log line.
  • Does header carriage let the server stop keeping a stored copy of the value?
    No. The check remains submitted-versus-stored, and the stored copy is what binds the value to the session. Carrying the value in a header changes only how the request delivers it. A scheme that drops the server-side copy and compares the submission against a value echoed out of a cookie is a different design resting on different assumptions.
  • Does the header field name need to be unguessable?
    No. The evidence is that an author-defined header field had to be set at all, which a document your site never served cannot do. Knowing the name gains an attacker nothing, because they still cannot attach it and still cannot read the value. Pick a clear name and document it.

saying these in an interview costs you the question

  • Thinks a cross-site page cannot produce a POST body at all
  • Believes an unguessable header name is what provides the protection
  • Says header fields are protected in transit while bodies are not
  • Puts the session-bound value in the query string for convenience
  • Assumes header carriage removes the need for server-side stored state