CSRF protection
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.
Quick start
import { App, csrf } from "@daloyjs/core";
const app = new App();
app.use(csrf());
app.route({
method: "GET",
path: "/me",
operationId: "me",
responses: { 200: { description: "ok" } },
handler: async ({ state }) => ({
status: 200 as const,
// ctx.state.csrfToken is always populated; render it into your form
// or expose it to the SPA via a JSON envelope.
body: { csrfToken: state.csrfToken },
}),
});
app.route({
method: "POST",
path: "/transfer",
operationId: "transfer",
responses: { 204: { description: "ok" }, 403: { description: "denied" } },
handler: async () => ({ status: 204 as const, body: undefined }),
});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:
function getCsrf(): string {
const m = document.cookie.match(/(?:^|; )__Host-daloy\.csrf=([^;]+)/);
return m ? decodeURIComponent(m[1]!) : "";
}
await fetch("/transfer", {
method: "POST",
credentials: "include",
headers: {
"content-type": "application/json",
"x-csrf-token": getCsrf(),
},
body: JSON.stringify({ amount: 42 }),
});Defaults
| Option | Default |
|---|---|
cookieName | __Host-daloy.csrf |
headerName | x-csrf-token |
ignoreMethods | ["GET", "HEAD", "OPTIONS"] |
cookieOptions.sameSite | "Lax" |
cookieOptions.secure | true |
cookieOptions.path | "/" |
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:
app.use(csrf({
cookieName: "csrf",
cookieOptions: {
secure: false, // local dev over plain HTTP
sameSite: "Lax",
},
}));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:
app.use(csrf({
cookieName: "XSRF-TOKEN",
headerName: "X-XSRF-TOKEN",
ignoreMethods: ["GET", "HEAD", "OPTIONS", "TRACE"],
cookieOptions: {
sameSite: "Lax",
secure: true,
// Optional: long-lived cookie so SPAs don't have to reissue per session.
maxAgeSeconds: 60 * 60 * 24 * 7,
},
}));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.