Blog/Authentication
AuthenticationJuly 18, 2026 · 6 min read

Session Fixation: Handing the Attacker a Key You Made

Session fixation flips the usual script. Instead of stealing a session after login, the attacker gives the victim a session before login and waits for them to authenticate it. The flow:

  1. The attacker obtains a valid session ID from your app (just by visiting it).
  2. They trick the victim into using that specific ID — via a link like https://app.com/?sid=ATTACKER_KNOWN_ID, or by injecting the cookie.
  3. The victim logs in. If your app keeps the same session ID across the login, that now-authenticated session is one the attacker already knows.
  4. The attacker uses the fixed ID and is logged in as the victim.

The victim did all the work; the attacker just supplied the key ahead of time. The bug is that the application treated the pre-login and post-login sessions as the same session, when authentication should have started a brand-new one.

Where it comes from

  • Session IDs accepted from the URL or a form (?sid=, ?PHPSESSID=) — the attacker can put any value there.
  • Not regenerating the ID at login. The most common cause: the framework keeps the anonymous session and just attaches the user to it.
  • Persistent cookies set before authentication that survive the login unchanged.
  • Subdomain or path scoping that lets an attacker-controlled context set a cookie the main app trusts.

The fix: new identity at every privilege change

The core rule is one line: when a user's privilege level changes — anonymous to logged-in, user to admin, or on logout — issue a completely new session ID and abandon the old one. Any value the attacker fixed belongs to the old, now-discarded session.

app.post('/login', async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
if (!user) return res.status(401).send('Invalid credentials');

// Regenerate: destroys the old session and starts a fresh one with a
// NEW id. Any pre-login id the attacker fixed is now worthless.
req.session.regenerate((err) => {
  if (err) return res.status(500).send('Login error');
  req.session.userId = user.id;
  req.session.save(() => res.redirect('/dashboard'));
});
});

// On logout, destroy the session entirely (don't just clear fields):
app.post('/logout', (req, res) => {
req.session.destroy(() => {
  res.clearCookie('connect.sid');
  res.redirect('/');
});
});

// Cookie settings that back this up:
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false, saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: 'lax' },
}));

The principles

  • Regenerate the session ID at login (and any privilege change). This is the essential fix — it turns any fixed pre-login ID into a dead value.
  • Only accept session IDs from cookies, in strict mode. Never from the URL or a form. Strict mode rejects IDs the server didn't issue.
  • Destroy sessions on logout, don't just clear fields — issue a fresh identity next time.
  • Set HttpOnly, Secure, SameSite on the session cookie so it can't be read by script, sent over HTTP, or attached to cross-site requests.

The one line: authentication should always begin a new session — never upgrade the anonymous one in place.

Try it in the session fixation simulation: fix a victim's session ID, watch their login hand it to you, then add ID regeneration and see the attack break. It pairs with securing cookies, which covers the flags that back this up.

Share this post

Frequently Asked Questions

Related posts