An email/password login. After authentication, the user id is stored in the session. The line is missing one call — `regenerate` — that distinguishes a safe login from a session-fixation vulnerability.
The vulnerable diff
// auth/login.ts
authRouter.post("/login", async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: "bad credentials" });
// BUG: session id unchanged from pre-login state
req.session.userId = user.id;
res.json({ ok: true });
});What is wrong
Session fixation works on a simple principle: if the attacker can plant a session id in the victim's browser before the victim logs in, and the server keeps that same session id after authentication, the attacker now shares the authenticated session. The fix is to discard the pre-auth session id and generate a fresh one at the moment of successful login. This is CWE-384 and is one of the OWASP Session Management Cheat Sheet's flagship rules.
The attack
The most common planting vector is a subdomain cookie attack — `evil.example.com` can set a cookie that the browser sends to `app.example.com` if the cookie domain is `.example.com`. Other vectors: physical access to a public terminal, URL parameter session ids on legacy apps, XSS that sets `document.cookie`.
1. Attacker plants known session id on victim's browser
(subdomain cookie / URL param / shared terminal).
2. Victim visits app.example.com, sees the login page, enters
credentials. Server validates, sets userId on the session.
3. Server does NOT regenerate the session id. The attacker, who
already knew the id from step 1, is now logged in as the victim.
4. Both browsers share the same authenticated session until logout.Detection is hard — there is no failed login, no anomaly. The victim sees a normal session. The only signal is parallel activity from multiple IPs against the same session id, which not all teams alert on.
Mesrai's review comment
mesraipilot · Bot · reviewed 90 sec ago
[mesrai] [code-review] [Security] [CWE-384] [high]
This login flow does not regenerate the session id after successful
authentication. Session fixation is the resulting attack class.
Fix: discard the pre-auth session and generate a fresh one before
setting `userId`. With express-session:
await new Promise<void>((resolve, reject) =>
req.session.regenerate(err => err ? reject(err) : resolve())
);
req.session.userId = user.id;
For other session libraries the equivalent calls are `req.session_regenerate_id`
(PHP), `request.session.cycle_key()` (Django), `session.invalidate()`
followed by a new session (Spring).
Reference: OWASP Session Management Cheat Sheet § Session ID
Regeneration, CWE-384The fix
// auth/login.ts — fixed
authRouter.post("/login", async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
if (!user) return res.status(401).json({ error: "bad credentials" });
// Regenerate session id — discards any pre-login attacker-planted id
await new Promise<void>((resolve, reject) =>
req.session.regenerate(err => err ? reject(err) : resolve())
);
req.session.userId = user.id;
res.json({ ok: true });
});The `regenerate` call wipes the existing session, creates a fresh id, and re-sets the cookie. The pre-auth id is now invalid; any attacker holding it is locked out. This is the canonical session-fixation defense for any session library that exposes a regenerate primitive.
Why human review missed it
Session fixation is one of those bugs that only makes sense if you have studied the attack class — the code reads as 'I set the user id on the session,' which sounds correct. Reviewers who have not internalized the regenerate-on-login rule trust the framework defaults. Mesrai's rule pack catches every login handler that writes user identity to a session without first regenerating the id.
Related rules + further reading
Mesrai rule pack: security/session-fixation — flags login handlers without session regeneration.
OWASP: Session Management Cheat Sheet § Session ID Regeneration.
CWE-384 — Session Fixation.
Takeaway
Regenerate on login. One line. Closes the entire fixation class.