NextRush
API ReferenceMiddleware

@nextrush/csrf

CSRF protection middleware using the Signed Double-Submit Cookie pattern with HMAC-SHA256.

Why CSRF Protection Exists

Cross-Site Request Forgery (CSRF) tricks a user's browser into making unwanted requests to your application — transferring funds, changing passwords, or deleting data — because the browser automatically attaches session cookies to every request.

Without CSRF protection, any website can forge requests that your server treats as legitimate because session cookies alone cannot distinguish intentional user actions from forged ones.

@nextrush/csrf implements the Signed Double-Submit Cookie pattern recommended by OWASP:

  1. Generate a cryptographically signed token (HMAC-SHA256)
  2. Set it as a cookie (readable by client JavaScript)
  3. Client submits the token via header or form field
  4. Server verifies cookie token matches submitted token AND validates the HMAC signature

What CSRF Protection Covers

AttackDefense
Cross-site form submissionToken required on unsafe methods
Token forgeryHMAC-SHA256 signature verification
Session fixation via token reuseOptional session binding
Cross-origin API abuseOptional Origin/Sec-Fetch-Site check
Brute-force token guessing32-byte cryptographic random values
Timing attacks on token comparisonConstant-time HMAC-based comparison

What CSRF Protection Does NOT Do

ConcernPackage
Security headers@nextrush/helmet
Input validationZod, ArkType, or similar
AuthenticationJWTs, OAuth, or session auth
Rate limiting@nextrush/rate-limit
CORS@nextrush/cors

Default Behavior

Calling csrf({ secret }) returns { protect, tokenProvider }. The protect middleware:

  • Skips safe methods: GET, HEAD, OPTIONS, TRACE
  • Validates unsafe methods: checks cookie → header/body/query token → constant-time compare → HMAC verification
  • Attaches ctx.state.csrf with generateToken() and cookieToken on every request

Stateless by Design

No server-side token storage required. HMAC verification ensures token integrity without database lookups or session state.


Installation

$ pnpm add @nextrush/csrf

Minimal Usage

import { createApp } from '@nextrush/core';
import { csrf } from '@nextrush/csrf';

const app = createApp();

const { protect, tokenProvider } = csrf({
  secret: process.env.CSRF_SECRET!, // min 32 characters
});

// Apply protection to all routes
app.use(protect);

Token Lifecycle

import { createApp } from '@nextrush/core';
import { createRouter } from '@nextrush/router';
import { csrf } from '@nextrush/csrf';
import type { CsrfContext } from '@nextrush/csrf';

const app = createApp();
const router = createRouter();

const { protect } = csrf({
  secret: process.env.CSRF_SECRET!,
});

app.use(protect);

// Step 1: Generate token on GET (e.g., render form page)
router.get('/form', async (ctx) => {
  const csrfCtx = ctx.state.csrf as CsrfContext;
  const token = await csrfCtx.generateToken(); // Sets cookie automatically
  ctx.json({ csrfToken: token });
});

// Step 2: Client submits token via header
// fetch('/api/submit', {
//   method: 'POST',
//   headers: { 'x-csrf-token': token },
//   credentials: 'same-origin',
// });

// Step 3: protect middleware validates automatically
router.post('/api/submit', (ctx) => {
  ctx.json({ ok: true });
});

app.route('/', router);

Session Binding

Bind tokens to user sessions to prevent token reuse across sessions:

const { protect } = csrf({
  secret: process.env.CSRF_SECRET!,
  getSessionIdentifier: (ctx) => ctx.state.sessionId as string,
});

When session binding is enabled:

  • Tokens generated for session A cannot be used in session B
  • Prevents session fixation attacks via CSRF tokens

Configuration Options

csrf()

Create CSRF protection middleware.

Signature:

function csrf(options: CsrfOptions): CsrfMiddleware;

Returns: CsrfMiddleware{ readonly protect: Middleware; readonly tokenProvider: Middleware }

  • protect: Full CSRF enforcement middleware (validates on unsafe methods)
  • tokenProvider: Attaches ctx.state.csrf without enforcement (for token generation routes)

CsrfOptions

