highWeb Security

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

Vulnerableapp/api/transfer/route.ts
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 });
}
Fixedapp/api/transfer/route.ts
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.

Frequently Asked Questions

Are JSON APIs immune to CSRF?
Not entirely. While browsers restrict simple cross-origin requests with application/json Content-Type, attackers can use other techniques. Flash-based exploits, DNS rebinding, and misconfigured CORS policies can all bypass the Content-Type restriction. Explicit CSRF protection is still recommended.
Does SameSite=Lax protect against CSRF?
SameSite=Lax prevents cookies from being sent on cross-site POST requests, which blocks the most common CSRF attack vector. However, it still sends cookies on top-level GET navigations, so GET endpoints that perform state changes remain vulnerable. Never use GET requests for state-changing operations.
How do I implement CSRF protection in a Next.js app?
Generate a random token per session and store it server-side. Send the token to the client via a cookie or API response. On each state-changing request, require the client to send the token in a custom header (like x-csrf-token). Validate the token against the stored value before processing the request.

Related Security Topics

Check Your Code for This Vulnerability

Run a free scan to check if your site is affected by missing csrf protection allows unauthorized state-changing requests.