CVE-2023-29019 (published 2023-04-21T23:15Z)

@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.\n

CVSS v3 base score: 8.1, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N)

Weakness information:
cwe_id: CWE-384
cwe_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.
extended_description: 
            Such a scenario is commonly observed when:
            
               A web application authenticates a user without first invalidating the existing session, thereby continuing to use the session already associated with the user.
               An attacker is able to force a known session identifier on a user so that, once the user authenticates, the attacker has access to the authenticated session.
               The application or container uses predictable session identifiers.
	    
	    In the generic exploit of session fixation vulnerabilities, an attacker creates a new session on a web application and records the associated session identifier. The attacker then causes the victim to associate, and possibly authenticate, against the server using that session identifier, giving the attacker access to the user's account through the active session.
         
         

Fix information:
repo_url: https://github.com/fastify/fastify-passport
commit_details: repo_url: https://github.com/fastify/fastify-passport
author: Rafael Gonzaga
author_date: 2023-04-20 04:41:03-03:00
msg: Merge pull request from GHSA-4m3m-ppvx-xgw9

Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com>
repository: repo_url: https://github.com/fastify/fastify-passport
repo_name: fastify/fastify-passport
description: Use passport strategies for authentication within a fastify application 
repo_language: TypeScript
owner: fastify
file_changes: filename: SecureSessionManager.ts
change_type: ModificationType.MODIFY
diff: @@ -24,11 +24,19 @@ 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) {

programming_language: TypeScript
filename: session-isolation.test.ts
change_type: ModificationType.MODIFY
diff: @@ -1,38 +1,44 @@
 /* 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
 
@@ -175,8 +181,22 @@ 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')

programming_language: TypeScript