PropertyTypeDescription
secretstring | (() => string)HMAC secret key for token signing. Must be at least 32 characters. Use a function for secret rotation.
getSessionIdentifier?(ctx: Context) => string | undefinedExtract session ID from context for session-bound tokens.
getTokenFromRequest?(ctx: Context) => string | undefined | nullCustom token extraction function. Overrides default extraction order.
ignoredMethodsstring[]= ['GET', 'HEAD', 'OPTIONS', 'TRACE']HTTP methods to skip validation for.
excludePathsstring[]= []URL paths to exclude from CSRF validation. Supports exact match, single wildcard (/*), and double wildcard (/**).
cookie?CsrfCookieOptionsCookie configuration for the CSRF token.
tokenSizenumber= 32Size of the random token value in bytes.
onError?(ctx: Context, reason: string) => void | Promise<void>Custom error handler called when CSRF validation fails. Receives the failure reason string.
originCheckboolean= falseEnable Origin/Sec-Fetch-Site header verification as an additional defense layer.
allowedOriginsstring[]= []Origins to allow when originCheck is enabled. Bypasses Origin/Host comparison.

CsrfCookieOptions

PropertyTypeDescription
namestring= '__Host-csrf'Cookie name. Use __Host- prefix for maximum security.
pathstring= '/'Cookie path.
sameSite'strict' | 'lax' | 'none'= 'strict'SameSite attribute.
secureboolean= trueSecure flag. Required for __Host- prefixed cookies.
httpOnlyboolean= falseHttpOnly flag. Must be false so client JavaScript can read the token.
domainstring= '' (not set)Cookie domain. Cannot be set with __Host- prefix.
maxAgenumber= 0Cookie max age in seconds. 0 means session cookie.

Token Extraction Order

When validating an unsafe request, the middleware extracts the submitted token in this order:

  1. Header: x-csrf-token or x-xsrf-token
  2. Body: _csrf field (requires body parser)
  3. Query: _csrf parameter (last resort)

Override with a custom extractor:

const { protect } = csrf({
  secret: process.env.CSRF_SECRET!,
  getTokenFromRequest: (ctx) => {
    return ctx.get('x-my-csrf-token') ?? undefined;
  },
});

Path Exclusion

Exclude webhook endpoints or public APIs from CSRF validation:

const { protect } = csrf({
  secret: process.env.CSRF_SECRET!,
  excludePaths: [
    '/api/webhooks/stripe', // Exact match
    '/api/webhooks/*', // Single level wildcard
    '/api/public/**', // Any depth wildcard
  ],
});
PatternMatchesDoes NOT Match
/api/webhooks/stripe/api/webhooks/stripe/api/webhooks/github
/api/webhooks/*/api/webhooks/stripe, /api/webhooks/github/api/webhooks/stripe/events
/api/webhooks/**/api/webhooks, /api/webhooks/stripe/events/1/api/users

Origin Check

Enable Sec-Fetch-Site and Origin header validation as defense-in-depth:

const { protect } = csrf({
  secret: process.env.CSRF_SECRET!,
  originCheck: true,
  allowedOrigins: ['https://admin.example.com'],
});

The origin check validates in order:

  1. Sec-Fetch-Site: allows same-origin, same-site, none; rejects cross-site
  2. Origin header: compares against Host header or allowedOrigins list
  3. No Origin header: allows (non-CORS same-origin request)

Custom Error Handling

const { protect } = csrf({
  secret: process.env.CSRF_SECRET!,
  onError: (ctx, reason) => {
    ctx.status = 403;
    ctx.json({
      error: 'CSRF_FAILED',
      reason,
      requestId: ctx.state.requestId,
    });
  },
});

Error reasons:

ReasonMeaning
'CSRF cookie missing'No CSRF cookie found on unsafe request
'CSRF token missing from request'Cookie present but no token in header/body/query
'CSRF token mismatch'Submitted token doesn't match cookie token
'CSRF token invalid (HMAC verification failed)'Token pair matches but HMAC signature is invalid
'Origin check failed'Origin/Sec-Fetch-Site check rejected the request

Dynamic Secret Rotation

Support key rotation without invalidating in-flight tokens:

const secrets = [process.env.CSRF_SECRET_NEW!, process.env.CSRF_SECRET_OLD!];

const { protect } = csrf({
  secret: () => secrets[0]!, // Always sign with newest
});

Token Engine API

Low-level functions exported for advanced use cases or testing:

generateToken()

function generateToken(secret: string, sessionId?: string, tokenSize?: number): Promise<string>;

Generate an HMAC-SHA256 signed CSRF token.

Returns: Token string in format <hmac-hex>.<random-hex>

validateToken()

function validateToken(token: string, secret: string, sessionId?: string): Promise<boolean>;

Validate a token's HMAC signature. Uses crypto.subtle.verify for implementation-provided constant-time comparison.

constantTimeEqual()

function constantTimeEqual(a: string, b: string): Promise<boolean>;

Compare two strings in constant time using HMAC-based comparison to prevent timing attacks.


Exported Types

import type {
  CsrfOptions,
  CsrfCookieOptions,
  CsrfContext,
  CsrfMiddleware,
  TokenExtractor,
  SessionIdentifierExtractor,
} from '@nextrush/csrf';

CsrfContext

Attached to ctx.state.csrf by both protect and tokenProvider middleware.

CsrfContext

PropertyTypeDescription
generateToken() => Promise<string>Generate a new CSRF token and set the cookie. Returns the token string.
cookieTokenstring | undefinedThe current CSRF token from the cookie, if present.

CsrfMiddleware

Return type of the csrf() factory function.

CsrfMiddleware

PropertyTypeDescription
protectMiddlewareFull CSRF enforcement middleware. Validates tokens on unsafe methods.
tokenProviderMiddlewareToken generation middleware. Attaches ctx.state.csrf without enforcement.

Exported Constants

import {
  DEFAULT_COOKIE_NAME, // '__Host-csrf'
  DEFAULT_IGNORED_METHODS, // ['GET', 'HEAD', 'OPTIONS', 'TRACE']
  DEFAULT_TOKEN_SIZE, // 32
  CSRF_HEADER, // 'x-csrf-token'
  XSRF_HEADER, // 'x-xsrf-token'
  CSRF_FIELD, // '_csrf'
  ERRORS, // Error reason strings
} from '@nextrush/csrf';

Security Design

The default cookie uses __Host- prefix which enforces:

  • Secure flag (HTTPS only)
  • No Domain attribute (origin-locked)
  • Path=/

Combined with SameSite=Strict and HttpOnly=false (client JS must read the token).

HMAC Token Format

<hmac-hex>.<random-hex>

The HMAC message payload:

  • Without session: <randomHex.length>!<randomHex>
  • With session: <sessionId.length>!<sessionId>!<randomHex.length>!<randomHex>

Runtime Compatibility

Uses Web Crypto API exclusively (crypto.subtle, crypto.getRandomValues). Compatible with:

  • Node.js 22+
  • Bun
  • Deno
  • Cloudflare Workers
  • Vercel Edge Functions

No node:crypto imports. Zero external dependencies.

On this page