Search docs

Jump between documentation pages.

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

ts
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:

ts
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

OptionDefault
cookieName__Host-daloy.csrf
headerNamex-csrf-token
ignoreMethods["GET", "HEAD", "OPTIONS"]
cookieOptions.sameSite"Lax"
cookieOptions.securetrue
cookieOptions.path"/"
generator32-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:

ts
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:

ts
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.csrfToken into a hidden field and forward it as the x-csrf-token header (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() with bearerAuth() or your session middleware.