VYPR
Medium severity4.2NVD Advisory· Published Apr 9, 2026· Updated Apr 14, 2026

CVE-2026-35041

CVE-2026-35041

Description

fast-jwt provides fast JSON Web Token (JWT) implementation. From 5.0.0 to 6.2.0, a denial-of-service condition exists in fast-jwt when the allowedAud verification option is configured using a regular expression. Because the aud claim is attacker-controlled and the library evaluates it against the supplied RegExp, a crafted JWT can trigger catastrophic backtracking in the JavaScript regex engine, resulting in significant CPU consumption during verification. This vulnerability is fixed in 6.2.1.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
fast-jwtnpm
>= 5.0.0, < 6.2.16.2.1

Affected products

1
  • cpe:2.3:a:nearform:fast-jwt:*:*:*:*:*:node.js:*:*
    Range: >=5.0.0,<6.2.1

Patches

1
b0be0ca16159

fix: GHSA-cjw9-ghj4-fwxf CVE-2026-35041 ReDoS when using RegExp in allowed options (#595)

https://github.com/nearform/fast-jwtAntonio AttanasioApr 9, 2026via ghsa
3 files changed · +170 1
  • package.json+2 1 modified
    @@ -55,7 +55,8 @@
         "@lukeed/ms": "^2.0.2",
         "asn1.js": "^5.4.1",
         "ecdsa-sig-formatter": "^1.0.11",
    -    "mnemonist": "^0.40.0"
    +    "mnemonist": "^0.40.0",
    +    "safe-regex2": "^5.1.0"
       },
       "devDependencies": {
         "@node-rs/jsonwebtoken": "^0.5.9",
    
  • src/verifier.js+21 0 modified
    @@ -3,6 +3,8 @@
     const { createPublicKey, createSecretKey } = require('node:crypto')
     const Cache = require('mnemonist/lru-cache')
     
    +const safeRegex = require('safe-regex2')
    +
     const { hsAlgorithms, verifySignature, detectPublicKeyAlgorithms } = require('./crypto')
     const createDecoder = require('./decoder')
     const { TokenError } = require('./error')
    @@ -36,6 +38,19 @@ function prepareKeyOrSecret(key, isSecret) {
       return isSecret ? createSecretKey(key) : createPublicKey(key)
     }
     
    +function checkForUnsafeRegExp(raw, optionName) {
    +  const patterns = Array.isArray(raw) ? raw : [raw]
    +  for (const r of patterns) {
    +    if (r instanceof RegExp && !safeRegex(r)) {
    +      process.emitWarning(
    +        `The ${optionName} option contains an unsafe RegExp ${r} that may cause a ReDoS attack. Please review it. ` +
    +          'See https://github.com/nearform/fast-jwt/security/advisories/GHSA-cjw9-ghj4-fwxf for details.',
    +        { code: 'FAST_JWT_UNSAFE_REGEXP' }
    +      )
    +    }
    +  }
    +}
    +
     function ensureStringClaimMatcher(raw) {
       if (!Array.isArray(raw)) {
         raw = [raw]
    @@ -519,6 +534,12 @@ module.exports = function createVerifier(options) {
         throw new TokenError(TokenError.codes.invalidOption, 'The requiredClaims option must be an array.')
       }
     
    +  if (allowedJti) checkForUnsafeRegExp(allowedJti, 'allowedJti')
    +  if (allowedAud) checkForUnsafeRegExp(allowedAud, 'allowedAud')
    +  if (allowedIss) checkForUnsafeRegExp(allowedIss, 'allowedIss')
    +  if (allowedSub) checkForUnsafeRegExp(allowedSub, 'allowedSub')
    +  if (allowedNonce) checkForUnsafeRegExp(allowedNonce, 'allowedNonce')
    +
       if (
         allowedCritHeaders !== undefined &&
         (!Array.isArray(allowedCritHeaders) || allowedCritHeaders.some(h => typeof h !== 'string' || h.length === 0))
    
  • test/verifier.spec.js+147 0 modified
    @@ -1778,6 +1778,153 @@ test('default errorCacheTTL should not cache errors when sub millisecond executi
       t.mock.timers.reset()
     })
     
    +async function captureWarnings(code, count, fn) {
    +  const collected = []
    +  let resolve
    +  const done = new Promise(r => (resolve = r))
    +  const onWarning = w => {
    +    if (w.code === code) {
    +      collected.push(w)
    +      if (collected.length === count) resolve()
    +    }
    +  }
    +  process.on('warning', onWarning)
    +  fn()
    +  const timeout = new Promise((_, reject) =>
    +    setTimeout(
    +      () => reject(new Error(`Timed out waiting for ${count} ${code} warnings (got ${collected.length})`)),
    +      500
    +    )
    +  )
    +  try {
    +    await Promise.race([done, timeout])
    +    return collected
    +  } finally {
    +    process.off('warning', onWarning)
    +  }
    +}
    +
    +async function captureWarning(code, fn) {
    +  let onWarning
    +  const warningPromise = new Promise(resolve => {
    +    onWarning = w => {
    +      if (w.code === code) resolve(w)
    +    }
    +    process.on('warning', onWarning)
    +  })
    +  fn()
    +  const timeout = new Promise((_, reject) =>
    +    setTimeout(() => reject(new Error(`Timed out waiting for ${code} warning`)), 500)
    +  )
    +  try {
    +    return await Promise.race([warningPromise, timeout])
    +  } finally {
    +    process.off('warning', onWarning)
    +  }
    +}
    +
    +test('createVerifier emits FAST_JWT_UNSAFE_REGEXP warning for unsafe RegExp in allowedAud', async t => {
    +  const w = await captureWarning('FAST_JWT_UNSAFE_REGEXP', () =>
    +    createVerifier({ key: 'secret', allowedAud: /^(a+)+X$/ })
    +  )
    +  t.assert.equal(w.code, 'FAST_JWT_UNSAFE_REGEXP')
    +  t.assert.ok(w.message.includes('allowedAud'))
    +  t.assert.ok(w.message.includes('/^(a+)+X$/'), 'warning should include the offending regex')
    +  t.assert.ok(w.message.includes('https://'), 'warning should include an advisory link')
    +})
    +
    +test('createVerifier emits FAST_JWT_UNSAFE_REGEXP warning for unsafe RegExp in allowedIss', async t => {
    +  const w = await captureWarning('FAST_JWT_UNSAFE_REGEXP', () =>
    +    createVerifier({ key: 'secret', allowedIss: /^(a+)+X$/ })
    +  )
    +  t.assert.equal(w.code, 'FAST_JWT_UNSAFE_REGEXP')
    +  t.assert.ok(w.message.includes('allowedIss'))
    +  t.assert.ok(w.message.includes('/^(a+)+X$/'), 'warning should include the offending regex')
    +})
    +
    +test('createVerifier emits FAST_JWT_UNSAFE_REGEXP warning for unsafe RegExp in allowedSub', async t => {
    +  const w = await captureWarning('FAST_JWT_UNSAFE_REGEXP', () =>
    +    createVerifier({ key: 'secret', allowedSub: /^(a+)+X$/ })
    +  )
    +  t.assert.equal(w.code, 'FAST_JWT_UNSAFE_REGEXP')
    +  t.assert.ok(w.message.includes('allowedSub'))
    +  t.assert.ok(w.message.includes('/^(a+)+X$/'), 'warning should include the offending regex')
    +})
    +
    +test('createVerifier emits FAST_JWT_UNSAFE_REGEXP warning for unsafe RegExp in allowedJti', async t => {
    +  const w = await captureWarning('FAST_JWT_UNSAFE_REGEXP', () =>
    +    createVerifier({ key: 'secret', allowedJti: /^(a+)+X$/ })
    +  )
    +  t.assert.equal(w.code, 'FAST_JWT_UNSAFE_REGEXP')
    +  t.assert.ok(w.message.includes('allowedJti'))
    +  t.assert.ok(w.message.includes('/^(a+)+X$/'), 'warning should include the offending regex')
    +})
    +
    +test('createVerifier emits FAST_JWT_UNSAFE_REGEXP warning for unsafe RegExp in allowedNonce', async t => {
    +  const w = await captureWarning('FAST_JWT_UNSAFE_REGEXP', () =>
    +    createVerifier({ key: 'secret', allowedNonce: /^(a+)+X$/ })
    +  )
    +  t.assert.equal(w.code, 'FAST_JWT_UNSAFE_REGEXP')
    +  t.assert.ok(w.message.includes('allowedNonce'))
    +  t.assert.ok(w.message.includes('/^(a+)+X$/'), 'warning should include the offending regex')
    +})
    +
    +test('createVerifier emits warning for various nested quantifier patterns that may cause ReDoS', async t => {
    +  const unsafePatterns = [/^(a+)+X$/, /(a*)+b/, /(\w+)+@/, /(a+)*b/, /(( a+))+X/, /(a{2,})+X/]
    +  for (const pattern of unsafePatterns) {
    +    const w = await captureWarning('FAST_JWT_UNSAFE_REGEXP', () =>
    +      createVerifier({ key: 'secret', allowedAud: pattern })
    +    )
    +    t.assert.equal(w.code, 'FAST_JWT_UNSAFE_REGEXP', `expected warning for pattern ${pattern}`)
    +  }
    +})
    +
    +test('createVerifier emits warning when an unsafe RegExp is among an array of allowed values', async t => {
    +  const w = await captureWarning('FAST_JWT_UNSAFE_REGEXP', () =>
    +    createVerifier({ key: 'secret', allowedAud: ['safe-audience', /^(a+)+X$/] })
    +  )
    +  t.assert.equal(w.code, 'FAST_JWT_UNSAFE_REGEXP')
    +  t.assert.ok(w.message.includes('allowedAud'))
    +  t.assert.ok(w.message.includes('/^(a+)+X$/'), 'warning should identify the specific offending regex')
    +})
    +
    +test('createVerifier emits one warning per unsafe RegExp when multiple are passed in the same option', async t => {
    +  const unsafePatterns = [/^(a+)+X$/, /(a*)+b/, /(\w+)+@/]
    +  const warnings = await captureWarnings('FAST_JWT_UNSAFE_REGEXP', 3, () =>
    +    createVerifier({ key: 'secret', allowedAud: unsafePatterns })
    +  )
    +  t.assert.equal(warnings.length, 3, 'should emit one warning per unsafe pattern')
    +  for (const [i, w] of warnings.entries()) {
    +    t.assert.equal(w.code, 'FAST_JWT_UNSAFE_REGEXP')
    +    t.assert.ok(w.message.includes(String(unsafePatterns[i])), `warning ${i} should name the offending regex`)
    +  }
    +})
    +
    +test('createVerifier does not emit warning for safe RegExp patterns in allowed options', async t => {
    +  let warningReceived = false
    +  const onWarning = w => {
    +    if (w.code === 'FAST_JWT_UNSAFE_REGEXP') warningReceived = true
    +  }
    +  process.on('warning', onWarning)
    +  t.after(() => process.off('warning', onWarning))
    +
    +  createVerifier({ key: 'secret', allowedAud: /^api\.company\.com$/ })
    +  createVerifier({ key: 'secret', allowedAud: /^[a-z]+$/ })
    +  createVerifier({ key: 'secret', allowedAud: /^admin$/ })
    +  createVerifier({ key: 'secret', allowedIss: /^https:\/\/auth\.example\.com$/ })
    +  createVerifier({ key: 'secret', allowedSub: /^user-\d+$/ })
    +
    +  await new Promise(resolve => setImmediate(resolve))
    +  t.assert.equal(warningReceived, false)
    +})
    +
    +test('tokens are still verified correctly with a safe RegExp in allowedAud', t => {
    +  t.mock.timers.enable({ now: 100000 })
    +  const sign = createSigner({ key: 'secret' })
    +  const token = sign({ aud: 'api.company.com' })
    +  const verifier = createVerifier({ key: 'secret', allowedAud: /^api\.company\.com$/ })
    +  t.assert.doesNotThrow(() => verifier(token))
    +})
     test('stateful RegExp /g flag must not cause non-deterministic claim validation - allowedAud', t => {
       t.mock.timers.enable({ now: 100000 })
       const sign = createSigner({ key: 'secret' })
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.