Skip to content

PHP Tutorial PHP Security

CSRF

Lesson 293 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

Cross-site request forgery (CSRF) tricks a logged-in user’s browser into sending a request to your site that the user never meant to send.

Browsers attach cookies automatically. So if you are logged in to shop.example, and you visit a harmful page that contains a hidden form pointing to shop.example/delete-account, your browser sends the request with your session cookie. The server sees a valid, logged-in request.

index.html
HTML
<!-- on the attacker's page -->
<form action="https://shop.example/transfer.php" method="post" id="f">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="500">
</form>
<script>document.getElementById("f").submit();</script>

The defence: a CSRF token

A token is a random secret that only your page knows. You put it in every form and check it on submit. The other site cannot read it (the browser’s same-origin rules stop that), so it cannot include a valid token.

index.php
PHP
<?php
function csrfToken(array &$session): string
{
    $session["csrf_token"] ??= bin2hex(random_bytes(32));

    return $session["csrf_token"];
}

function csrfValid(array $session, ?string $submitted): bool
{
    return isset($session["csrf_token"], $submitted)
        && hash_equals($session["csrf_token"], $submitted);
}

$session = [];

$token = csrfToken($session);       // put this in the form
echo strlen($token) . " characters\n";

var_dump(csrfValid($session, $token));         // the real form
var_dump(csrfValid($session, "forged-value")); // another site
var_dump(csrfValid($session, null));           // no token at all
Output
OUTPUT
64 characters
bool(true)
bool(false)
bool(false)

hash_equals() compares in constant time, which avoids timing attacks.

In the form and the handler

index.php
PHP
<form method="post" action="transfer.php">
  <input type="hidden" name="csrf_token" value="<?= h(csrfToken($_SESSION)) ?>">
  <!-- other fields -->
  <button type="submit">Send</button>
</form>
index.php
PHP
<?php
session_start();

if ($_SERVER["REQUEST_METHOD"] !== "POST") {
    http_response_code(405);
    exit;
}

if (!csrfValid($_SESSION, $_POST["csrf_token"] ?? null)) {
    http_response_code(403);
    exit("Invalid form token.");
}

// safe to continue

Rules

  1. Use POST (or PUT, DELETE) for anything that changes data, never GET.
  2. Check the token on every state-changing request: login, logout, delete, change password, settings.
  3. One token per session is fine. Tokens per form are stronger.
  4. Do not put the token in the URL.

SameSite cookies

The SameSite cookie attribute tells the browser not to send the cookie on many cross-site requests. With SameSite=Lax (the default in modern browsers) or Strict, the forged form above would arrive without the session cookie, so the user would not be logged in.

index.php
PHP
<?php
session_set_cookie_params(["samesite" => "Lax", "httponly" => true, "secure" => true]);
session_start();

Use both: SameSite for a strong default, and tokens as the real guarantee. SameSite alone has gaps (older browsers, Lax allows top-level GET navigation, subdomains).

Also check Origin

For extra safety on important actions, compare the Origin (or Referer) header with your own domain.

index.php
PHP
<?php
function originIsOurs(?string $origin, string $host): bool
{
    return $origin !== null && parse_url($origin, PHP_URL_HOST) === $host;
}

var_dump(originIsOurs("https://shop.example", "shop.example"));
var_dump(originIsOurs("https://evil.example", "shop.example"));
var_dump(originIsOurs(null, "shop.example"));
Output
OUTPUT
bool(true)
bool(false)
bool(false)

APIs

Browser-based APIs that use cookies need CSRF protection. APIs that use a token in an Authorization header (not a cookie) are not affected by CSRF in the same way, because the browser does not add that header by itself.

What does not stop CSRF

  • Checking that the request is POST (the attacker can send POST).
  • A secret cookie value (the browser attaches it automatically).
  • Keeping the URL secret.
info
Why this matters in WordPress and Laravel
WordPress calls the token a nonce (wp_nonce_field(), wp_verify_nonce()). Laravel adds @csrf to forms and checks it for you. Both do what you just built.