@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
})
| Option | Default | Description |
|---|---|---|
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=Laxsession cookie attribute provides an additional layer of CSRF protection for simple navigations, but does not replace token validation for cross-origin POST requests