Missing CSRF Protection Allows Unauthorized State-Changing Requests
What Is This Vulnerability?
Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks a user's browser into sending an authenticated request to your application. Without CSRF tokens or proper SameSite cookie configuration, any state-changing endpoint (transfers, password changes, account deletion) can be triggered by a third-party site while the user is logged in.
Why It Happens
APIs built as JSON endpoints often assume they are immune to CSRF because browsers do not send JSON Content-Type in simple cross-origin requests. However, attackers can use form submissions, fetch with no-cors mode, or other techniques to bypass this assumption. Single-page applications that rely solely on cookie-based authentication without CSRF tokens are especially vulnerable.
Example Code
import { NextResponse } from "next/server";
import { getSession } from "@/lib/auth";
export async function POST(request: Request) {
const session = await getSession();
const { to, amount } = await request.json();
// No CSRF token validation
await transferFunds(session.userId, to, amount);
return NextResponse.json({ success: true });
}import { NextResponse } from "next/server";
import { getSession } from "@/lib/auth";
import { validateCsrfToken } from "@/lib/csrf";
export async function POST(request: Request) {
const session = await getSession();
const csrfToken = request.headers.get("x-csrf-token");
if (!csrfToken || !validateCsrfToken(csrfToken, session.id)) {
return NextResponse.json(
{ error: "Invalid CSRF token" },
{ status: 403 },
);
}
const { to, amount } = await request.json();
await transferFunds(session.userId, to, amount);
return NextResponse.json({ success: true });
}How Hackers Exploit It
An attacker creates a page with a hidden form or JavaScript that submits a POST request to your API. When a logged-in user visits the attacker's page, their browser automatically includes session cookies with the request. The server cannot distinguish this forged request from a legitimate one, so it processes the action (e.g., transferring funds) without the user's consent.
How to Fix It
Implement the synchronizer token pattern: generate a unique CSRF token per session, embed it in forms or send it as a custom header, and validate it on every state-changing request. Alternatively, use SameSite=Strict cookies, which prevent the browser from sending cookies with cross-site requests. For defense in depth, combine both approaches.