0

CSRF — Cross-Site Request Forgery, tokens, SameSite cookies

Intermediate5 min read·php-16-003
securityinterviewlaravel-src

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
<?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+)