Skip to content

Authentication cookie valid for too long

CWE-613CWE-539OWASP A07:2021Updated September 4, 20265 min read

An authentication cookie with an expiry measured in months keeps the user logged in, but gives an attacker who obtains the token just as long. Comfort and risk run directly against each other here. The answer is not to force a short session, but to move the long validity into a separate, revocable token bound to a device.

“Remember me” is one of the most appreciated features in any application, and at the same time one of the places where security is quietly given away. A checkbox that keeps the user logged in for months also keeps an attacker logged in for months. This article explains how to keep the comfort without paying that price.

An authentication cookie carries the proof that someone is logged in. The expiry determines how long that proof lasts. A validity that is too long means the token still grants access long after the original login has been forgotten, sometimes for months, sometimes with no upper bound at all.

The trade-off is genuinely difficult, because both sides are real. A short validity forces users to log in constantly, which leads to weaker passwords, saved credentials and irritation. A long validity is comfortable, but it extends exactly the period in which a stolen token stays usable.

It is the difference between a day pass and a key you hand someone for an indefinite period. Both grant access; only with the second does that remain true long after you have forgotten you issued it.

Vulnerable:

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

  req.session.userId = user.id;

  res.cookie('sid', req.sessionID, {
    httpOnly: true,
    secure: true,
    maxAge: 365 * 24 * 60 * 60 * 1000,   // a year
  });
  res.send('Welcome');
});

The session itself is valid for a year. Whoever gets hold of the token has a year of access. That token meanwhile ends up in more places than the user’s browser: in backups of the profile, on a shared computer nobody thinks about any more, in a synchronisation service between devices, or in the hands of someone who takes over the laptop later. There is no second checkpoint at which the application asks itself whether this user still belongs there.

Safe:

const SESSION_MAX = 8 * 60 * 60 * 1000;             // 8 hours
const REMEMBER_MAX = 30 * 24 * 60 * 60 * 1000;      // 30 days

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

  req.session.regenerate(async () => {
    req.session.userId = user.id;
    req.session.startedAt = Date.now();

    res.cookie('__Host-sid', req.sessionID, {
      httpOnly: true, secure: true, sameSite: 'strict',
      path: '/', maxAge: SESSION_MAX,
    });

    if (req.body.rememberMe) {
      // Separate token: only good for starting a new session
      const raw = randomBytes(32).toString('base64url');
      await rememberTokens.store({
        userId: user.id,
        hash: sha256(raw),                       // never store the token itself
        expiresAt: Date.now() + REMEMBER_MAX,
        device: req.get('user-agent'),
      });
      res.cookie('__Host-remember', raw, {
        httpOnly: true, secure: true, sameSite: 'strict',
        path: '/', maxAge: REMEMBER_MAX,
      });
    }
    res.send('Welcome');
  });
});

The session is now short and the long validity sits in a separate token with its own role. That token grants no direct access: at most it produces a new session. It is stored as a hash, so a leaked database does not reveal the tokens, it is tied to a device, and it is revocable. When it is used, the server issues a new token and the old one lapses; if someone then presents the old token anyway, that is a clear signal a copy is in circulation.

Always ask for the password again before sensitive actions when the session was restored from a remember-me token. The user has not demonstrated that they know the password; they have only demonstrated that they hold a token. For changing a password, an email address or payment details, that is too little.

The severity is usually rated low to medium, because no direct attack is possible: the attacker needs the token. What this finding does is increase the value of every other leak.

A session token can leak in many ways, through cross-site scripting, an unencrypted request, a log file, a screenshot, or simply a computer changing hands. With an eight-hour session the damage from that is bounded. With a year-long session the attacker has all the time they need: they can wait until outside office hours, explore at leisure and strike at a moment of their choosing, without a second login attempt that might raise an alarm.

There is an organisational side as well. With a long validity you lose sight of who still has access. Employees who have left, devices that have been decommissioned and accounts whose rights have been withdrawn all keep a valid token, unless you actively enforce otherwise.

The cookie expiry is read directly from the Set-Cookie header, and that is the starting point. More important is whether that expiry means anything: a tester removes it from the cookie and checks whether the token is still accepted. If it is, validity is guarded only by the browser and not on the server at all.

They also examine how the remember-me function is built. Is it the same session cookie with a longer lifetime, or a separate token? Is that token stored hashed? Does it rotate on use? Does it stay valid after a password change? And is authentication requested again before sensitive actions when the session was restored from such a token? AssistSec assesses those questions together, because a long validity with a well-built, revocable and rotating token is defensible, while the same lifetime on the session cookie itself is not.

  • Keep the session cookie short and move a longer validity into a separate remember-me token.
  • Enforce validity on the server; never rely on the cookie expiry alone.
  • Store remember-me tokens hashed, so a leaked database yields no usable tokens.
  • Rotate the token on every use and revoke everything as soon as an expired or reused token is presented.
  • Bind the token to a device and show the user an overview of active devices.
  • Revoke all tokens on a password change, a change of rights, or the blocking of an account.
  • Ask for authentication again before sensitive actions within a restored session.
  • Match the maximum lifetime to the sensitivity of the application and record that choice in your policy.

Sources

Frequently asked questions

May I not keep users logged in for a long time at all?

You may, but not with the session cookie itself. Use a separate remember-me token that only grants the right to create a new session. You can revoke that token, bind it to a device and rotate it on use, while the actual session stays short.

What is a session cookie without an expiry?

A cookie without Expires or Max-Age is removed by the browser as soon as it closes. For authentication that is usually the safest choice. Do note that many browsers restore the previous session including cookies on startup, so do not rely on it blindly.

Why must a remember-me token rotate?

So that reuse becomes visible. If you issue a new token on every automatic recognition and an old token is later presented anyway, a copy evidently exists. That is the moment to revoke all of that user's tokens and warn them.

Is shortening the expiry enough?

Only if the server enforces that limit itself. The expiry on a cookie is an instruction to the browser and says nothing about the validity of the token. An attacker simply sends the value after that date; if that is not checked on the server, nothing changes.

Related articles

Press / to search · Esc