0

CSRF — Cross-Site Request Forgery: tricking a browser into forged requests

Beginner5 min read·eng-19-004
interviewsecurity

Concept

CSRF (Cross-Site Request Forgery) — an attack where a malicious website tricks a user's browser into making an authenticated request to your application without the user's knowledge.

How it works:

  1. User logs in to bank.com. Browser has session cookie.
  2. User visits evil.com.
  3. evil.com has <form action="https://bank.com/transfer" method="POST"><input name="amount" value="1000"><input name="to" value="attacker"></form> that auto-submits with JavaScript.
  4. The browser sends the POST request to bank.com WITH the session cookie (cookies are always sent to their domain).
  5. bank.com sees a valid session cookie and processes the transfer.

The attack exploits: Browsers automatically include cookies with cross-origin requests. The server can't tell if the request came from the real user or from an attacker's page.

Prevention — CSRF Token:

  1. Server generates a random, unpredictable token per session.
  2. Token is embedded in forms (hidden field) or sent in a header.
  3. On each state-changing request (POST/PUT/DELETE), server verifies the token.
  4. evil.com cannot read the token (same-origin policy) so can't forge the request.

In Laravel:

  • App\Http\Middleware\VerifyCsrfToken runs automatically for all stateful routes.
  • @csrf Blade directive adds <input type="hidden" name="_token" value="...">.
  • AJAX: send token in X-CSRF-TOKEN header.
  • APIs using tokens (Sanctum with Authorization: Bearer) are NOT vulnerable — no cookies involved.

SameSite cookies: Modern defense. SameSite=Strict or SameSite=Lax cookie attribute tells browsers not to send cookies with cross-site requests. Laravel sets SameSite=Lax by default.

Code Example

php
<?php
// CSRF PROTECTION in Laravel

// 1. In HTML forms — @csrf directive adds hidden input
// resources/views/transfer.blade.php:
// <form method="POST" action="/transfer">
//     @csrf
//     <input type="hidden" name="amount" value="1000">
//     <button type="submit">Transfer</button>
// </form>
// Renders: <input type="hidden" name="_token" value="abc123xyz...">

// 2. AJAX — send token in header
// resources/js/bootstrap.js (auto-configured in Laravel):
// axios.defaults.headers.common['X-CSRF-TOKEN'] =
//     document.querySelector('meta[name="csrf-token"]').content;
//
// Or via meta tag in layout:
// <meta name="csrf-token" content="{{ csrf_token() }}">

// 3. Verifying token manually
if (!hash_equals(session()->token(), $request->input('_token'))) {
    abort(419); // TokenMismatchException
}

// 4. Excluding routes from CSRF (webhooks from external services)
// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
    protected $except = [
        'webhooks/stripe',   // Stripe sends POST with no browser session
        'webhooks/github',
    ];
}
// Better: verify Stripe signature instead of CSRF token
Route::post('/webhooks/stripe', function (Request $request) {
    $signature = $request->header('Stripe-Signature');
    \Stripe\Webhook::constructEvent(
        $request->getContent(),
        $signature,
        config('services.stripe.webhook_secret')
    );
});

// 5. API routes — stateless (Sanctum Bearer tokens, not cookies → no CSRF needed)
// routes/api.php — CSRF middleware NOT applied to these routes by default
Route::middleware('auth:sanctum')->post('/orders', [OrderController::class, 'store']);
// Bearer token in Authorization header can't be forced by evil.com