CVE-2021-41246 (JavaScript)

Express OpenID Connect is vulnerable to session fixation during the OpenID Connect callback login flow. Before authentication completes, an attacker may be able to make a victim use an application session cookie or server-side session identifier that the attacker knows or controls, such as an anonymous pre-login session or a session associated with another principal. If a successful callback stores the newly authenticated user's session under that same client-visible cookie or server-side id, the attacker can continue using the fixed identifier to access or confuse the victim's authenticated session. A fix should treat any callback that establishes a new authenticated principal over an unauthenticated session or a different existing user as a security boundary: the authenticated session must be bound to a fresh client-visible session cookie or server-side session id, and the attacker-known pre-login id must not resolve to the newly authenticated user's credentials or session data. Implementations may destroy the old session record or leave it only as a non-promoted anonymous or previous-user session, and they may choose whether to preserve unrelated anonymous application state, but these choices must not allow the old fixed identifier to become valid for the new authenticated user. Clearing or overwriting selected session fields without rotating the identifier and preventing old-id promotion is not sufficient.

Weakness:
CWE-384: name: Session Fixation
description: Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.

Repository: https://github.com/auth0/express-openid-connect

Vulnerable code:
file_path: middleware/auth.js
snippet:       async (req, res, next) => {
        next = cb(next).once();

        client =
          client ||
          (await getClient(config).catch((err) => {
            next(err);
          }));

        if (!client) {
          return;
        }

        try {
          const redirectUri = res.oidc.getRedirectUri();

          let session;

          try {
            const callbackParams = client.callbackParams(req);
            const authVerification = transient.getOnce(
              'auth_verification',
              req,
              res
            );

            const { max_age, code_verifier, nonce, state } = authVerification
              ? JSON.parse(authVerification)
              : {};

            req.openidState = decodeState(state);
            const checks = {
              max_age,
              code_verifier,
              nonce,
              state,
            };

            let extras;
            if (config.tokenEndpointParams) {
              extras = { exchangeBody: config.tokenEndpointParams };
            }

            session = await client.callback(
              redirectUri,
              callbackParams,
              checks,
              extras
            );
          } catch (err) {
            throw createError.BadRequest(err.message);
          }

          if (config.afterCallback) {
            session = await config.afterCallback(
              req,
              res,
              Object.assign({}, session), // Remove non-enumerable methods from the TokenSet
              req.openidState
            );
          }

          Object.assign(req[config.session.name], session);
          attemptSilentLogin.resumeSilentLogin(req, res);

          next();
        } catch (err) {
          // Swallow errors if this is a silentLogin
          if (req.openidState && req.openidState.attemptingSilentLogin) {
            next();
          } else {
            next(err);
          }
        }
      },

Fixed code:
file_path: middleware/auth.js
snippet:       async (req, res, next) => {
        next = cb(next).once();

        client =
          client ||
          (await getClient(config).catch((err) => {
            next(err);
          }));

        if (!client) {
          return;
        }

        try {
          const redirectUri = res.oidc.getRedirectUri();

          let tokenSet;

          try {
            const callbackParams = client.callbackParams(req);
            const authVerification = transient.getOnce(
              'auth_verification',
              req,
              res
            );

            const { max_age, code_verifier, nonce, state } = authVerification
              ? JSON.parse(authVerification)
              : {};

            req.openidState = decodeState(state);
            const checks = {
              max_age,
              code_verifier,
              nonce,
              state,
            };

            let extras;
            if (config.tokenEndpointParams) {
              extras = { exchangeBody: config.tokenEndpointParams };
            }

            tokenSet = await client.callback(
              redirectUri,
              callbackParams,
              checks,
              extras
            );
          } catch (err) {
            throw createError.BadRequest(err.message);
          }

          let session = Object.assign({}, tokenSet); // Remove non-enumerable methods from the TokenSet

          if (config.afterCallback) {
            session = await config.afterCallback(
              req,
              res,
              session,
              req.openidState
            );
          }

          if (req.oidc.isAuthenticated()) {
            if (req.oidc.user.sub === tokenSet.claims().sub) {
              // If it's the same user logging in again, just update the existing session.
              Object.assign(req[config.session.name], session);
            } else {
              // If it's a different user, replace the session to remove any custom user
              // properties on the session
              replaceSession(req, session, config);
              // And regenerate the session id so the previous user wont know the new user's session id
              regenerateSessionStoreId(req, config);
            }
          } else {
            // If a new user is replacing an anonymous session, update the existing session to keep
            // any anonymous session state (eg. checkout basket)
            Object.assign(req[config.session.name], session);
            // But update the session store id so a previous anonymous user wont know the new user's session id
            regenerateSessionStoreId(req, config);
          }
          attemptSilentLogin.resumeSilentLogin(req, res);

          next();
        } catch (err) {
          // Swallow errors if this is a silentLogin
          if (req.openidState && req.openidState.attemptingSilentLogin) {
            next();
          } else {
            next(err);
          }
        }
      },