After the last two lessons the Hacker set the session cookie to SameSite=Lax, so cross-site requests no longer carry the session. But an old GET route still changes the recovery email, and SameSite=Lax does send the cookie on a top-level GET navigation. The leader is curious and opens links sent to him.
The same Ravens control panel, but the session cookie is now SameSite=Lax, so it is not attached on a cross-site POST or a background sub-resource load, and the state change here is over GET. Think which delivery both carries the cookie and is accepted. You pick how the forged request is delivered from the attacker page: an auto-submitting form (POST), an image that loads a URL (GET), or a link the leader opens himself (GET). The app shows whether the browser attached the session cookie, and why.
Change the leader’s recovery email to yours despite the SameSite=Lax protection.
A top-level GET navigation carried the Lax cookie and changed the leader’s email.
Why the solution worked
SameSite=Lax is good protection but not complete: it still sends the cookie on a top-level GET navigation, which is enough when a state-changing action exists on GET. Two mistakes met: a dangerous action on GET and a Lax (not Strict) cookie. The fix: never change state on GET, use SameSite=Strict for sensitive sessions, and keep the CSRF token — the layers complete each other.
The root cause
SameSite=Lax reduces CSRF but does not stop it while a state-changing action is reachable via a top-level GET navigation, because Lax attaches the cookie in exactly that case.
How the code is fixed
Lax is not a complete shield against CSRF; it is one layer. The real shield is never changing state on GET, plus a CSRF token, and perhaps Strict.
Vulnerable code
// Dangerous: it changes state on GET, which Lax still allows cross-site via a top-level link.
app.get('/settings', (req, res) => {
if (!req.session.user) return res.status(401).end();
db.setRecoveryEmail(req.session.user, req.query.email);
res.end('updated');
});
// Set-Cookie: session=...; SameSite=Lax
Safe code
// State changes are POST + token; the cookie is Strict for a sensitive panel.
app.post('/settings', requireToken, (req, res) => {
db.setRecoveryEmail(req.session.user, req.body.email);
res.end('updated');
});
// Set-Cookie: session=...; SameSite=Strict; HttpOnly; Secure
Impact in the real world
Relying on SameSite alone leaves a hole in every state-changing GET action, a common mistake when a cookie setting is treated as a substitute for a token.
How defenders detect it
Defenders combine SameSite with a CSRF token, forbid GET from changing state, and use SameSite=Strict or Lax plus an Origin check.