CSRF protection
Double-submit CSRF protection sends a token in a cookie and requires the same value in a request header. A cross-site form cannot read the cookie value, so it cannot produce the matching header.
DaloyJS ships a small, framework-agnostic csrf() middleware that implements the double-submit cookie pattern. The server stamps a random token in a cookie on safe requests. The client mirrors that token in a request header on mutating requests. The middleware then compares the cookie and header in constant time and rejects mismatches with 403 Forbidden.
The token cookie is intentionally readable by client-side JavaScript so a browser client can echo it into the header. Treat XSS prevention as a separate requirement: if an attacker can run script in your origin, they can read the CSRF token too.
- 01requestBrowsercsrf()Safe request (GET) stamps a token cookieSet-Cookie: __Host-daloy.csrf=<random>
- 02responsecsrf()BrowserToken also exposed via ctx.state.csrfTokenclient echoes it on the next mutating call
- 03requestBrowsercsrf()POST /transfer with header mirroring the cookiex-csrf-token === __Host-daloy.csrf
- 04responsecsrf()HandlerConstant-time compare matches, request proceedstimingSafeEqual(cookie, header)
- 05noteAttacker pagecsrf()Cross-site POST cannot read or set the headermissing / mismatched token to 403 Forbidden
Quick start
How clients send the token
Browsers cache the token cookie automatically. Your client code only needs to read it and echo it on the next mutating call. From a SPA:
Defaults
| Option | Default |
|---|---|
cookieName | __Host-daloy.csrf |
headerName | x-csrf-token |
ignoreMethods | ["GET", "HEAD", "OPTIONS"] |
cookieOptions.sameSite | "Lax" |
cookieOptions.secure | true |
cookieOptions.path | "/" |
strategy | "double-submit" |
allowedOrigins | Required only for Fetch-Metadata legacy fallback / cross-origin allowlists |
generator | 32-byte WebCrypto random token |
The default generator requires WebCrypto (crypto.getRandomValues or crypto.randomUUID). If you run DaloyJS in an unusual runtime without WebCrypto, pass a cryptographically secure custom generator rather than falling back to predictable randomness.
The __Host- prefix
The default cookie name is prefixed with __Host-. Browsers refuse to set such a cookie unless it is also Secure, has Path=/, and has no Domain attribute. The middleware enforces those constraints at construction time, so you cannot ship a misconfigured prefix to production. To use a non-prefixed cookie (for example during local HTTP development), pass an explicit cookieName:
Custom header names and methods
Some clients (Angular, Axios) read XSRF-TOKEN and reflect it as X-XSRF-TOKEN. To match that convention, override both names and the safe-method list:
What is not covered
- Cross-origin reads. Set a strict CORS allowlist via
cors()so other origins cannot trigger credentialed reads. - HTML form posts. Render
ctx.state.csrfTokeninto a hidden field and forward it as thex-csrf-tokenheader (or use a small client-side script to do so), the middleware only reads the header, not multipart bodies. - Authentication. CSRF is orthogonal to auth. Combine
csrf()withbearerAuth()or your session middleware.
Fetch-Metadata strategy (tokenless, recommended for new apps)
The csrf() middleware also implements the modern Fetch Metadata strategy. Modern browsers send a Sec-Fetch-Site header on every request that tells the server whether the request originated from the same origin, a cross-site context, or no navigable context at all (none, such as bookmarks or direct address-bar typing). That single header is enough to defeat the classic CSRF attack model without any cookie round-trip and without coupling your HTML rendering to a token.
- ingressMutating requestPOST / PUT / PATCH / DELETE
- headerRead Sec-Fetch-Sitebrowser-attested origin context
- same-origin / noneAcceptsame-origin or none to handler
- cross-siteRejectcross-site to 403 Forbidden
In "fetch-metadata" mode the middleware does not issue or require a cookie. On mutating requests it accepts the request when:
Sec-Fetch-Siteissame-originornone, orSec-Fetch-Siteis missing (legacy browser) andOriginorReferermatches yourallowedOriginslist/predicate.
For defense in depth, pass strategy: "both". Mutating requests must then pass both the Fetch-Metadata check and the double-submit cookie check.
Non-browser clients usually do not send Sec-Fetch-Site. If they need to call protected mutating routes, give them an explicit Origin that matches allowedOrigins, or use route-level middleware so browser and machine clients can follow different CSRF policies.