Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

CSRF Protection from Scratch: What the Frameworks Are Doing for You

C
Carlo Fontanos
· 2 min read

The attack first, because it explains every design choice: a user is logged into yoursite.com; they visit evil.com, which contains <form action="https://yoursite.com/transfer" method="POST"> and auto-submits it. The browser attaches yoursite's session cookie automatically - that's what cookies do - and your server sees a perfectly authenticated request the user never intended. Cross-Site Request Forgery: the attacker rides the victim's session.

The defense: require proof that the request came from a page you served - a secret evil.com can't read (same-origin policy blocks it) and therefore can't include.

The server side (whole thing)

function csrf_token(): string
{
    return $_SESSION['csrf'] ??= bin2hex(random_bytes(32));
}

function csrf_field(): string
{
    return '<input type="hidden" name="_token" value="' . csrf_token() . '">';
}

function csrf_verify(): void
{
    $sent = $_POST['_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
    if (!hash_equals($_SESSION['csrf'] ?? '', $sent)) {
        http_response_code(419);
        exit('Request expired - please reload and try again.');
    }
}

// Every state-changing endpoint, first line:
csrf_verify();

Design choices decoded: random_bytes for unguessability (not Math.random-grade randomness); one token per session (per-request tokens break back-button and multi-tab flows for marginal gain); hash_equals against timing leaks; checking POST body or header so both forms and AJAX use one verifier; 419 because that's the "page expired" convention users' frameworks trained them on.

The client side

<form method="POST" action="/settings"><?= csrf_field() ?> ...</form>
<meta name="csrf-token" content="<?= csrf_token() ?>">
const token = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/save', {
    method: 'POST',
    headers: { 'X-CSRF-TOKEN': token, 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
});
// (jQuery shops: set it once in $.ajaxSetup and forget it)

The second layer and the exemptions

SameSite=Lax cookies independently block the auto-submitted-form attack in modern browsers - run both layers; defense in depth is cheap here. And know the legitimate exemptions: webhook endpoints (Stripe can't read your token - they authenticate by signature instead) and true public APIs using token auth. Everything else that changes state - including "harmless" logout and preference endpoints - verifies. GET handlers, meanwhile, must never change state, or no token scheme can save you.

Rotate the token on login (session_regenerate_id time), keep it out of URLs (referrers leak), and that's genuinely the whole discipline. Thirty lines - and the next time Laravel throws a 419 at you, you'll know precisely which line of this you violated.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading