SameSite Lax Bypass
debt(d8/e5/b5/t7)
Closest to 'silent in production' (d8); owasp-zap and semgrep can flag GET-based state changes but won't reliably detect Lax-bypass scenarios — mostly requires manual review and threat modeling, so slightly better than d9.
Closest to 'touches multiple files / significant refactor in one component' (e5); quick_fix says to add CSRF tokens to state-changing requests and audit GET endpoints — this means refactoring routes (GET→POST), adding token middleware, and updating forms across the auth/action surface.
Closest to 'persistent productivity tax' (b5); applies to all web/api contexts and means every state-changing endpoint must be audited for method + CSRF token, shaping how routes and forms are written going forward.
Closest to 'serious trap' (t7); misconception explicitly states devs believe Lax is sufficient CSRF protection, which contradicts the reality that GET navigations, OAuth callbacks, and the Lax+POST window all bypass it — the 'obvious' defense is wrong in multiple documented ways.
Also Known As
TL;DR
Explanation
SameSite=Lax cookies are sent on cross-site top-level GET navigations (clicking a link, window.location redirect) but not on POST or iframe requests. This prevents classic CSRF but leaves a gap: if your application accepts state-changing GET requests (a logout link, a one-click delete, a token-based action URL), Lax doesn't protect them from CSRF. Additionally, some browsers apply a 2-minute 'Lax+POST' grace period for cookies without an explicit SameSite attribute — exploitable in some flows. Defences: use SameSite=Strict for sensitive session cookies where cross-site navigation isn't required; avoid state-changing GET endpoints; use CSRF tokens as a defence-in-depth layer even alongside SameSite.
Common Misconception
Why It Matters
Common Mistakes
- Using SameSite=Lax as the sole CSRF defence without auditing GET-based state changes.
- Login CSRF — if login itself is a GET (e.g. OAuth callback), Lax does not protect the session cookie.
- Assuming Lax protects against all CSRF — Strict is needed for any sensitive GET-triggered actions.
- Not combining SameSite with explicit CSRF tokens for high-security forms — defence in depth.
Code Examples
// GET-based logout — CSRF-able even with SameSite=Lax:
<a href="/logout">Logout</a>
// Attacker's page: <img src="https://victim.com/logout">
// Browser follows img src as top-level GET — Lax cookie sent — user logged out
// Fix: logout must be POST with CSRF token, not GET
// SameSite=Lax still sends cookies on top-level GET navigations
// Protect GET endpoints that change state:
// Bad: state-changing GET (bypasses SameSite=Lax)
// Route::get('/logout', [AuthController::class, 'logout']);
// Good: state changes via POST (SameSite=Lax blocks cross-site POST)
Route::post('/logout', [AuthController::class, 'logout']);
// Add CSRF token as an additional layer:
// <form method='POST'><input name='_token' value='{{ csrf_token() }}'>
// Highest security — use SameSite=Strict:
ini_set('session.cookie_samesite', 'Strict');
// Tradeoff: breaks auth on cross-site navigations (OAuth, email links)