CVE-2023-27495: Cross-Site Request Forgery (CSRF) (CWE-352)
value: @fastify/csrf-protection is a plugin which helps protect Fastify servers against CSRF attacks. The CSRF protection enforced by the @fastify/csrf-protection library in combination with @fastify/cookie can be bypassed from network and same-site attackers under certain conditions. @fastify/csrf-protection supports an optional userInfo parameter that binds the CSRF token to the user. This parameter has been introduced to prevent cookie-tossing attacks as a fix for CVE-2021-29624. Whenever userInfo parameter is missing, or its value can be predicted for the target user account, network and same-site attackers can 1. fixate a _csrf cookie in the victim's browser, and 2. forge CSRF tokens that are valid for the victim's session. This allows attackers to bypass the CSRF protection mechanism. As a fix, @fastify/csrf-protection starting from version 6.3.0 (and v4.1.0) includes a server-defined secret hmacKey that cryptographically binds the CSRF token to the value of the _csrf cookie and the userInfo parameter, making tokens non-spoofable by attackers. This protection is effective as long as the userInfo parameter is unique for each user. This is patched in versions 6.3.0 and v4.1.0. Users are advised to upgrade. Users unable to upgrade may use a random, non-predictable userInfo parameter for each user as a mitigation.
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 6.5
Published: 2023-04-20T18:15Z
Repository: https://github.com/fastify/csrf-protection
Language: JavaScript
Files changed: README.md, index.js, test/user-info.test.js, types/index.d.ts, types/index.test-d.ts
Commit date: 2023-04-20 09:39:58 +0200
Commit message: Merge pull request from GHSA-qrgf-9gpc-vrxw

* fix: enforce hmac key

* fix: check only if fastify/cookie is enabled

* fix: restored working testts

* fix: update README.md

Co-authored-by: Uzlopak <aras.abbasi@googlemail.com>

* Update index.d.ts

* Update index.test-d.ts

* Update index.d.ts

* Update index.test-d.ts

* fix unit tests

---------

