Session Fixation
Also known as: Session fixation, Session ID fixation, CWE-384
A web flaw in which an application keeps the same session identifier across the login boundary, so an identifier known to someone else before sign-in becomes an authenticated session once the user logs in.
How it works
Session fixation is a flaw in how an application manages session identifiers. The application hands a visitor a session id before they sign in, and then keeps using that same id after authentication succeeds. Any party who already knew that id before login now holds an identifier that the server treats as the signed-in user.
The weakness needs two conditions. The application issues, or accepts, a session id before authentication. And the login step does not replace it: the id on the request before login is the id on the session afterwards. Accepting an id supplied in a URL or other client-controlled place makes the first condition easier to meet, which is why identifiers must never travel in URLs.
The defence is to make the pre-login identifier worthless. On every privilege change, such as login or step-up authentication, the server creates a brand-new session id, copies over only the data that should survive, and invalidates the old session. Cookies should carry Secure, HttpOnly and SameSite attributes, and the server should only accept identifiers it generated itself.
Defence in depth supports this. Bind sessions to additional context where practical, enforce idle and absolute timeouts, and destroy the session fully on logout. A reviewer's quickest test is to compare the session id before and after authentication: if the value is unchanged, the finding stands.
Walk through it
- 1Review the login handler
- 2Name the missing control
- 3Check where ids are accepted from
- Rotate on every privilege change
- Harden and verify the fix
A pre-release review flags the sign-in route. Read the handler as a reviewer and note what it does with the session identifier when the password check succeeds.
1// BEFORE: id is kept across the login boundary2router.post("/login", async (req, res) => {3 const user = await auth.verify(req.body.user, req.body.pass);4 if (!user) return res.status(401).render("login");5 req.session.userId = user.id; // same session id as before login6 res.redirect("/account");7});8 9// AFTER: a fresh id is issued when privilege changes10router.post("/login", async (req, res) => {11 const user = await auth.verify(req.body.user, req.body.pass);12 if (!user) return res.status(401).render("login");13 req.session.regenerate((err) => { // new id, old session destroyed14 if (err) return res.status(500).end();15 req.session.userId = user.id;16 res.redirect("/account");17 });18});Spot it
- The same session identifier observed on a request before authentication and on requests after authentication.
- Session identifiers appearing in URLs, query strings or referrer headers in access logs.
- A session first seen from one client context that later authenticates from a different one without a new id being issued.
- Login responses that do not set a new session cookie when authentication succeeds.
- A code or configuration review that finds no session regeneration call in the login or step-up path.
Application access log
ts=2026-10-11T08:02:11Z method=GET path=/login sid=a41fc9 user=anon status=200
ts=2026-10-11T08:03:40Z method=POST path=/login sid=a41fc9 user=u_2210 status=302 sid_rotated=false
ts=2026-10-11T08:03:41Z method=GET path=/account sid=a41fc9 user=u_2210 status=200Reverse proxy
ts=2026-10-11T08:05:02Z method=GET uri=/account?sid=a41fc9 status=200 flag=session-id-in-urlsplSplunk: sessions that authenticate without rotating the id
index=app sourcetype=access path=/login method=POST status=302
| where sid_rotated="false"
| stats count by user, sidDepends on the application logging whether the id changed; add that field if it is absent.
grepgrep: session identifiers carried in URLs
grep -E '[?&](sid|sessionid|phpsessid|jsessionid)=' access.logStop it
Regenerate the session id on every privilege change
Issue a new identifier at login and at step-up authentication, carry over only the data that should survive, and invalidate the old session on the server so the earlier value is dead.
Never accept session ids from the URL, and harden the cookie
Take identifiers only from a cookie the server set. Mark it Secure, HttpOnly and SameSite, use a framework-generated high-entropy value, and reject any identifier the server did not itself issue.
Bind sessions and bound their lifetime
Apply idle and absolute timeouts, bind the session to additional context where it will not break legitimate use, and destroy the session completely on logout so it cannot be replayed.
Regenerate the session when the user authenticates
Vulnerable
router.post("/login", async (req, res) => {
const user = await auth.verify(req.body.user, req.body.pass);
if (!user) return res.status(401).end();
req.session.userId = user.id;
res.redirect("/account");
});Hardened
router.post("/login", async (req, res) => {
const user = await auth.verify(req.body.user, req.body.pass);
if (!user) return res.status(401).end();
req.session.regenerate((err) => {
if (err) return res.status(500).end();
req.session.userId = user.id;
req.session.authTime = Date.now();
res.redirect("/account");
});
});The same pattern applies to step-up authentication and any role change. Use your framework's built-in regenerate or rotate call.
Session cookie attributes
Hardened
Set-Cookie: sid=<opaque>; Path=/; Secure; HttpOnly; SameSite=Lax- Regenerate the session id at login, step-up authentication and any change of privilege or role.
- Invalidate the pre-authentication session on the server when a new one is issued.
- Accept session identifiers only from the cookie the server set, never from the URL or request body.
- Set Secure, HttpOnly and SameSite on the session cookie.
- Enforce idle and absolute session timeouts and destroy the session fully on logout.
- Use the framework's session management rather than a hand-rolled implementation.
If it already happened
Ship the regeneration fix or force re-authentication, and invalidate all existing sessions so any pre-set identifier stops working.
Remove acceptance of session ids from URLs, review every authentication and privilege-change path for the same gap, and purge session ids from logs where they were exposed.
Notify users where account activity looks unfamiliar, confirm the fix with a test that compares ids before and after login, and monitor for sessions that fail to rotate.
Add a review checklist item for session rotation on every authentication path, add a regression test, and enable the detection above.
Check yourself
1. What makes an application vulnerable to session fixation?
2. Which fix is the primary remediation for session fixation?
3. Why must session identifiers never be accepted from a URL?