CSRF protection
Think of it like… a coat-check counter. When you walk in, the doorman quietly slips a numbered token into your pocket (the cookie). When you later try to claim something at the counter, you have to show that same number written on a slip (the header). A stranger who never walked past the doorman can't guess the number, so they can't use your name to grab a coat that isn't theirs, even if they know which counter to walk up to.
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 Metadatastrategy. 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.