module

KemalIdentity::CSRF

Cross-site request forgery protection.

SameSite is defence in depth, not a replacement. It is a browser-side control with inconsistent behaviour across clients, it does nothing for a request the browser considers same-site, and Lax — which this shard defaults to, because Strict breaks return-from-OAuth navigation — permits top-level cross-site GETs (docs/02-security-model.md).

The scheme: a signed, session-bound, masked token

No server-side token store, and no plain double-submit either.

Plain double-submit — compare a cookie to a form field — fails the requirement that a token from another session is rejected: anyone who can set the cookie can also set the field, and the two would agree. So the token is an HMAC the application alone can compute:

raw    = HMAC-SHA256(secret, anchor)
pad    = 32 random bytes
token  = base64url(pad + (raw XOR pad))

anchor is the session id for an authenticated request, and the value of a dedicated __Host- prefixed cookie for an anonymous one. An attacker cannot compute raw without the secret, and cannot read the victim's anchor — so a token minted for their own session fails against the victim's.

The mask exists because raw is otherwise constant for the lifetime of a session, and a value that repeats in every response is what BREACH-style compression oracles extract. The pad changes per issue, so the rendered token changes with it while still verifying.

The anonymous anchor, and why login CSRF is covered

docs/02-security-model.md calls login CSRF "the case most implementations miss": without a token on the login form, an attacker logs the victim into the attacker's account and then observes whatever the victim does under it — anything typed, uploaded or purchased lands in an account the attacker controls.

The login form is anonymous, so it has no session to bind to. It binds to the anchor cookie instead, which is issued lazily the first time a token is asked for. The __Host- prefix is doing real work here: it forbids a Domain attribute, so a compromised sibling subdomain cannot plant an anchor the attacker knows.

Constants

DIGEST_BYTES = 32

SHA-256, so 32 bytes of signature and 32 of pad.

PATTERN = /\A[A-Za-z0-9_-]+\z/
TOKEN_BYTES = DIGEST_BYTES * 2
TOKEN_LENGTH = 86

base64url of 64 bytes, unpadded.

Class methods

issue(secret : Secret, anchor : String, random : RandomSource) : String

Mints a token for anchor.

Source
valid?(secret : Secret, anchor : String, presented : String | Nil) : Bool

Whether presented is a token this application issued for anchor.

Returns false for anything malformed rather than raising: the input is whatever a client chose to send, and a hostile value must be a rejection, not a 500.

Source