PHP cookbook · Beginner
Add CSRF protection to PHP forms
Prevent cross-site request forgery on session-authenticated forms without a framework.
Approach
Generate a random token per session, embed it in every state-changing form (and an X-CSRF-Token header for AJAX), and verify it with hash_equals() on POST/PUT/PATCH/DELETE. Combine with SameSite=Lax session cookies and never change state on GET.
Solution
<?php
declare(strict_types=1);
session_start();
function csrf_token(): string
{
return $_SESSION['_csrf'] ??= bin2hex(random_bytes(32));
}
function verify_csrf(): void
{
if (in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD', 'OPTIONS'], true)) {
return;
}
$token = $_POST['_csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!is_string($token) || !hash_equals($_SESSION['_csrf'] ?? '', $token)) {
http_response_code(419);
exit('Page expired. Please reload and try again.');
}
}
verify_csrf();
?>
<form method="post" action="/profile">
<input type="hidden" name="_csrf" value="<?= htmlspecialchars(csrf_token(), ENT_QUOTES) ?>">
<input name="display_name">
<button>Save</button>
</form>
<meta name="csrf-token" content="<?= htmlspecialchars(csrf_token(), ENT_QUOTES) ?>">
Pitfalls
- Comparing tokens with
==or===(timing leaks) instead ofhash_equals. - Exposing tokens in URLs (logs, Referer).
- Skipping CSRF checks for AJAX endpoints that use cookies.
Variations
- Per-form tokens for extra-sensitive actions.
- Frameworks: Laravel's
@csrf, Symfony'scsrf_token()— keep them enabled.
Further reading: OWASP CSRF Prevention Cheat Sheet
More PHP recipes
- Add health check endpoints to a PHP app
- Add structured logging with request IDs in PHP
- Build a secure password reset flow in PHP
- Cache expensive queries with Redis in PHP
- Call external APIs with timeouts and retries in PHP
- Handle file uploads securely in PHP
- Implement rate limiting in a PHP API
- Make a PHP payment endpoint idempotent