@davaux/csrf

Provides Cross-Site Request Forgery (CSRF) protection using the synchronizer token pattern. A random token is generated per session and must be present on every state-changing request (POST, PUT, PATCH, DELETE). Comparison uses crypto.timingSafeEqual to prevent timing attacks.

Installation

npm install @davaux/csrf

Requirements

CSRF middleware depends on a session to store the token. Install and configure @davaux/session first:

// davaux.config.ts
import { defineConfig } from 'davaux/config'
import { sessionMiddleware } from '@davaux/session'
import { csrfMiddleware } from '@davaux/csrf'

export default defineConfig({
  middleware: [
    sessionMiddleware({ secret: process.env.SESSION_SECRET! }),
    csrfMiddleware(),  // must come after session
  ],
})

Embedding the token in forms

After the middleware runs, ctx.state.csrf.token contains the current CSRF token. Embed it as a hidden field in every HTML form:

// src/routes/transfer.page.tsx
import { definePage } from 'davaux'

export default definePage((ctx) => {
  const { token } = ctx.state.csrf

  return (
    <form method="post" action="/transfer">
      <input type="hidden" name="_csrf" value={token} />
      <input type="number" name="amount" placeholder="Amount" />
      <button type="submit">Transfer</button>
    </form>
  )
})

The middleware reads the token from the _csrf form field on POST requests and validates it against the session-stored value.

AJAX requests

For fetch/XHR requests, send the token as an X-CSRF-Token header:

// Client-side fetch
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')

await fetch('/api/items', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken ?? '',
  },
  body: JSON.stringify({ name: 'New item' }),
})

Expose the token via a <meta> tag in your layout:

<meta name="csrf-token" content={ctx.state.csrf?.token ?? ''} />

Options

csrfMiddleware({
  cookieName: '_csrf',      // session key used to store the token
  fieldName: '_csrf',       // form field name to check
  headerName: 'X-CSRF-Token', // request header to check
  ignoreMethods: ['GET', 'HEAD', 'OPTIONS'],  // methods that skip validation
})
OptionDefaultDescription
cookieName'_csrf'Session key for the stored token
fieldName'_csrf'Form field name containing the submitted token
headerName'X-CSRF-Token'HTTP header containing the submitted token
ignoreMethods['GET', 'HEAD', 'OPTIONS']Methods that do not require a valid token

TypeScript

// src/types.d.ts
import '@davaux/csrf'

declare module 'davaux' {
  interface State {
    csrf: { token: string }
  }
}

Security notes

  • Tokens are cryptographically random, generated with crypto.randomBytes
  • Comparison uses crypto.timingSafeEqual — constant-time comparison prevents timing attacks
  • Tokens are stored in the session (signed cookie), not in a separate cookie, so they are bound to the authenticated session
  • The SameSite=Lax session cookie attribute provides an additional layer of CSRF protection for simple navigations, but does not replace token validation for cross-origin POST requests