CVE-2023-29019: Session Fixation (CWE-384)
value: @fastify/passport is a port of passport authentication library for the Fastify ecosystem. Applications using `@fastify/passport` in affected versions for user authentication, in combination with `@fastify/session` as the underlying session management mechanism, are vulnerable to session fixation attacks from network and same-site attackers. fastify applications rely on the `@fastify/passport` library for user authentication. The login and user validation are performed by the `authenticate` function. When executing this function, the `sessionId` is preserved between the pre-login and the authenticated session. Network and same-site attackers can hijack the victim's session by tossing a valid `sessionId` cookie in the victim's browser and waiting for the victim to log in on the website. As a solution, newer versions of `@fastify/passport` regenerate `sessionId` upon login, preventing the attacker-controlled pre-session cookie from being upgraded to an authenticated session. Users are advised to upgrade. There are no known workarounds for this vulnerability.

CWE 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.
CVSS v3 base score: 8.1
Published: 2023-04-21T23:15Z
Repository: https://github.com/fastify/fastify-passport
Language: TypeScript
Files changed: src/session-managers/SecureSessionManager.ts, test/session-isolation.test.ts
Commit date: 2023-04-20 09:41:03 +0200
Commit message: Merge pull request from GHSA-4m3m-ppvx-xgw9

Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
Diff: diff --git a/src/session-managers/SecureSessionManager.ts b/src/session-managers/SecureSessionManager.ts
index 16eb548..e9b27a1 100644
--- a/src/session-managers/SecureSessionManager.ts
+++ b/src/session-managers/SecureSessionManager.ts
@@ -22,15 +22,23 @@ export class SecureSessionManager {
     }
   }
 
   async logIn(request: FastifyRequest, user: any) {
     const object = await this.serializeUser(user, request)
+    // Handle sessions using @fastify/session
+    if (request.session.regenerate) {
+      // regenerate session to guard against session fixation
+      await request.session.regenerate()
+    }
     request.session.set(this.key, object)
   }
 
   async logOut(request: FastifyRequest) {
     request.session.set(this.key, undefined)
+    if (request.session.regenerate) {
+      await request.session.regenerate()
+    }
   }
 
   getUserFromSession(request: FastifyRequest) {
     return request.session.get(this.key)
   }
diff --git a/test/session-isolation.test.ts b/test/session-isolation.test.ts
index 00a8605..694d0a2 100644
--- a/test/session-isolation.test.ts
+++ b/test/session-isolation.test.ts
@@ -1,40 +1,46 @@
 /* eslint-disable @typescript-eslint/no-empty-function */
 import { generateTestUser, getConfiguredTestServer, TestBrowserSession } from './helpers'
 
-const { server, fastifyPassport } = getConfiguredTestServer()
-
-server.get(
-  '/protected',
-  { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
-  async () => 'hello!'
-)
-server.get('/my-id', { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) }, async (request) =>
-  String((request.user as any).id)
-)
-server.post(
-  '/login',
-  { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
-  async () => 'success'
-)
-
-server.post('/force-login', async (request, reply) => {
-  await request.logIn(generateTestUser())
-  void reply.send('logged in')
-})
-
-server.post(
-  '/logout',
-  { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
-  async (request, reply) => {
-    await request.logout()
-    void reply.send('logged out')
-  }
-)
+function createServer() {
+  const { server, fastifyPassport } = getConfiguredTestServer()
+
+  server.get(
+    '/protected',
+    { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
+    async () => 'hello!'
+  )
+  server.get('/my-id', { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) }, async (request) =>
+    String((request.user as any).id)
+  )
+  server.post(
+    '/login',
+    { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
+    async () => 'success'
+  )
+
+  server.post('/force-login', async (request, reply) => {
+    await request.logIn(generateTestUser())
+    void reply.send('logged in')
+  })
+
+  server.post(
+    '/logout',
+    { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
+    async (request, reply) => {
+      await request.logout()
+      void reply.send('logged out')
+    }
+  )
+  return server
+}
 
 const suite = (sessionPluginName) => {
+  process.env.SESSION_PLUGIN = sessionPluginName
+  const server = createServer()
   describe(`${sessionPluginName} tests`, () => {
+    const sessionOnlyTest = sessionPluginName === '@fastify/session' ? test : test.skip
     describe('session isolation', () => {
       let userA, userB, userC
 
       beforeEach(() => {
         userA = new TestBrowserSession(server)
@@ -173,11 +179,25 @@ const suite = (sessionPluginName) => {
         )
 
         // expect each returned ID to be unique
         expect(Array.from(new Set(ids)).sort()).toEqual(ids.sort())
       })
+
+      sessionOnlyTest('should regenerate session on login', async () => {
+        expect(userA.cookies['sessionId']).toBeUndefined()
+        await userA.inject({ method: 'GET', url: '/protected' })
+        expect(userA.cookies['sessionId']).not.toBeUndefined()
+        const prevSessionId = userA.cookies.sessionId
+        await userA.inject({
+          method: 'POST',
+          url: '/login',
+          payload: { login: 'test', password: 'test' },
+        })
+        expect(userA.cookies.sessionId).not.toBe(prevSessionId)
+      })
     })
   })
+  delete process.env.SESSION_PLUGIN
 }
 
 suite('@fastify/session')
 suite('@fastify/secure-session')

Vulnerable code: const { server, fastifyPassport } = getConfiguredTestServer()

server.get(
  '/protected',
  { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
  async () => 'hello!'
)
server.get('/my-id', { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) }, async (request) =>
  String((request.user as any).id)
)
server.post(
  '/login',
  { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
  async () => 'success'
)

server.post('/force-login', async (request, reply) => {
  await request.logIn(generateTestUser())
  void reply.send('logged in')
})

server.post(
  '/logout',
  { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
  async (request, reply) => {
    await request.logout()
    void reply.send('logged out')
  }
)
Fixed code:     // Handle sessions using @fastify/session
    if (request.session.regenerate) {
      // regenerate session to guard against session fixation
      await request.session.regenerate()
    }
    if (request.session.regenerate) {
      await request.session.regenerate()
    }
function createServer() {
  const { server, fastifyPassport } = getConfiguredTestServer()

  server.get(
    '/protected',
    { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
    async () => 'hello!'
  )
  server.get('/my-id', { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) }, async (request) =>
    String((request.user as any).id)
  )
  server.post(
    '/login',
    { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
    async () => 'success'
  )

  server.post('/force-login', async (request, reply) => {
    await request.logIn(generateTestUser())
    void reply.send('logged in')
  })

  server.post(
    '/logout',
    { preValidation: fastifyPassport.authenticate('test', { authInfo: false }) },
    async (request, reply) => {
      await request.logout()
      void reply.send('logged out')
    }
  )
  return server
}
  process.env.SESSION_PLUGIN = sessionPluginName
  const server = createServer()
    const sessionOnlyTest = sessionPluginName === '@fastify/session' ? test : test.skip

      sessionOnlyTest('should regenerate session on login', async () => {
        expect(userA.cookies['sessionId']).toBeUndefined()
        await userA.inject({ method: 'GET', url: '/protected' })
        expect(userA.cookies['sessionId']).not.toBeUndefined()
        const prevSessionId = userA.cookies.sessionId
        await userA.inject({
          method: 'POST',
          url: '/login',
          payload: { login: 'test', password: 'test' },
        })
        expect(userA.cookies.sessionId).not.toBe(prevSessionId)
      })
  delete process.env.SESSION_PLUGIN