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:
- User logs in to
bank.com. Browser has session cookie. - User visits
evil.com. evil.comhas<form action="https://bank.com/transfer" method="POST"><input name="amount" value="1000"><input name="to" value="attacker"></form>that auto-submits with JavaScript.- The browser sends the POST request to
bank.comWITH the session cookie (cookies are always sent to their domain). bank.comsees 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:
- Server generates a random, unpredictable token per session.
- Token is embedded in forms (hidden field) or sent in a header.
- On each state-changing request (POST/PUT/DELETE), server verifies the token.
evil.comcannot read the token (same-origin policy) so can't forge the request.
In Laravel:
App\Http\Middleware\VerifyCsrfTokenruns automatically for all stateful routes.@csrfBlade directive adds<input type="hidden" name="_token" value="...">.- AJAX: send token in
X-CSRF-TOKENheader. - 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