iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Safe Demo Lab

A concrete walkthrough of a CSRF attack and the matching defences — a vulnerable route, an attacker page, the exploit, and the four defences that each kill it. Use it as the worked example when teaching the topic.

A vulnerable route + the four-layer defence

EXAMPLE
// ===== 1) The vulnerable shape =====
// A classic 'change email' endpoint that:
// - is GET (sigh)
// - requires a session cookie
// - has no token

// app/Http/Controllers/AccountController.php (UNSAFE)
class AccountController {
    public function setEmail(Request $req) {            // SHOULD BE POST
        $user = auth()->user();
        $user->email = $req->query('to');                // SHOULD VALIDATE
        $user->save();
        return 'ok';                                      // SHOULD CSRF-VERIFY
    }
}

// routes/web.php (UNSAFE)
Route::get('/account/email', [AccountController::class, 'setEmail']);

// ===== 2) The attacker page =====
// Hosted on attacker.example. Loaded by a logged-in victim.

// <!-- attacker.example/dashboard.html -->
// <!doctype html>
// <html>
// <body>
//   <h1>Funny cats</h1>
//   <!-- The image triggers a GET to the victim's email-change route.
//        The browser attaches the session cookie automatically. -->
//   <img src='https://shop.example/account/email?to=attacker@evil.test' style='display:none'>
// </body>
// </html>

// ===== 3) Four defences, each of which alone kills this exploit =====

// 3a) Change to POST + protect with CSRF token
//
// routes/web.php
// Route::post('/account/email', [AccountController::class, 'setEmail'])
//      ->middleware('auth');
//
// Blade form
// <form method='POST' action='/account/email'>
//   @csrf
//   <input name='to' type='email'>
//   <button>Update</button>
// </form>
//
// The <img src=> POST attempt by the attacker cannot send a POST with a body,
// and even if it could, it lacks the per-session @csrf token.

// 3b) Same-site cookie on the session
// config/session.php
// 'same_site' => 'lax',           // Lax already blocks cross-site cookies on POST
// 'secure'    => true,
// 'http_only' => true,
//
// The <img src=> request comes from attacker.example, so the browser does
// NOT attach the SameSite=Lax session cookie. The request hits the server
// unauthenticated and 401s.

// 3c) Origin / Referer check (belt-and-braces)
// app/Http/Middleware/ValidateOrigin.php
// public function handle($req, $next) {
//     if (in_array($req->method(), ['POST','PUT','PATCH','DELETE'])) {
//         $expected = config('app.url');
//         $origin   = $req->header('Origin') ?: $req->header('Referer');
//         if (!$origin || !str_starts_with($origin, $expected)) abort(403);
//     }
//     return $next($req);
// }
//
// A POST from attacker.example carries Origin: https://attacker.example,
// the check fails, 403.

// 3d) Custom required header (bearer-token APIs)
// If the API is JSON + Authorization: Bearer:
// - The browser cannot attach a custom header on a cross-origin simple form
//   submission (it would trigger a CORS preflight that the attacker's site
//   has not configured for the target).
// - The endpoint refuses requests without the Authorization header.
// - No CSRF token needed for THIS endpoint shape.

// ===== 4) Summary table =====
// Cookie auth  + Web app  -> POST + @csrf + SameSite=Lax + Origin check
// Cookie auth  + SPA      -> double-submit XSRF-TOKEN cookie + X-XSRF-TOKEN header
//                            (Laravel, Angular, Django all do this)
// Bearer auth  + any      -> require Authorization: Bearer header; CSRF impossible
// Webhook      + signed   -> verify signature header; bypass CSRF middleware

// ===== 5) The XSS interaction =====
// XSS defeats every CSRF defence because the attacker runs JS as the victim.
// CSRF defences are NOT a substitute for XSS protection; they are an
// in-depth layer that catches the simpler 'invisible POST' attacks before
// they reach an authenticated session.

Why it matters

The combination that makes CSRF essentially impossible: state-changing routes are POST/PUT/PATCH/DELETE, every cookie carries SameSite=Lax (or Strict for admin), the framework default CSRF token middleware is enabled, and origin checks run on writes. None of those alone is overkill; together they remove most of the surface even if a future PR forgets one.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
<!-- ALWAYS demo CSRF in a sandboxed lab against an app you own. -->
<!-- index.html on attacker.test -->
<form action="http://victim.test/transfer" method="POST">
    <input name="to" value="attacker">
    <input name="amount" value="1">
    <button>Click me</button>
</form>
Try it Yourself »

Discussion

Loading…