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:

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

A Route Handler that performs a sensitive action and, since it gets none of the Server Action protections, manually verifies the Origin header before proceeding.
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.