← All news
security4 min read

Session fixation: the auth bug AI code skips every time

By Seaworthy · 7 July 2026

Keys hanging in a door lock

Photo by Matt Clark on Unsplash

How session fixation works

The attack is simple enough to explain in four steps.

First, the attacker visits the site. They get a session cookie, let's call it SESSION=abc123. The session is unauthenticated at this point.

Second, the attacker tricks a victim into making a request with that same cookie. This could be via a link with the session ID in the URL, a shared network, or a crafted form. The victim's browser now carries SESSION=abc123.

Third, the victim logs in. The server verifies credentials, writes user data to the session, and responds with success.

Fourth, the attacker loads their own browser, which still has SESSION=abc123. The session is now authenticated. The attacker is in.

The vulnerability is that the session ID never changed. The server authenticated the user but kept the same identifier the attacker already knew about.

The code AI generates

Here is the pattern you will see in almost every AI-generated Express login route:

app.post('/login', async (req, res) => {
  const user = await db.findUser(req.body.email);
  if (!user || !await bcrypt.compare(req.body.password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Vulnerable: session ID is not regenerated
  req.session.userId = user.id;
  req.session.role = user.role;
  res.json({ success: true });
});

The credential check is fine. The bcrypt compare is fine. But writing auth data directly to the existing session is the problem. Whatever session ID the request arrived with is now the authenticated session ID. If an attacker had that ID before login, they still have a valid authenticated session after.

The fix

Call req.session.regenerate() after authentication succeeds, before writing anything to the session. This creates a new session ID and destroys the old one.

app.post('/login', async (req, res) => {
  const user = await db.findUser(req.body.email);
  if (!user || !await bcrypt.compare(req.body.password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  // Authenticate first, then regenerate before writing session data
  await new Promise((resolve, reject) => {
    req.session.regenerate((err) => {
      if (err) reject(err);
      else resolve();
    });
  });

  req.session.userId = user.id;
  req.session.role = user.role;
  res.json({ success: true });
});

The order matters. Regenerate after authentication is confirmed, then write auth data to the new session. A reusable promise wrapper keeps things tidy if you have multiple endpoints that need this:

function regenerateSession(req) {
  return new Promise((resolve, reject) => {
    req.session.regenerate((err) => (err ? reject(err) : resolve()));
  });
}

// In your login route:
await regenerateSession(req);
req.session.userId = user.id;

It is not just login

Session fixation applies to any privilege boundary, not only initial authentication. If your app has a "promote to admin" endpoint that writes a new role to the existing session, the same attack applies. An attacker who held a regular authenticated session before the promotion still holds it after, now with elevated privileges.

The rule: any time the trust level of a session changes, regenerate the session ID. Login is the obvious case. Role escalation, sudo-style re-authentication, completing a two-factor step, all of these qualify. Treat privilege escalation exactly the same as login.

Session IDs in URLs

While you are here: never accept session IDs from query parameters. Some middleware supports this for "compatibility" reasons, but it is a serious mistake. A URL like https://example.com/dashboard?sessionId=abc123 leaks the session ID through browser history, server access logs, referrer headers, and anywhere URLs get copied or shared.

express-session does not do this by default, so you are probably safe unless you have customised the genid option or are using separate middleware that reads from query params. Check your session configuration explicitly rather than assuming.

Why AI skips this

AI code generators are trained on existing code. A lot of existing code is wrong. The correct req.session.regenerate() pattern is less common in training data than the vulnerable write-directly pattern, and the wrong version still produces working login functionality in happy-path testing. There is no error, no failed test, nothing to surface the omission.

Session fixation does not crash anything. It does not throw an exception. It quietly leaves a door open, and tools that only test the happy path will never catch it.

Seaworthy flags missing req.session.regenerate() calls after credential verification as part of its session management checks.

This article was generated by AI and summarises publicly available sources.

From the makers of Seaworthy

Seaworthy scans your repo for the issues covered in articles like this one.

Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.

$ npx seaworthycode