HIPsHanzo Proposals
Back to HIPs
HIP-1043FinalStandards TrackSecurity

CSRF

Hanzo AI
Created: 2026-08-20
Requires: HIP-0519

HIP-1043: CSRF

Abstract

/v1/account/csrf mints the token a browser echoes on a state-changing write. It exists for exactly one caller shape — a request authenticated by an AMBIENT cookie — and it is inert for every other. A facet of the account capability (HIP-1200), served by apps/account in hanzoai/cloud (apps/account/csrf.go); the router still serves it at the bare root today, a pair hanzoai/cloud openapi/misfiled.txt carries.

The token is a keyed MAC over the caller's validated identity and a timestamp, so it authorizes writes as that identity and as nobody else, and it expires.

Motivation

A browser write on our own origin is authenticated from an httpOnly session cookie, and a cookie is ambient: a page on another site that posts to our origin carries it too. The negative heuristics available — origin, referer, fetch metadata — pass VACUOUSLY when a request carries none of them, which is a request an attacker can construct.

So a state-changing write needs a POSITIVE control: a value obtainable only by reading a same-origin response, echoed in a header a cross-site simple or form request cannot set without a preflight this server never grants.

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

1. Scope: only the ambient path

Enforcement applies when, and only when, the request carries NO explicit Authorization/X-Authorization AND does carry a Cookie (apps/account/csrf.go:150). That is exactly the ambient-cookie browser write.

Every other caller is untouched, and this is not laxity: a bearer-authenticated request is immune because a cross-site page cannot set the Authorization header, and a gateway-fronted request carries minted identity headers and no browser cookie. A gate that also fired on those would be enforcing against a threat that does not exist there, and machine callers would grow a token dance for nothing.

Within scope, enforcement FAILS SECURE: a missing or invalid token is a refusal.

2. The token

token = base64url( timestamp_be64 || MAC )
MAC   = keyed-BLAKE3( key, domain ‖ 0 ‖ user ‖ 0 ‖ org ‖ 0 ‖ timestamp )

truncated to 128 bits (apps/account/csrf.go:100). The MAC is BOUND to the validated principal — user and org — so a token minted for one identity MUST NOT authorize a write as another. It expires after a bounded lifetime, and a small future tolerance absorbs clock skew (apps/account/csrf.go:53).

Verification is against the CURRENT request's validated identity, in constant time (apps/account/csrf.go:133).

The token carries no secret and identifies nothing that is not already known to its holder; the mint response is answered no-store so no shared cache holds it.

3. One key per process

The mint and the writes that verify are registered by different subsystems in one process, so the MAC key MUST be resolved ONCE for the process (apps/account/csrf.go:63). Resolving it per registration gives each its own random key in the un-configured case, and no minted token ever verifies.

The key comes from the secrets plane. Absent one, a per-process random key is generated with a warning: tokens then reset on restart and the client re-fetches on the refusal, which is tolerable at one replica and NOT tolerable across several. A failure of the random source is fatal — a zero key would be forgeable.

4. The gate is a middleware, not a decorator

The enforcement is a single middleware value used by both typed operations and raw handlers (apps/account/csrf.go:165). It has to be, because a decorator applied around a typed handler is dropped at registration and the operation ships UNGATED — the failure is silent and looks exactly like success.

A write registered outside the minting package uses the same exported middleware bound to the same process key (apps/account/csrf.go:195), so a token minted at the endpoint verifies at that write byte-identically. Two implementations of one check are two answers eventually.

5. What it is not

This is not authentication and MUST NOT be read as authority: the gated handler still resolves the caller and applies its own rules. The token only establishes that the request was composed by something able to read a same-origin response.

Rationale

The considered alternative is double-submit with a cookie, which needs no server key. It also trusts a value the client can write, and any subdomain that can set a cookie on the parent domain can forge one. A keyed MAC bound to the identity has no such surface, at the cost of a key the deployment has to hold — and it already holds one.

Binding to identity rather than to a session id keeps the token verifiable in a stateless process: nothing has to be looked up to check it.

Security Considerations

The token's whole strength is the same-origin policy on the READ. It follows that the mint MUST require a validated caller, MUST answer no-store, and MUST NOT be reachable in a form another origin can read — a permissive cross-origin policy on this route would silently void the entire control.

Truncating the MAC to 128 bits is sound for a bound, expiring token; extending the lifetime is what would weaken it, since an exfiltrated token is a bearer value until it lapses.

Multi-replica deployments MUST configure the key from the secrets plane. With per-process random keys, a token minted by one replica fails at another, and the refusal is indistinguishable from an attack — which trains operators to relax the gate.

References

  • HIP-0519 — One Identity Boundary
  • HIP-0027 — Secrets Management Standard
  • HIP-0106 — The Hanzo Plugin Contract
  • HIP-1200 — Account — The Caller's Own Surface

Copyright

Released under CC0 1.0 Universal Public Domain Dedication.