Next.js Server Actions rely on Origin header checks, not CSRF tokens. Reverse proxies and misconfigured allowedOrigins can bypass this protection. [CWE-352 · A01:2021]
Why This Matters
cross-site request forgery enabling unauthorized state changes on behalf of authenticated users
Impact: HIGH (cross-site request forgery enabling unauthorized state changes on behalf of authenticated users)
Next.js Server Actions have built-in CSRF mitigation: they only accept POST requests and compare the Origin header against the host. However, this is not a CSRF token — it has known limitations:
Reverse proxies that strip the Origin header — some proxies, load balancers, or WAFs remove or rewrite the Origin header, disabling the check entirely
Misconfigured serverActions.allowedOrigins — adding domains to the allowlist expands who can invoke your actions cross-origin
Same-site subdomains — if an attacker controls evil.example.com and your app is on app.example.com, same-origin policies may not protect you
Incorrect (assuming Server Actions are CSRF-proof):
// next.config.js// ❌ Allowing untrusted originsmodule.exports = { experimental: { serverActions: { allowedOrigins: [ 'app.example.com', 'staging.example.com', 'partner-site.com', // Do you trust this domain's security? ], }, },}
Correct (tighten CSRF protection):
// next.config.js// ✅ Only allow your own domainsmodule.exports = { experimental: { serverActions: { allowedOrigins: ['app.example.com'], // Strict — only your production domain }, },}
// ✅ For sensitive mutations, add a CSRF token as defense-in-depth// lib/csrf.tsimport { cookies } from 'next/headers'import { randomBytes } from 'crypto'export function generateCsrfToken() { const token = randomBytes(32).toString('hex') cookies().set('csrf-token', token, { httpOnly: true, sameSite: 'strict', secure: process.env.NODE_ENV === 'production', }) return token}export function validateCsrfToken(token: string) { const stored = cookies().get('csrf-token')?.value if (!stored || stored !== token) { throw new Error('Invalid CSRF token') }}
// ✅ Use the token in high-value Server Actions'use server'import { validateCsrfToken } from '@/lib/csrf'import { z } from 'zod'const TransferSchema = z.object({ csrfToken: z.string(), toAccount: z.string(), amount: z.number().positive(),})export async function transferFunds(rawInput: unknown) { const { csrfToken, toAccount, amount } = TransferSchema.parse(rawInput) validateCsrfToken(csrfToken) // Extra layer for high-value actions // ... proceed with transfer}
If behind a reverse proxy, ensure Origin is forwarded: