skip to content

In PHP, would you keep one CSRF token per session in $_SESSION or one per form, and why regenerate the token after login?

level: seniorimportance: should knowfreq 32%

answer

  1. one $_SESSION value reused all session
  2. per-form: a map keyed by form id
  3. per-form tokens unset once used
  4. regenerate token value on privilege change
  5. a pre-login token may be attacker-known

basics

~20 s

A single $_SESSION['csrf'] reused for the session is simplest and multi-tab friendly; per-form tokens are a $_SESSION map keyed by form and unset after use, but break the back button. Regenerate the token value at login, since one issued to a pre-auth session may be attacker-known.

solid answer

~40 s

Two PHP storage shapes. **Per-session:** one `$_SESSION['csrf']` generated once and reused for every form — simple, and it survives multiple tabs and the back button. **Per-form (or per-request):** a **map** like `$_SESSION['csrf'][$formId] = $token`, each token **`unset()`** after its check so it cannot be replayed; this narrows the leak window but **breaks the back button and multiple tabs**, and what that buys is owned by the CSRF synchronizer-token topic. Both designs need **rotation after login**: regenerate the token *value* (`$_SESSION['csrf'] = bin2hex(random_bytes(32));`) when privileges change, because a token issued to the pre-authentication session may have been observed by an attacker who set that session up. Regenerating the session **ID** at login is a related but separate concern owned by the sessions leaf.

go deeper

for a junior

Know that a token lives in $_SESSION and should be regenerated at login.

for a middle

Explain per-session vs per-form storage shapes and why a pre-auth token must be rotated on authentication.

for a senior

Weigh the multi-tab/back-button cost of per-request tokens, cap per-form maps, and pair token rotation with session-ID regeneration at login.

for a principal

Set the token-lifecycle policy for sensitive flows like a bank change-email and reason about the UX/security trade of rotation frequency.

## Two storage shapes in `$_SESSION` Because PHP sessions are server-side, both designs store the trustworthy token in `$_SESSION`; they differ in *how many* tokens and *how long* each lives. **Per-session token** — one value for the whole session: ```php if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } ``` Every form echoes the same token; every POST checks against it. Simple, and it *just works* with multiple tabs, the back button, and re-submitted forms because the token does not change under the user. **Per-form / per-request token** — a map, each entry consumed on use: ```php $formId = 'change_email'; $_SESSION['csrf'][$formId] = bin2hex(random_bytes(32)); // when rendering // on POST: $ok = isset($_SESSION['csrf'][$formId]) && hash_equals($_SESSION['csrf'][$formId], $_POST['csrf'] ?? ''); unset($_SESSION['csrf'][$formId]); // one-time use ``` ## Choosing between them | Aspect | Per-session | Per-form / per-request | |---|---|---| | Storage | one `$_SESSION['csrf']` | a `$_SESSION['csrf']` **array** keyed by form/request | | Reuse | reused all session | consumed (`unset`) after one check | | Multiple tabs / back button | works | **breaks** (token already consumed elsewhere) | | Leak window | whole session | one submission | | Complexity | minimal | must track, expire, and cap the map size | A subtle PHP-specific trap with per-form tokens: the map can **grow unbounded** if the user opens many forms without submitting, so you must cap or expire entries. For most apps the per-session token is the right default; reserve per-form for genuinely sensitive, single-shot actions. *What* per-request rotation actually defends against, beyond the leak window, is owned by the CSRF synchronizer-token topic. ## Why regenerate after login Both designs share one rule: **change the token value when the trust level changes**, above all at login. The reason is that a token minted for the *anonymous* session may not be trustworthy after authentication: 1. An attacker sets up a session (or fixes its identifier) and learns the CSRF token issued to it. 2. The victim logs in on that same session. 3. If the token value carried over unchanged, the attacker already knows the authenticated session's CSRF token and can forge requests. Regenerating the token at login closes that: ```php // on successful authentication: $_SESSION['csrf'] = bin2hex(random_bytes(32)); ``` For a bank's change-email form you would treat the sensitive action similarly — re-issue (and often re-authenticate) so a stale token cannot be replayed. ## The related-but-separate session-ID rotation At login you should **also** regenerate the *session identifier* to defeat session fixation. That is a distinct mechanism (owned by the request-handling/sessions leaf) and operates on the cookie/ID, not the CSRF token value. The two often happen together in a login handler, but they solve different problems: session-ID rotation stops an attacker reusing a known session ID; CSRF-token rotation stops them reusing a known token. ## Pitfalls - Generating the token but never rotating it at login, leaving a pre-auth token in force. - Per-form maps that grow without bound, or that `unset` the token *before* the check runs. - Assuming per-request rotation is strictly better — it is not free; it costs the back button and multi-tab flows, which for many apps is the wrong trade.

  • Why does a per-request token break the back button?
    Because the token is `unset` from `$_SESSION` once it is checked, so it is single-use. If the user navigates back to a cached form and resubmits, or has the same form open in another tab, the token there was already consumed and the check fails. That UX cost is why many PHP apps use a per-session token instead; the security rationale for per-request rotation is owned by the CSRF synchronizer-token topic.
  • Is rotating the CSRF token the same as `session_regenerate_id`?
    No. Rotating the CSRF token replaces `$_SESSION['csrf']` — a value inside the session. `session_regenerate_id` changes the session *identifier* (the cookie) to defeat session fixation. You typically do both at login, but they are separate mechanisms; the session-ID one is owned by the sessions leaf.
  • How do you stop a per-form token map from growing unbounded?
    Cap the number of stored tokens and evict the oldest, and/or attach a timestamp and expire entries. Because each unsubmitted form adds an entry to `$_SESSION['csrf']`, a user opening many tabs could otherwise bloat the session store; a bounded FIFO or TTL keeps it in check.

saying these in an interview costs you the question

  • a per-request token works fine with the back button
  • you never need to change the token after login
  • rotating the CSRF token is the same as session_regenerate_id
  • per-form tokens can be kept forever without cleanup
  • one token per session is always insecure