CSRF — Cross-Site Request Forgery, tokens, SameSite cookies
Concept
Cross-Site Request Forgery (CSRF) tricks authenticated users into making unintended requests to a web application. Because browsers automatically send cookies with every request to a domain, an attacker can host a page that submits a form to your app — the victim's session cookie is included automatically.
Example attack: Victim is logged into bank.com. Attacker's page contains <form action="https://bank.com/transfer" method="POST"><input name="amount" value="10000"><input name="to" value="attacker">. When the victim visits the attacker's page, their browser submits the form with their bank session cookie.
CSRF token protection: The server generates a unique, secret, per-session token that's embedded in forms. When the form is submitted, the server verifies the token matches. An attacker on a different origin can't read the token (same-origin policy), so they can't forge a valid request.
Laravel's CSRF protection: The VerifyCsrfToken middleware checks all POST, PUT, PATCH, DELETE requests for a valid _token field or X-CSRF-TOKEN header. Token is available as csrf_token() helper and {{ csrf_field() }} blade directive. SPA/API routes use X-CSRF-TOKEN header or X-XSRF-TOKEN (set from the XSRF-TOKEN cookie by axios/axios defaults).
SameSite cookie attribute: A defense-in-depth mechanism. SameSite=Strict — cookie not sent on cross-site requests at all. SameSite=Lax (browser default) — cookie not sent for cross-site POST/PUT/DELETE (sent for GET navigation). SameSite=None; Secure — sent on all cross-site requests (required for embedded iframes, third-party contexts).
Code Example
<?php
// Blade template — CSRF token in form
// {{ csrf_field() }} or @csrf
?>
<form method="POST" action="/orders">
@csrf
<!-- Expands to: <input type="hidden" name="_token" value="RANDOM_TOKEN"> -->
<button type="submit">Place Order</button>
</form>
<?php
// Manual CSRF verification (outside Laravel)
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['_token'] ?? '';
$sessionToken = $_SESSION['csrf_token'] ?? '';
if (empty($token) || !hash_equals($sessionToken, $token)) {
http_response_code(403);
die("CSRF token mismatch");
}
}
// Generate token (store in session)
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// Laravel — exclude routes from CSRF (e.g., webhooks from external services)
// In app/Http/Middleware/VerifyCsrfToken.php:
class VerifyCsrfToken extends \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken
{
protected $except = [
'webhooks/*', // Stripe/GitHub webhooks come from external servers
'stripe/webhook',
];
}
// Laravel API routes use Sanctum tokens instead of CSRF
// config/sanctum.php or config/auth.php for stateless APIs
// axios (frontend) — auto-sends XSRF-TOKEN cookie as header
// axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content;
// OR: axios.defaults.withCredentials = true; (sends XSRF-TOKEN cookie automatically)
// Cookie flags for defense in depth
ini_set('session.cookie_httponly', '1'); // JS can't read session cookie
ini_set('session.cookie_secure', '1'); // HTTPS only
ini_set('session.cookie_samesite', 'Lax'); // SameSite=Lax (default in PHP 8+)