MENU
CSRF Attacks
Cross-Site Request Forgery (CSRF) tricks a logged-in user's own browser into sending a request to your site that they never intended to make. The attack doesn't need to read anything back — it just needs the browser to attach the user's session cookie automatically, the way browsers do for same-site as well as many cross-site requests by default. A malicious page that auto-submits a form to https://your-app.com/api/transfer-funds is enough, if that endpoint trusts the cookie alone as proof of intent.
Server Actions have built-in CSRF protection
Server Actions in Next.js are not a plain, generic HTTP endpoint you'd need to protect by hand — the framework builds several defenses in:
- Every Server Action is invoked exclusively over POST, and combined with a SameSite session cookie (see Session Management), that alone blocks most cross-site forgery attempts in modern browsers.
- Next.js compares the request's Origin header against the Host (or X-Forwarded-Host) header on every Server Action invocation, and rejects the request outright if they don't match. A form hosted on an attacker's domain, submitting to your Server Action, arrives with an Origin that doesn't match your app's host — and gets aborted before your action code ever runs.
- Each Server Action is addressed by an encrypted, build-specific id rather than a guessable, stable URL, which makes it harder to target blind.
This same-origin check is on by default and isn't something you opt into. If you do run behind a reverse proxy or a multi-layered backend where the apparent origin legitimately differs from the app's own host, Next.js exposes serverActions.allowedOrigins in next.config.js to list additional trusted origins explicitly, rather than disabling the check.
Route Handlers do not get this automatically
A route.ts file under app/api is a plain HTTP endpoint. Unlike a Server Action, it does not get Next.js's automatic Origin/Host comparison — nothing stops a GET or a form-encoded POST from another site reaching it, cookies included, unless you've protected it yourself. Any Route Handler that changes state (creates, updates, or deletes something) and relies on a cookie for authentication needs its own CSRF protection, exactly like a hand-written API in any other framework would.
Mitigations for Route Handlers and other state-changing endpoints
- SameSite cookies: setting the session cookie's sameSite attribute to lax or strict (see Session Management) means the browser simply won't attach it to most cross-site requests in the first place, which blocks the majority of CSRF attempts before any application code runs.
- Check the Origin (or Referer) header yourself: for a state-changing Route Handler, compare the incoming Origin header against your app's own host and reject anything that doesn't match, mirroring what Server Actions do automatically.
- CSRF tokens: generate a random, unpredictable token per session (or per form), embed it in the page, and require the client to send it back with the request — typically as a header or hidden field, verified against the value stored server-side. This is the traditional defense and remains useful for endpoints that must accept cross-origin requests in some form, where SameSite alone isn't a complete answer.
- Avoid treating a GET request as state-changing: browsers will follow links, prefetch, and load images without any user intent to submit a form, so a GET endpoint that mutates data is trivially forgeable and should be redesigned to require POST at minimum.
app,api,account,delete,route.ts:
import { NextResponse } from 'next/server'
import { requireUser } from '@/lib/auth'
import { db } from '@/lib/db'
const ALLOWED_ORIGIN = process.env.APP_ORIGIN // e.g. "https://your-app.com"
export async function POST(request: Request) {
const origin = request.headers.get('origin')
if (origin !== ALLOWED_ORIGIN) {
return NextResponse.json({ error: 'Invalid origin.' }, { status: 403 })
}
const user = await requireUser()
await db.user.delete({ where: { id: user.id } })
return NextResponse.json({ ok: true })
}None of these mitigations are exclusive — a well-defended endpoint typically relies on SameSite cookies as the baseline, with an explicit Origin check or a token as a second layer for anything that changes state.