CSRF Protection
Patterns for Cross-Site Request Forgery protection in Next.js applications.
Overview#
CSRF attacks trick authenticated users into submitting malicious requests. This pattern covers:
- Token-based CSRF protection
- Double submit cookie pattern
- SameSite cookie protection
- Middleware integration
Prerequisites#
# No additional packages required - uses Node.js cryptoCode Example#
Token-Based CSRF Protection#
1// lib/csrf.ts
2import { randomBytes, createHmac } from 'crypto'
3import { cookies } from 'next/headers'
4
5const CSRF_SECRET = process.env.CSRF_SECRET!
6const CSRF_COOKIE = 'csrf_token'
7const CSRF_HEADER = 'x-csrf-token'
8
9export function generateCsrfToken(): string {
10 const token = randomBytes(32).toString('hex')
11 const signature = createHmac('sha256', CSRF_SECRET)
12 .update(token)
13 .digest('hex')
14
15 return `${token}.${signature}`
16}
17
18export function validateCsrfToken(token: string): boolean {
19 const [value, signature] = token.split('.')
20
21 if (!value || !signature) return false
22
23 const expectedSignature = createHmac('sha256', CSRF_SECRET)
24 .update(value)
25 .digest('hex')
26
27 return signature === expectedSignature
28}
29
30export async function setCsrfCookie() {
31 const token = generateCsrfToken()
32 const cookieStore = await cookies()
33
34 cookieStore.set(CSRF_COOKIE, token, {
35 httpOnly: true,
36 secure: process.env.NODE_ENV === 'production',
37 sameSite: 'strict',
38 path: '/'
39 })
40
41 return token
42}
43
44export async function verifyCsrfToken(headerToken: string | null): Promise<boolean> {
45 if (!headerToken) return false
46
47 const cookieStore = await cookies()
48 const cookieToken = cookieStore.get(CSRF_COOKIE)?.value
49
50 if (!cookieToken) return false
51
52 return headerToken === cookieToken && validateCsrfToken(headerToken)
53}CSRF Middleware#
1// middleware.ts
2import { NextResponse } from 'next/server'
3import type { NextRequest } from 'next/server'
4
5const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
6
7export function middleware(request: NextRequest) {
8 // Skip CSRF check for safe methods
9 if (SAFE_METHODS.includes(request.method)) {
10 return NextResponse.next()
11 }
12
13 // Skip for API routes that use other auth (API keys, webhooks)
14 if (request.nextUrl.pathname.startsWith('/api/webhooks')) {
15 return NextResponse.next()
16 }
17
18 const csrfHeader = request.headers.get('x-csrf-token')
19 const csrfCookie = request.cookies.get('csrf_token')?.value
20
21 if (!csrfHeader || !csrfCookie || csrfHeader !== csrfCookie) {
22 return NextResponse.json(
23 { error: 'Invalid CSRF token' },
24 { status: 403 }
25 )
26 }
27
28 return NextResponse.next()
29}
30
31export const config = {
32 matcher: '/api/:path*'
33}CSRF Token Provider#
1// components/providers/CsrfProvider.tsx
2'use client'
3
4import { createContext, useContext, useEffect, useState } from 'react'
5
6const CsrfContext = createContext<string | null>(null)
7
8export function CsrfProvider({ children }: { children: React.ReactNode }) {
9 const [token, setToken] = useState<string | null>(null)
10
11 useEffect(() => {
12 fetch('/api/csrf')
13 .then(res => res.json())
14 .then(data => setToken(data.token))
15 }, [])
16
17 return (
18 <CsrfContext.Provider value={token}>
19 {children}
20 </CsrfContext.Provider>
21 )
22}
23
24export function useCsrfToken() {
25 return useContext(CsrfContext)
26}Protected Form#
1// components/forms/ProtectedForm.tsx
2'use client'
3
4import { useCsrfToken } from '@/components/providers/CsrfProvider'
5
6export function ProtectedForm() {
7 const csrfToken = useCsrfToken()
8
9 async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
10 e.preventDefault()
11 const formData = new FormData(e.currentTarget)
12
13 const response = await fetch('/api/submit', {
14 method: 'POST',
15 headers: {
16 'Content-Type': 'application/json',
17 'X-CSRF-Token': csrfToken ?? ''
18 },
19 body: JSON.stringify(Object.fromEntries(formData))
20 })
21
22 // Handle response...
23 }
24
25 return (
26 <form onSubmit={handleSubmit}>
27 <input type="hidden" name="csrf_token" value={csrfToken ?? ''} />
28 {/* Form fields */}
29 <button type="submit">Submit</button>
30 </form>
31 )
32}SameSite Cookie Protection#
1// lib/session.ts
2// Modern approach: rely on SameSite cookies
3import { cookies } from 'next/headers'
4
5export async function setSessionCookie(sessionId: string) {
6 const cookieStore = await cookies()
7
8 cookieStore.set('session', sessionId, {
9 httpOnly: true,
10 secure: true,
11 sameSite: 'lax', // or 'strict' for more protection
12 path: '/',
13 maxAge: 60 * 60 * 24 * 7 // 1 week
14 })
15}
16
17// SameSite=Lax: Cookie sent with top-level navigations and GET from third-party
18// SameSite=Strict: Cookie only sent in first-party context
19// Combined with secure: true, this prevents most CSRF attacksUsage Instructions#
- Generate CSRF tokens for each session or form
- Include the token in a cookie and require it in request headers
- Validate tokens on all state-changing requests (POST, PUT, DELETE)
- Skip validation for safe methods and webhook endpoints
- Use SameSite cookies as an additional layer of protection
Best Practices#
- Use strong secrets - Generate CSRF_SECRET with
openssl rand -base64 32 - Validate on server - Never trust client-side validation alone
- Rotate tokens - Generate new tokens periodically or per-request
- Skip webhooks - Webhooks use signature verification instead
- Combine with SameSite - Use both CSRF tokens and SameSite cookies
- Handle failures gracefully - Return clear error messages
Related Patterns#
- Input Validation - Validate all user input
- Security Headers - HTTP security headers
- Session Management - Secure session handling
- API Middleware - Request middleware patterns