← Back to PHP Course | Chapter 10: Forms & Validation | Lesson 5 of 8

PHP CSRF Protection

CSRF is a trick where a bad site makes your browser secretly send a request while you are logged in elsewhere. A protection token is like a secret stamp on real forms so the site can spot fakes.
Syntax
php
session_start();
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));

// in the form:
// <input type="hidden" name="csrf_token" value="<?= $_SESSION["csrf_token"] ?>">

if (hash_equals($_SESSION["csrf_token"], $_POST["csrf_token"])) {
    // request is genuine
}

What is CSRF?

Cross-Site Request Forgery tricks a logged-in user's browser into submitting a request to your site without their knowledge, by embedding a malicious form or link on another page they happen to visit.

Example: What is CSRF?

php
<?php
// A malicious page could auto-submit this hidden form using the victim's session
echo '<form action="https://bank.example.com/transfer" method="POST">
  <input type="hidden" name="amount" value="1000">
</form>';
?>

Generating a CSRF Token

A CSRF token is a random, unpredictable value generated per session (or per form) and embedded as a hidden form field, which your server then verifies matches on submission — an attacker's forged request has no way to know this value.

Example: Generating a CSRF Token

php
<?php
// Call `session_start()`
session_start();
// Set `$_SESSION['csrf_token']` to `bin2hex(random_bytes(16))`
$_SESSION['csrf_token'] = bin2hex(random_bytes(16));
// Print `$_SESSION['csrf_token']` to the output
echo $_SESSION['csrf_token'];
?>

Adding Tokens to Forms

Generate the token with a cryptographically secure function like random_bytes(), store it in $_SESSION, and compare it using hash_equals() on submission to avoid timing-attack vulnerabilities in the comparison itself.

Example: Adding Tokens to Forms

php
<?php
// Call `session_start()`
session_start();
// Set `$_SESSION['csrf_token']` to `bin2hex(random_bytes(16))`
$_SESSION['csrf_token'] = bin2hex(random_bytes(16));
// Print '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">' to the output
echo '<input type="hidden" name="csrf_token" value="' . $_SESSION['csrf_token'] . '">';
?>

Verifying the CSRF Token

CSRF protection matters specifically for state-changing requests (anything that modifies data, like a password change or a purchase) — read-only GET requests are typically not the concern this defense addresses.

Example: Verifying the CSRF Token

php
<?php
// Call `session_start()`
session_start();
// Set `$_SESSION['csrf_token']` to "abc123"
$_SESSION['csrf_token'] = "abc123";
// Set `$_POST['csrf_token']` to "abc123"
$_POST['csrf_token'] = "abc123";
// Check whether `hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])`
if (hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
    // Print "Token valid -- proceed with the state-changing action" to the output
    echo "Token valid -- proceed with the state-changing action";
// Otherwise, run this branch
} else {
    // Print "Token mismatch -- reject request" to the output
    echo "Token mismatch -- reject request";
}
?>

Clearing Used Tokens

Most PHP frameworks like Laravel and Symfony generate and verify CSRF tokens automatically for you, which is one of the strong reasons to prefer a framework's form helpers over hand-rolling raw HTML forms for anything that changes data.

Example: Clearing Used Tokens

php
<?php
// Call `session_start()`
session_start();
// Set `$_SESSION['csrf_token']` to "abc123"
$_SESSION['csrf_token'] = "abc123";
// Remove `$_SESSION['csrf_token']`
unset($_SESSION['csrf_token']);
// Print `isset($_SESSION['csrf_token']) ? "Still set" : "Token cleared after use"` to the output
echo isset($_SESSION['csrf_token']) ? "Still set" : "Token cleared after use";
?>
Related Topics
{# common_mistakes/chapter_summary/browser_support: on Hindi pages the view already swaps in the hi_ translation fields (or blanks these out if untranslated), so this renders correctly for both languages without a lang_code check here. #}
Common Mistakes
  1. Using a predictable token instead of random_bytes or bin2hex(random_bytes(32)).
  2. Comparing tokens with ==, when hash_equals avoids timing attacks.
  3. Forgetting to check the token on the server for POST requests.
🔒

Chapter Quiz — Complete all 8 topics to unlock

0/8 topics done

Complete these topics first:

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.