Fixing Session Fixation

Jared Hanson
Passport.js
Published in
4 min readMay 20, 2022

Version 0.6.0 of passport has been released, which improves robustness against classes of session fixation attacks. Being a security enhancement, I’d advise upgrading as soon as possible. But first, let’s look at the problem and the enhancements introduced in this release in order to understand the impact.

When a user visits an app using sessions, a session ID is generated, its value is signed, and the signed value is set as a session ID cookie. This cookie is transmitted by the browser to the server on subsequent requests. The server-side app can then verify the signed cookie and use the session ID to look up associated information stored in a backend database or cache.

Cookies allow an app to maintain state between the server and a specific browser over HTTP, which is otherwise a “stateless” protocol. State is particularly important to authentication, because we need to accurately track who is logged into the app.

Session state is also used for many purposes other than authentication. As a result, a session ID is often generated for unauthenticated requests. This is where susceptibility to session fixation vulnerabilities can become a concern. Let’s examine a scenario.

Chuck, a malicious attacker, sits down at a shared computer in the local library. He opens a browser and visits our vulnerable app, which creates a session with a session ID of s1Xn(in reality these are much longer, but we are simplifying here for clarity). Note that Chuck simply visits the app, he does not log in. Chuck then walks away from the computer, leaving the browser open, but not before copying the session ID cookie which he gets from the browser’s developer tools. Chuck waits for a victim.

A short while later, Alice sits down at the same computer and is also a user of our app, which (unfortunately for Alice) is already open on the computer’s browser. She logs in. Our vulnerable app updates the session information in its cache, but maintains the same session ID s1Xn.

Now, Chuck launches the last half of his attack. He takes the session ID cookie he copied earlier, containing s1Xn as the session ID, and imports it into the browser on his own laptop. He then visits our vulnerable app. Our app loads the session data, which now identifies Alice as the logged in user, and Chuck has access to Alice’s account.

Such an attack requires physical access to the same computer as the victim. It is not exploitable remotely unless the app suffers from additional vulnerabilities, such as accepting session IDs in URL parameters or form fields. Cross-Site Scripting (XSS) vulnerabilities are also a vector by which cookies could be injected remotely.

This issue has been discussed previously in both passport(#192) and express-session (#425). Previously, it has been my position that this issue is better addressed at the application-level because the app may be using sessions for purposes other than authentication. Recently, however, GitHub Advanced Security has started flagging this issue as a warning. Marco Squarcina also provided a detailed analysis performed as part of a research project by the Security & Privacy group at TU Wein.

As a result, I’ve changed my position on this matter. Authentication and sessions are tightly interrelated aspects of any web application, and Passport should be doing more out-of-the-box to ensure proper security. So what is changing?

The root cause of the session fixation is the fact that the session ID is “fixed” at the time it is generated, and spans across authentication events. To address this, Passport now regenerates the session, resulting in a new session ID, any time a user logs in or logs out. This follows the latest best practices recommended by OWASP.

By default, any information in the session will be lost after login or logout. This is a change from prior versions of Passport which preserved this data. If you want to keep the previous session information, there’s a new keepSessionInfo option, which can be passed to passport.authenticate() or to req.login(). For example:

app.post('/login/password', passport.authenticate('local', {
successReturnToOrRedirect: '/',
failureRedirect: '/login',
failureMessage: true,
keepSessionInfo: true
}));

Use caution when doing this, as session information is often correlated to the authenticated user or contains other credentials that should also be rotated. For instance, any secrets used to verify CSRF tokens (as implemented by csurf) should be regenerated. The change in behavior results in “secure by default” operation, with the ability for the application to explicitly override when necessary.

The other major change is that that req.logout() is now an asynchronous function, whereas previously it was synchronous. For instance, a logout route that was previously:

app.post('/logout', function(req, res, next) {
req.logout();
res.redirect('/');
});

should be modified to:

app.post('/logout', function(req, res, next) {
req.logout(function(err) {
if (err) { return next(err); }
res.redirect('/');
});
});

As with logging in, logging out also clears session information. The keepSessionInfooption can be passsed to req.logout() to override this.

This improves the overall security posture of any app using Passport for authentication. While the situations in which this vulnerability can be exploited are limited, and, in the most severe cases, depend upon other vulnerabilities in the app, the importance of defense in depth cannot be understated. Passport strives to make authentication both simple and secure, and this release is a step forward towards that persistent goal.

--

--