Rules Hub
Coding Rules Library
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Regenerate the session ID on login/logout to prevent session fixation
Rotate the session identifier on authentication and privilege elevation, and destroy it on logout; never reuse a pre-auth session ID.
Bad example
| 1 | app.post('/login', async (req, res) => { |
| 2 | const user = await authenticate(req.body.email, req.body.password); |
| 3 | if (!user) return res.status(401).send('invalid'); |
| 4 |
|
| 5 | // Reuses the SAME session id that existed before login |
| 6 | req.session.userId = user.id; |
| 7 | res.send('ok'); |
| 8 | }); |
| 9 |
|
| 10 | app.post('/logout', (req, res) => { |
| 11 | req.session.userId = undefined; // session row + id still alive |
| 12 | res.send('bye'); |
| 13 | }); |
Explanation (EN)
The authenticated state is attached to the session ID the client already had. An attacker who planted/knew that ID before login (session fixation) now holds a valid authenticated session. Logout that only clears userId leaves the session ID usable, so a captured cookie still works.
Objašnjenje (HR)
Autenticirano stanje veze se uz ID sesije koji je klijent vec imao. Napadac koji je taj ID podmetnuo ili znao prije prijave (session fixation) sada ima valjanu autenticiranu sesiju. Odjava koja samo brise userId ostavlja ID sesije iskoristivim pa uhvaceni kolacic i dalje radi.
Good example
| 1 | app.post('/login', async (req, res, next) => { |
| 2 | const user = await authenticate(req.body.email, req.body.password); |
| 3 | if (!user) return res.status(401).send('invalid'); |
| 4 |
|
| 5 | // New session id; old one is invalidated |
| 6 | req.session.regenerate((err) => { |
| 7 | if (err) return next(err); |
| 8 | req.session.userId = user.id; |
| 9 | res.send('ok'); |
| 10 | }); |
| 11 | }); |
| 12 |
|
| 13 | app.post('/logout', (req, res, next) => { |
| 14 | req.session.destroy((err) => { // remove server-side store + id |
| 15 | if (err) return next(err); |
| 16 | res.clearCookie('connect.sid'); |
| 17 | res.send('bye'); |
| 18 | }); |
| 19 | }); |
Explanation (EN)
regenerate() issues a fresh session ID at the moment of authentication, so any fixed pre-auth ID becomes worthless. destroy() removes the server-side session and the cookie on logout, so a stolen cookie cannot be replayed. The same rotation applies on any privilege elevation (e.g. step-up auth).
Objašnjenje (HR)
regenerate() izdaje novi ID sesije u trenutku autentikacije pa svaki podmetnuti pred-auth ID postaje bezvrijedan. destroy() pri odjavi uklanja sesiju na posluzitelju i kolacic pa se ukradeni kolacic ne moze ponovno iskoristiti. Ista rotacija vrijedi i pri svakom podizanju ovlasti (npr. step-up auth).
Notes (EN)
In NestJS/passport the same applies: call req.session.regenerate inside the login handler before persisting the user.
Bilješke (HR)
U NestJS-u/passportu vrijedi isto: pozovi req.session.regenerate unutar login handlera prije spremanja korisnika.
Exceptions / Tradeoffs (EN)
Stateless JWT auth has no server session to regenerate; the equivalent is issuing a brand-new token on login and invalidating the old one (rotate refresh tokens, bump a token version/jti, or use a short-lived access token). The principle — never carry a credential identifier across a privilege boundary — still holds.
Iznimke / Tradeoffi (HR)
Stateless JWT autentikacija nema serversku sesiju za regeneraciju; ekvivalent je izdavanje potpuno novog tokena pri prijavi i ponistavanje starog (rotiraj refresh tokene, povecaj verziju tokena/jti ili koristi kratkotrajni access token). Nacelo — nikad ne nositi identifikator vjerodajnice preko granice ovlasti — i dalje vrijedi.