Co-authored-by: Uzlopak <aras.abbasi@googlemail.com>
Diff: diff --git a/README.md b/README.md
index ec8218b..651f4f0 100644
--- a/README.md
+++ b/README.md
@@ -159,10 +159,11 @@ const token = await reply.generateCsrf()
 You can also pass the [cookie serialization](https://github.com/fastify/fastify-cookie) options to the function.
 
 The option `userInfo` is required if `getUserInfo` has been specified in the module option.
 The provided `userInfo` is hashed  inside the csrf token and it is not directly exposed.
 This option is needed to protect against cookie tossing.
+The option `csrfOpts.hmacKey` is required if `getUserInfo` has been specified in the module option in combination with using [@fastify/cookie](https://github.com/fastify/fastify-cookie) as sessionPlugin
 
 ### `fastify.csrfProtection(request, reply, next)`
 
 A hook that you can use for protecting routes or entire plugins from CSRF attacks.
 Generally, we recommend using an `onRequest` hook, but if you are sending the token
diff --git a/index.js b/index.js
index 4c12ef0..478ff49 100644
--- a/index.js
+++ b/index.js
@@ -40,10 +40,15 @@ async function fastifyCsrfProtection (fastify, opts) {
   )
 
   if (opts.getUserInfo) {
     csrfOpts.userInfo = true
   }
+
+  if (sessionPlugin === '@fastify/cookie' && csrfOpts.userInfo) {
+    assert(csrfOpts.hmacKey, 'csrfOpts.hmacKey is required')
+  }
+
   const tokens = new CSRF(csrfOpts)
 
   const isCookieSigned = cookieOpts && cookieOpts.signed
 
   if (sessionPlugin === '@fastify/secure-session') {
diff --git a/test/user-info.test.js b/test/user-info.test.js
index 58917c8..269ca80 100644
--- a/test/user-info.test.js
+++ b/test/user-info.test.js
@@ -15,10 +15,13 @@ test('Cookies with User-Info', async t => {
   const fastify = Fastify()
   await fastify.register(fastifyCookie)
   await fastify.register(fastifyCsrf, {
     getUserInfo (req) {
       return userInfoDB[req.body.username]
+    },
+    csrfOpts: {
+      hmacKey: 'foo'
     }
   })
 
   const userInfoDB = {
     foo: 'a42'
@@ -71,10 +74,13 @@ test('Session with User-Info', async t => {
   })
   await fastify.register(fastifyCsrf, {
     sessionPlugin: '@fastify/session',
     getUserInfo (req) {
       return req.session.username
+    },
+    csrfOpts: {
+      hmacKey: 'foo'
     }
   })
 
   fastify.post('/login', async (req, reply) => {
     req.session.username = req.body.username
@@ -120,10 +126,13 @@ test('SecureSession with User-Info', async t => {
   await fastify.register(fastifySecureSession, { key, cookie: { path: '/', secure: false } })
   await fastify.register(fastifyCsrf, {
     sessionPlugin: '@fastify/secure-session',
     getUserInfo (req) {
       return req.session.get('username')
+    },
+    csrfOpts: {
+      hmacKey: 'foo'
     }
   })
 
   fastify.post('/login', async (req, reply) => {
     req.session.set('username', req.body.username)
@@ -161,5 +170,84 @@ test('SecureSession with User-Info', async t => {
     }
   })
 
   t.equal(response2.statusCode, 200)
 })
+
+test('Validate presence of hmac key with User-Info /1', async (t) => {
+  const fastify = Fastify()
+  await fastify.register(fastifyCookie)
+
+  await t.rejects(fastify.register(fastifyCsrf, {
+    getUserInfo (req) {
+      return req.session.get('username')
+    }
+  }), Error('csrfOpts.hmacKey is required'))
+})
+
+test('Validate presence of hmac key with User-Info /2', async (t) => {
+  const fastify = Fastify()
+  await fastify.register(fastifyCookie)
+
+  await t.rejects(fastify.register(fastifyCsrf, {
+    getUserInfo (req) {
+      return req.session.get('username')
+    },
+    sessionPlugin: '@fastify/cookie'
+  }), Error('csrfOpts.hmacKey is required'))
+})
+
+test('Validate presence of hmac key with User-Info /3', async (t) => {
+  const fastify = Fastify()
+  await fastify.register(fastifyCookie)
+
+  await t.rejects(fastify.register(fastifyCsrf, {
+    getUserInfo (req) {
+      return req.session.get('username')
+    },
+    csrfOpts: {
+      hmacKey: undefined
+    }
+  }), Error('csrfOpts.hmacKey is required'))
+})
+
+test('Validate presence of hmac key with User-Info /4', async (t) => {
+  const fastify = Fastify()
+  await fastify.register(fastifyCookie)
+
+  await t.rejects(fastify.register(fastifyCsrf, {
+    getUserInfo (req) {
+      return req.session.get('username')
+    },
+    sessionPlugin: '@fastify/cookie',
+    csrfOpts: {
+      hmacKey: undefined
+    }
+  }), Error('csrfOpts.hmacKey is required'))
+})
+
+test('Validate presence of hmac key with User-Info /5', async (t) => {
+  const fastify = Fastify()
+  await fastify.register(fastifySecureSession, { key, cookie: { path: '/', secure: false } })
+
+  await t.resolves(fastify.register(fastifyCsrf, {
+    getUserInfo (req) {
+      return req.session.get('username')
+    },
+    sessionPlugin: '@fastify/secure-session'
+  }))
+})
+
+test('Validate presence of hmac key with User-Info /6', async (t) => {
+  const fastify = Fastify()
+  await fastify.register(fastifySecureSession, { key, cookie: { path: '/', secure: false } })
+
+  await t.resolves(fastify.register(fastifyCsrf, {
+    getUserInfo (req) {
+      return req.session.get('username')
+    },
+    sessionPlugin: '@fastify/secure-session',
+    csrfOpts: {
+      hmacKey: 'foo'
+    }
+  }))
+})
diff --git a/types/index.d.ts b/types/index.d.ts
index 8c3c30c..365ead8 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -24,21 +24,40 @@ type FastifyCsrfProtection = FastifyPluginAsync<fastifyCsrfProtection.FastifyCsr
 
 declare namespace fastifyCsrfProtection {
   export type CookieSerializeOptions = FastifyCookieSerializeOptions
 
   export type GetTokenFn = (req: FastifyRequest) => string | void;
-  
-  export interface FastifyCsrfProtectionOptions {
-    csrfOpts?: CSRFOptions;
+
+  interface FastifyCsrfProtectionOptionsBase {
     cookieKey?: string;
     cookieOpts?: CookieSerializeOptions;
     sessionKey?: string;
     getUserInfo?: (req: FastifyRequest) => string;
     getToken?: GetTokenFn;
-    sessionPlugin?: '@fastify/cookie' | '@fastify/session' | '@fastify/secure-session';
   }
 
+  interface FastifyCsrfProtectionOptionsFastifyCookie {
+    sessionPlugin?: '@fastify/cookie';
+    csrfOpts: Omit<CSRFOptions, 'hmacKey'> & Required<Pick<CSRFOptions, 'hmacKey'>>;
+  }
+
+  interface FastifyCsrfProtectionOptionsFastifySession {
+    sessionPlugin: '@fastify/session';
+    csrfOpts?: CSRFOptions;
+  }
+
+  interface FastifyCsrfProtectionOptionsFastifySecureSession {
+    sessionPlugin: '@fastify/secure-session';
+    csrfOpts?: CSRFOptions;
+  }
+
+  export type FastifyCsrfProtectionOptions = FastifyCsrfProtectionOptionsBase & (
+    FastifyCsrfProtectionOptionsFastifyCookie |
+    FastifyCsrfProtectionOptionsFastifySession |
+    FastifyCsrfProtectionOptionsFastifySecureSession
+  )
+
   /**
    * @deprecated Use FastifyCsrfProtectionOptions instead
    */
   export type FastifyCsrfOptions = FastifyCsrfProtectionOptions;
 
diff --git a/types/index.test-d.ts b/types/index.test-d.ts
index 467f402..a7d479c 100644
--- a/types/index.test-d.ts
+++ b/types/index.test-d.ts
@@ -29,15 +29,30 @@ async function run() {
     }
   })
 }
 
 
-fastify.register(FastifyCsrfProtection, { csrfOpts: { algorithm: 'sha1' } })
+fastify.register(FastifyCsrfProtection, { csrfOpts: { algorithm: 'sha1', hmacKey: 'hmac' } })
 expectError(fastify.register(FastifyCsrfProtection, { csrfOpts: { algorithm: 1 } }))
 
 fastify.register(FastifySession)
-fastify.register(FastifyCsrfProtection, { getUserInfo(req) {
-  return req.session.get('username')
-}})
+fastify.register(FastifyCsrfProtection, {
+  csrfOpts: {
+    hmacKey: '123'
+  },
+  getUserInfo(req) {
+    return req.session.get('username')
+  }
+})
 expectError(fastify.register(FastifyCsrfProtection, { getUserInfo: 'invalid' }))
 
+fastify.register(FastifyCsrfProtection, { csrfOpts: { hmacKey: 'hmac' }, sessionPlugin: '@fastify/cookie' })
+fastify.register(FastifyCsrfProtection, { csrfOpts: { hmacKey: 'hmac' } })
+expectError(fastify.register(FastifyCsrfProtection, { }))
+expectError(fastify.register(FastifyCsrfProtection, { csrfOpts: { }}))
+expectError(fastify.register(FastifyCsrfProtection, { sessionPlugin: '@fastify/cookie', csrfOpts: { }}))
+fastify.register(FastifyCsrfProtection, { csrfOpts: { }, sessionPlugin: '@fastify/session' })
+fastify.register(FastifyCsrfProtection, { csrfOpts: { }, sessionPlugin: '@fastify/secure-session' })
+fastify.register(FastifyCsrfProtection, { sessionPlugin: '@fastify/session' })
+fastify.register(FastifyCsrfProtection, { sessionPlugin: '@fastify/secure-session' })
+
 expectDeprecated({} as FastifyCsrfOptions)

Vulnerable code:   
  export interface FastifyCsrfProtectionOptions {
    csrfOpts?: CSRFOptions;
    sessionPlugin?: '@fastify/cookie' | '@fastify/session' | '@fastify/secure-session';
fastify.register(FastifyCsrfProtection, { csrfOpts: { algorithm: 'sha1' } })
fastify.register(FastifyCsrfProtection, { getUserInfo(req) {
  return req.session.get('username')
}})
Fixed code: The option `csrfOpts.hmacKey` is required if `getUserInfo` has been specified in the module option in combination with using [@fastify/cookie](https://github.com/fastify/fastify-cookie) as sessionPlugin

  if (sessionPlugin === '@fastify/cookie' && csrfOpts.userInfo) {
    assert(csrfOpts.hmacKey, 'csrfOpts.hmacKey is required')
  }

    },
    csrfOpts: {
      hmacKey: 'foo'
    },
    csrfOpts: {
      hmacKey: 'foo'
    },
    csrfOpts: {
      hmacKey: 'foo'

test('Validate presence of hmac key with User-Info /1', async (t) => {
  const fastify = Fastify()
  await fastify.register(fastifyCookie)

  await t.rejects(fastify.register(fastifyCsrf, {
    getUserInfo (req) {
      return req.session.get('username')
    }
  }), Error('csrfOpts.hmacKey is required'))
})

test('Validate presence of hmac key with User-Info /2', async (t) => {
  const fastify = Fastify()
  await fastify.register(fastifyCookie)

  await t.rejects(fastify.register(fastifyCsrf, {
    getUserInfo (req) {
      return req.session.get('username')
    },
    sessionPlugin: '@fastify/cookie'
  }), Error('csrfOpts.hmacKey is required'))
})

test('Validate presence of hmac key with User-Info /3', async (t) => {
  const fastify = Fastify()
  await fastify.register(fastifyCookie)

  await t.rejects(fastify.register(fastifyCsrf, {
    getUserInfo (req) {
      return req.session.get('username')
    },
    csrfOpts: {
      hmacKey: undefined
    }
  }), Error('csrfOpts.hmacKey is required'))
})

test('Validate presence of hmac key with User-Info /4', async (t) => {
  const fastify = Fastify()
  await fastify.register(fastifyCookie)

  await t.rejects(fastify.register(fastifyCsrf, {
    getUserInfo (req) {
      return req.session.get('username')
    },
    sessionPlugin: '@fastify/cookie',
    csrfOpts: {
      hmacKey: undefined
    }
  }), Error('csrfOpts.hmacKey is required'))
})

test('Validate presence of hmac key with User-Info /5', async (t) => {
  const fastify = Fastify()
  await fastify.register(fastifySecureSession, { key, cookie: { path: '/', secure: false } })

  await t.resolves(fastify.register(fastifyCsrf, {
    getUserInfo (req) {
      return req.session.get('username')
    },
    sessionPlugin: '@fastify/secure-session'
  }))
})

test('Validate presence of hmac key with User-Info /6', async (t) => {
  const fastify = Fastify()
  await fastify.register(fastifySecureSession, { key, cookie: { path: '/', secure: false } })

  await t.resolves(fastify.register(fastifyCsrf, {
    getUserInfo (req) {
      return req.session.get('username')
    },
    sessionPlugin: '@fastify/secure-session',
    csrfOpts: {
      hmacKey: 'foo'
    }
  }))
})

  interface FastifyCsrfProtectionOptionsBase {
  interface FastifyCsrfProtectionOptionsFastifyCookie {
    sessionPlugin?: '@fastify/cookie';
    csrfOpts: Omit<CSRFOptions, 'hmacKey'> & Required<Pick<CSRFOptions, 'hmacKey'>>;
  }

  interface FastifyCsrfProtectionOptionsFastifySession {
    sessionPlugin: '@fastify/session';
    csrfOpts?: CSRFOptions;
  }

  interface FastifyCsrfProtectionOptionsFastifySecureSession {
    sessionPlugin: '@fastify/secure-session';
    csrfOpts?: CSRFOptions;
  }

  export type FastifyCsrfProtectionOptions = FastifyCsrfProtectionOptionsBase & (
    FastifyCsrfProtectionOptionsFastifyCookie |
    FastifyCsrfProtectionOptionsFastifySession |
    FastifyCsrfProtectionOptionsFastifySecureSession
  )

fastify.register(FastifyCsrfProtection, { csrfOpts: { algorithm: 'sha1', hmacKey: 'hmac' } })
fastify.register(FastifyCsrfProtection, {
  csrfOpts: {
    hmacKey: '123'
  },
  getUserInfo(req) {
    return req.session.get('username')
  }
})
fastify.register(FastifyCsrfProtection, { csrfOpts: { hmacKey: 'hmac' }, sessionPlugin: '@fastify/cookie' })
fastify.register(FastifyCsrfProtection, { csrfOpts: { hmacKey: 'hmac' } })
expectError(fastify.register(FastifyCsrfProtection, { }))
expectError(fastify.register(FastifyCsrfProtection, { csrfOpts: { }}))
expectError(fastify.register(FastifyCsrfProtection, { sessionPlugin: '@fastify/cookie', csrfOpts: { }}))
fastify.register(FastifyCsrfProtection, { csrfOpts: { }, sessionPlugin: '@fastify/session' })
fastify.register(FastifyCsrfProtection, { csrfOpts: { }, sessionPlugin: '@fastify/secure-session' })
fastify.register(FastifyCsrfProtection, { sessionPlugin: '@fastify/session' })
fastify.register(FastifyCsrfProtection, { sessionPlugin: '@fastify/secure-session' })