Cross-Site Request Forgery (CSRF) attacks represent one of the most insidious threats facing modern web applications. Unlike attacks that require stealing credentials, CSRF exploits the browser's fundamental behavior of automatically including authentication cookies with every request. For Next.js applications handling sensitive operations--from financial transactions to account management--understanding and implementing robust CSRF protection is not optional, it's essential.
This guide walks you through comprehensive defense strategies specifically tailored for Next.js applications. You'll learn how SameSite cookies provide your first line of defense, how to implement CSRF tokens using proven patterns, and how to leverage middleware for centralized protection across your entire application.
Understanding Cross-Site Request Forgery
Cross-Site Request Forgery attacks exploit a fundamental browser behavior: when you authenticate to a website, the browser automatically includes your session cookies with every request to that domain. This convenient feature becomes a critical vulnerability when attackers trick your browser into making requests you never intended.
The danger of CSRF lies in its invisibility to users. Unlike phishing attacks that require fake login pages, CSRF attacks can be triggered by a single click on a malicious link or by simply visiting a compromised website. The attacker doesn't need to know your password--they simply need you to have an active session when their attack fires.
According to StackHawk's CSRF security research, CSRF attacks commonly target financial transactions, password changes, and account settings modifications--operations that can have serious real-world consequences when executed without the user's consent.
Why Next.js Applications Are Targets
Modern Next.js applications present unique attack surfaces that require careful security consideration. The framework's API routes handle state-changing operations without traditional form submissions, making them potential targets for forged requests. Server Actions blur the line between client and server code, creating new vectors that must be properly protected.
The App Router's middleware layer provides powerful capabilities for request handling, but this power requires careful CSRF configuration to prevent attacks from slipping through. Additionally, third-party integrations and webhooks often bypass traditional CSRF protections, requiring explicit handling in your Next.js application.
As highlighted in Clerk's Next.js authentication guide, modern Next.js applications must implement defense-in-depth strategies that combine multiple CSRF protection mechanisms rather than relying on any single approach.
The Anatomy of a CSRF Attack
Understanding how CSRF attacks unfold helps clarify why protection is essential. Consider this typical attack scenario:
-
User Authentication: A user logs into your Next.js application, receiving a session cookie that authenticates their future requests.
-
Malicious Setup: The attacker creates a website containing malicious code--often hidden in an image tag or form that submits automatically.
-
The Trigger: While still logged into your application, the user visits the attacker's site through a link, email, or compromised advertisement.
-
Automatic Execution: The browser loads the attacker's page and automatically sends a request to your API endpoint, including all authentication cookies.
-
Server Processing: Your server receives what appears to be a legitimate request from an authenticated user and processes the forged command.
The attack succeeds because the server has no way to distinguish between a request the user intentionally made and one the attacker triggered. This is why implementing CSRF protection is critical for any Next.js application handling sensitive operations.
Visualizing the Attack Flow
┌─────────────┐ 1. Login ┌──────────────────┐
│ User │ ───────────────► │ Your Next.js │
│ Browser │ │ Application │
└─────────────┘ └──────────────────┘
│ │
│ 3. Visit malicious site │ 5. Forged request
│ (cookies still valid) │ with valid cookies
▼ ▼
┌─────────────┐ ┌──────────────────┐
│ Attacker's │ │ Your Next.js │
│ Website │ ─────────────────► │ Application │
│ │ 4. Triggers │ (processes │
└─────────────┘ attack │ forged request) │
└──────────────────┘
First Line of Defense: SameSite Cookies
SameSite cookies represent the foundational layer of CSRF protection built directly into browsers. By setting the SameSite attribute on your session cookies, you gain browser-enforced protection against cross-site request forgery without additional application logic.
When you configure a cookie with SameSite=Strict or SameSite=Lax, browsers will automatically block cross-site requests from including that cookie. This means even if an attacker manages to trigger a request from a user's browser, the authentication cookie won't be sent--making the forged request meaningless.
Configuring SameSite Cookie Attributes
The SameSite attribute offers three modes, each providing different levels of protection:
-
SameSite=Strict: The cookie is never sent with cross-site requests. This provides maximum security but may impact user experience when users arrive at your site through external links. -
SameSite=Lax: The cookie is sent with top-level navigations (like clicking a link) and with safe HTTP methods like GET. This provides solid protection while maintaining reasonable user experience for normal browsing. -
SameSite=None: The cookie is always sent with cross-site requests but requires theSecureattribute (HTTPS only). Use this only when cross-site access is genuinely required, such as in OAuth flows.
Implementing proper cookie configuration in your Next.js application requires setting these attributes at the session level. The following example demonstrates secure cookie configuration using Next.js conventions:
1// lib/cookies.ts - Secure cookie configuration for Next.js2 3export const sessionCookieOptions = {4 httpOnly: true, // Prevents JavaScript access to cookies5 secure: process.env.NODE_ENV === 'production', // HTTPS only in production6 sameSite: 'strict' as const, // Blocks cross-site requests7 maxAge: 60 * 60 * 24 * 7, // 1 week expiration8 path: '/', // Available across entire site9}10 11export const csrfCookieOptions = {12 httpOnly: true,13 secure: process.env.NODE_ENV === 'production',14 sameSite: 'strict' as const,15 maxAge: 60 * 60, // 1 hour for CSRF tokens16 path: '/',17}Limitations of SameSite Alone
While SameSite cookies provide valuable protection, they shouldn't be your only defense against CSRF attacks. Understanding their limitations helps you build a complete security strategy.
Browser Compatibility: Although modern browsers universally support SameSite, older browsers may not fully implement the specification, leaving some users unprotected.
OAuth and Third-Party Integrations: Many OAuth flows require SameSite=None to function properly, which removes CSRF protection for those specific interactions. You'll need additional mechanisms for these scenarios.
Safari Quirks: Apple's Safari browser has historically implemented Intelligent Tracking Prevention in ways that can interfere with SameSite cookie behavior, potentially reducing protection effectiveness.
Defense in Depth: Security best practices recommend layering SameSite cookies with additional CSRF protection mechanisms. If an attacker finds a way to bypass SameSite (perhaps through a browser vulnerability), token-based validation provides a second line of defense.
As recommended in StackHawk's security guide, treat SameSite cookies as your first line of defense while implementing CSRF tokens for critical operations.
CSRF Token Implementation Strategies
CSRF tokens provide application-level protection that works alongside browser-level SameSite cookies. These tokens create a secret value that both the server and client know, allowing the server to verify that requests genuinely originated from your application.
Two primary patterns have proven effective for CSRF protection: the Synchronizer Token Pattern and the Double-Submit Cookie Pattern. Each offers different tradeoffs between complexity and statelessness. Our web development team routinely implements these patterns as part of our comprehensive security approach for Next.js projects.
Synchronizer Token Pattern
The synchronizer token pattern has been the gold standard for CSRF protection for years. The server generates a cryptographically random token, stores it in the user's session, and requires that token to be included with every state-changing request.
This approach requires server-side session storage but provides strong guarantees: an attacker cannot guess the token value, and the server can always verify that submitted tokens match the session-bound value.
1// lib/csrf.ts - Synchronizer token implementation2 3import { createCookie } from '@oslojs/cookie'4import { encodeBase64url, generateRandomBytes } from '@oslojs/encoding'5 6const csrfCookie = createCookie('csrf_token', {7 httpOnly: true,8 secure: process.env.NODE_ENV === 'production',9 sameSite: 'strict',10 maxAge: 60 * 60, // 1 hour expiration11 path: '/',12})13 14/**15 * Generate a cryptographically secure CSRF token16 */17export function generateCsrfToken(): string {18 const bytes = generateRandomBytes(32)19 return encodeBase64url(bytes)20}21 22/**23 * Set CSRF token in response headers24 */25export function setCsrfToken(headers: Headers): void {26 const token = generateCsrfToken()27 csrfCookie.set(headers, token)28}29 30/**31 * Retrieve CSRF token from request headers32 */33export function getCsrfToken(headers: Headers): string | null {34 return csrfCookie.get(headers)35}36 37/**38 * Validate CSRF token against expected value39 */40export function validateCsrfToken(41 headers: Headers,42 expectedToken: string43): boolean {44 const token = csrfCookie.get(headers)45 return token === expectedToken && token.length > 046}Double-Submit Cookie Pattern
The double-submit cookie pattern offers a stateless alternative that eliminates the need for server-side session storage. A random token is generated and set as both a cookie and required header--the server simply validates that the cookie value matches the header value.
This approach works because an attacker cannot read cookie values due to the Same-Origin Policy, meaning they cannot include the correct token value in their forged requests. Even if they can set cookies, they cannot read them to confirm the value they set.
The critical security requirement is using constant-time comparison to prevent timing attacks, where attackers infer token values by measuring response times.
1// lib/csrf.ts - Double-submit cookie pattern2 3import { createCookie } from '@oslojs/cookie'4import { encodeBase64url, generateRandomBytes } from '@oslojs/encoding'5import { timingSafeEqual } from '@oslojs/crypto'6 7const csrfCookie = createCookie('csrf_token', {8 httpOnly: true,9 secure: process.env.NODE_ENV === 'production',10 sameSite: 'strict',11 maxAge: 60 * 60,12 path: '/',13})14 15/**16 * Create a new double-submit token17 */18export function createDoubleSubmitToken(): string {19 const bytes = generateRandomBytes(32)20 return encodeBase64url(bytes)21}22 23/**24 * Validate double-submit tokens with constant-time comparison25 * Prevents timing attacks by ensuring comparison takes constant time26 */27export async function validateDoubleSubmit(28 cookieToken: string | null,29 headerToken: string | null30): Promise<boolean> {31 if (!cookieToken || !headerToken) return false32 if (cookieToken.length < 32) return false33 34 // Constant-time comparison to prevent timing attacks35 return timingSafeEqual(36 Buffer.from(cookieToken),37 Buffer.from(headerToken)38 )39}40 41/**42 * Set double-submit token in cookie43 */44export function setDoubleSubmitToken(headers: Headers): string {45 const token = createDoubleSubmitToken()46 csrfCookie.set(headers, token)47 return token48}Implementing CSRF Protection in Next.js App Router
Modern Next.js applications using the App Router require thoughtful integration of CSRF protection across API routes, Server Actions, and middleware. Creating a dedicated CSRF utility module centralizes your protection logic and makes maintenance easier.
For API routes, you should validate both the origin header and the CSRF token. The origin check provides an early exit for cross-origin attacks, while token validation ensures the request originated from your application. This layered approach catches different attack vectors at different points in your request handling. Our web development services include comprehensive security implementation for Next.js applications.
1// app/api/protected-route/route.ts - API route with CSRF validation2 3import { NextRequest, NextResponse } from 'next/server'4import { validateDoubleSubmit, setDoubleSubmitToken } from '@/lib/csrf'5 6export async function GET(request: NextRequest) {7 // Return CSRF token for client-side retrieval8 const response = NextResponse.json({})9 const token = setDoubleSubmitToken(response.headers)10 return NextResponse.json({ token })11}12 13export async function POST(request: NextRequest) {14 // Validate origin for cross-origin protection15 const origin = request.headers.get('origin')16 const allowedOrigins = ['https://yourdomain.com', 'http://localhost:3000']17 18 // Skip CSRF check for same-origin requests (browser security)19 if (origin && !allowedOrigins.includes(origin)) {20 return NextResponse.json(21 { error: 'Origin not allowed' },22 { status: 403 }23 )24 }25 26 // Validate CSRF token27 const cookieToken = request.cookies.get('csrf_token')?.value ?? null28 const headerToken = request.headers.get('x-csrf-token') ?? null29 30 if (!(await validateDoubleSubmit(cookieToken, headerToken))) {31 return NextResponse.json(32 { error: 'Invalid CSRF token' },33 { status: 403 }34 )35 }36 37 // Proceed with request handling38 return NextResponse.json({ success: true, data: 'Protected operation completed' })39}Server Actions CSRF Protection
Next.js Server Actions introduce a powerful paradigm for handling form submissions and server-side operations directly from React components. Understanding how they handle CSRF protection--and where manual intervention is required--is essential for building secure applications.
How Next.js Server Actions Handle CSRF
Out of the box, Next.js includes built-in CSRF protection for Server Actions. When you use the standard <form> element with a Server Action, Next.js automatically includes a CSRF token and validates it on the server. This automatic protection covers the majority of use cases without requiring additional code.
However, the protection isn't universal. Custom Server Actions that receive raw FormData or use programmatic invocation bypass this automatic mechanism. For these cases, you must implement CSRF validation manually.
As documented in TurboStarter's Next.js security guide, even built-in Server Action protection should be understood rather than blindly trusted--knowing what's happening under the hood helps you identify potential gaps.
Custom Server Actions with CSRF Validation
When implementing custom Server Actions that handle sensitive operations, adding explicit CSRF validation ensures protection even when the automatic mechanisms don't apply.
1// app/actions/secure-action.ts - Custom Server Action with CSRF validation2 3'use server'4 5import { cookies } from 'next/headers'6import { validateDoubleSubmit } from '@/lib/csrf'7 8export async function secureAction(formData: FormData) {9 // Extract CSRF token from form data10 const csrfToken = formData.get('_csrf') as string11 12 // Get token from cookie13 const cookieStore = await cookies()14 const cookieToken = cookieStore.get('csrf_token')?.value ?? null15 16 // Validate CSRF token17 if (!(await validateDoubleSubmit(cookieToken, csrfToken))) {18 throw new Error('Invalid CSRF token')19 }20 21 // Proceed with the secure operation22 try {23 // Your protected logic here24 await processSensitiveOperation(formData)25 return { success: true, message: 'Operation completed securely' }26 } catch (error) {27 return { success: false, error: 'Operation failed' }28 }29}30 31// Example usage in component:32// <form action={secureAction}>33// <input type="hidden" name="_csrf" value={csrfToken} />34// {/* other form fields */}35// </form>1// components/SecureForm.tsx - Form component with CSRF token integration2 3'use client'4 5import { useState, useEffect } from 'react'6import { secureAction } from '@/app/actions/secure-action'7 8export function SecureForm() {9 const [csrfToken, setCsrfToken] = useState('')10 const [loading, setLoading] = useState(true)11 12 // Fetch CSRF token on component mount13 useEffect(() => {14 async function fetchToken() {15 try {16 const response = await fetch('/api/csrf-token')17 const data = await response.json()18 setCsrfToken(data.token)19 } catch (error) {20 console.error('Failed to fetch CSRF token:', error)21 } finally {22 setLoading(false)23 }24 }25 fetchToken()26 }, [])27 28 if (loading) {29 return <div className="loading">Loading secure form...</div>30 }31 32 return (33 <form action={secureAction} className="secure-form">34 <input35 type="hidden"36 name="_csrf"37 value={csrfToken}38 />39 <div className="form-group">40 <label htmlFor="email">Email Address</label>41 <input42 type="email"43 id="email"44 name="email"45 required46 placeholder="[email protected]"47 />48 </div>49 <button type="submit" disabled={!csrfToken}>50 Submit Securely51 </button>52 </form>53 )54}Middleware-Based CSRF Protection
Middleware provides a centralized point for enforcing CSRF protection across all routes in your Next.js application. Rather than implementing validation in every API route or Server Action, middleware can apply consistent protection rules automatically.
This approach offers significant maintenance benefits: when you need to update your CSRF validation logic, you change it in one place rather than across dozens of files. Middleware also enables consistent handling of edge cases like exempt routes and custom error responses.
Implementing middleware-based CSRF protection requires careful consideration of which routes need protection, which can be exempted, and how to handle requests that fail validation.
1// middleware.ts - Centralized CSRF protection middleware2 3import { NextResponse } from 'next/server'4import type { NextRequest } from 'next/server'5import { validateDoubleSubmit } from '@/lib/csrf'6 7// HTTP methods that can modify state8const protectedMethods = ['POST', 'PUT', 'DELETE', 'PATCH']9 10// Paths that require CSRF protection11const protectedPaths = ['/api/', '/actions/']12 13// Paths exempted from CSRF validation14const skipPaths = [15 '/api/webhook/', // External webhooks16 '/api/auth/', // Authentication endpoints17 '/api/health/', // Health check endpoints18]19 20export async function middleware(request: NextRequest) {21 // Skip CSRF check for non-modifying methods22 if (!protectedMethods.includes(request.method)) {23 return NextResponse.next()24 }25 26 const path = request.nextUrl.pathname27 28 // Skip exempted paths29 if (skipPaths.some(skip => path.startsWith(skip))) {30 return NextResponse.next()31 }32 33 // Skip paths not requiring protection34 if (!protectedPaths.some(protected => path.startsWith(protected))) {35 return NextResponse.next()36 }37 38 // Validate origin header for cross-origin requests39 const origin = request.headers.get('origin')40 const allowedOrigins = ['https://yourdomain.com', 'http://localhost:3000']41 42 if (origin && !allowedOrigins.includes(origin)) {43 return NextResponse.json(44 { error: 'Cross-origin requests not allowed' },45 { status: 403 }46 )47 }48 49 // Validate CSRF token50 const cookieToken = request.cookies.get('csrf_token')?.value ?? null51 const headerToken = request.headers.get('x-csrf-token') ?? null52 53 if (!(await validateDoubleSubmit(cookieToken, headerToken))) {54 return NextResponse.json(55 { error: 'CSRF validation failed' },56 { status: 403 }57 )58 }59 60 return NextResponse.next()61}62 63// Configure which routes the middleware applies to64export const config = {65 matcher: [66 '/api/:path*',67 '/actions/:path*',68 ],69}Testing Your CSRF Protection
Implementing CSRF protection is only half the battle--you must also verify that your protection actually works. Regular testing ensures your defenses haven't regressed and catches new vulnerabilities before attackers find them.
Manual Testing Approaches
Manual testing provides quick feedback during development and helps you understand exactly how your CSRF protection behaves. Using command-line tools like curl lets you send requests with and without valid tokens to verify your protection works as expected. Our web development team follows rigorous testing protocols to ensure security implementations are thoroughly validated.
1# Test 1: Request without CSRF token (should fail with 403)2curl -X POST https://yourdomain.com/api/protected \3 -H "Content-Type: application/json" \4 -d '{"data": "test"}'5 6# Expected response: {"error":"CSRF validation failed"}7# Expected status: 403 Forbidden8 9# Test 2: Request with valid CSRF token (should succeed)10curl -X POST https://yourdomain.com/api/protected \11 -H "Content-Type: application/json" \12 -H "X-CSRF-Token: valid-token-here" \13 -b "csrf_token=valid-token-here" \14 -d '{"data": "test"}'15 16# Expected response: {"success":true}17# Expected status: 200 OK18 19# Test 3: Cross-origin request without valid origin (should fail)20curl -X POST https://yourdomain.com/api/protected \21 -H "Content-Type: application/json" \22 -H "Origin: https://malicious-site.com" \23 -d '{"data": "test"}'24 25# Expected response: {"error":"Origin not allowed"}26# Expected status: 403 ForbiddenAutomated Testing
Automated tests provide regression protection and ensure your CSRF protection continues working as your application evolves. Integrate CSRF tests into your CI/CD pipeline to catch issues before they reach production.
Integration Tests: Use testing libraries like Vitest or Jest to test CSRF validation logic in isolation. Mock cookies and headers to verify token generation, validation, and error handling work correctly.
End-to-End Tests: Tools like Playwright or Cypress can simulate complete user flows including form submissions and API calls. Test that legitimate requests succeed while forged requests are blocked.
Security Scanning: Automated security tools like OWASP ZAP or specialized CSRF scanners can identify potential bypasses in your implementation. Run these regularly as part of your security testing regimen.
// app/api/__tests__/csrf.test.ts - Example integration test
import { GET, POST } from '../route'
import { createMockRequest } from '@/test/utils'
describe('CSRF Protection', () => {
it('rejects requests without CSRF token', async () => {
const request = createMockRequest({
method: 'POST',
cookies: {},
headers: {},
})
const response = await POST(request)
expect(response.status).toBe(403)
})
it('accepts requests with valid CSRF token', async () => {
const validToken = 'valid-csrf-token-32-chars-long!'
const request = createMockRequest({
method: 'POST',
cookies: { csrf_token: validToken },
headers: { 'x-csrf-token': validToken },
})
const response = await POST(request)
expect(response.status).toBe(200)
})
})
Protect your application from CSRF and other security threats
Security Audits
Comprehensive security assessments identifying CSRF vulnerabilities and other threats in your Next.js application.
Custom Implementation
Tailored CSRF protection built specifically for your application architecture and requirements.
Ongoing Monitoring
Continuous security monitoring and alerts for potential attack attempts on your protected endpoints.
Developer Training
Team education on secure coding practices and CSRF prevention strategies.
Common Pitfalls and Best Practices
Even well-intentioned CSRF implementations can fail due to common mistakes. Understanding these pitfalls helps you avoid them in your own implementation.
Avoiding Common Mistakes
These critical errors undermine your CSRF protection and should be avoided:
-
Using GET requests for state-changing operations: GET requests can be triggered through images, links, and prefetching. Always use POST, PUT, or DELETE for operations that modify data.
-
Accepting CSRF tokens from query parameters: URLs are logged in browser history, server logs, and referrer headers. Tokens in URLs can leak and be exploited.
-
Storing tokens in localStorage: localStorage is accessible to any JavaScript on your page, making tokens vulnerable to XSS attacks. Use HTTP-only cookies instead.
-
Skipping validation for "internal" API routes: Any API route that modifies data needs CSRF protection. Attackers can craft requests from any origin.
-
Using predictable random number generators: Math.random() is not cryptographically secure. Use crypto.randomBytes() or equivalent secure random functions.
-
Not using constant-time comparison: String comparison exits early on mismatched characters, allowing timing attacks to infer token values byte by byte.
Best Practices Summary
Following these practices ensures robust CSRF protection for your Next.js application:
-
Always use SameSite cookies with Strict or Lax - Browser-level protection is your first line of defense.
-
Implement CSRF tokens for all state-changing operations - Application-level validation provides defense in depth.
-
Use cryptographically secure random token generation - Tokens must be unpredictable to attackers.
-
Validate tokens with constant-time comparison - Prevent timing attacks that could leak token values.
-
Skip CSRF for purely internal API calls with proper authentication - Exempt routes that can't be accessed externally.
-
Test your CSRF protection regularly - Automated and manual testing catches regressions and new vulnerabilities.
-
Monitor for CSRF attack attempts in logs - Failed validation attempts may indicate attack targeting.
Frequently Asked Questions
Sources
-
StackHawk: Node.js CSRF Protection Guide - Comprehensive coverage of CSRF attack patterns, token-based protection strategies, and implementation examples for Node.js applications
-
Clerk: Complete Authentication Guide for Next.js App Router - Modern Next.js authentication patterns including middleware security and vulnerability context
-
TurboStarter: Complete Next.js Security Guide 2025 - Next.js App Router security, Server Actions protection, and comprehensive security practices
-
Next.js: Data Security Guide - Official Next.js documentation on built-in security features and best practices