fast-jwt CVE-2026-34950: how leading whitespace re-enabled algorithm confusion
The RS256-to-HS256 algorithm-confusion attack has been documented since 2015. Most JWT libraries now block it. CVE-2026-34950 is a reminder that a fix can have a gap.
In fast-jwt, a Node.js JWT library, versions 6.1.0 and earlier
could mistake an RSA public key for an HMAC secret if the key string
started with whitespace: a space, a tab or a newline. That re-opened
the exact attack that CVE-2023-48223 had fixed in 2023. The advisory
rates it Critical (CVSS 9.1). It’s fixed in 6.2.0.
This post covers the attack, what went wrong in fast-jwt, how to tell whether you’re affected, and what to check in your own code whatever library you use.
The attack class: algorithm confusion
A JWT header names the algorithm used to sign it. Two families matter here:
- HMAC (
HS256,HS384,HS512) is symmetric. Signer and verifier share one secret. - RSA (
RS256, etc.) is asymmetric. A private key signs, and the matching public key verifies.
Say your service signs tokens with RS256. The public key is public,
by design. An attacker builds a token with "alg": "HS256" and
computes an HMAC over it using your public key as the HMAC secret.
If the verifier lets the token’s header choose the algorithm, it runs HMAC-SHA256 with the public key as the secret. The result matches what the attacker computed, and the forged token is accepted.
RFC 8725, the
JWT best-practices RFC, describes this attack and cites
CVE-2015-9235, a
2015 bug in the Node.js jsonwebtoken library (fixed in 4.2.2). Its
fix, in section 3.1, is that the caller chooses which algorithms are
allowed, and “each key MUST be used with exactly one algorithm.”
The fast-jwt history
With fast-jwt, you create a verifier with createVerifier(). The
algorithms option is optional. The README says: “By default, all
algorithms are accepted.” In practice, if you leave algorithms
out, fast-jwt looks at the key you passed and picks the allowed
algorithms from it. An RSA public key allows the RSA algorithms. A
plain string allows HS256, HS384 and HS512.
That detection is where both bugs lived.
CVE-2023-48223 (2023). fast-jwt recognised a public key by looking
for the text -----BEGIN PUBLIC KEY-----. An RSA key in the older
PKCS #1 format starts with -----BEGIN RSA PUBLIC KEY----- instead,
so it wasn’t recognised and was treated as an HMAC secret. Version
3.3.2 (November 2023) fixed this by switching to a regular expression
that matches both headers. In 3.3.2 it read:
const publicKeyPemMatcher = /^-----BEGIN( RSA)? PUBLIC KEY-----/
The CVE-2026-34950 gap
The ^ at the start of that regular expression means “at the very
start of the string.” If the key string has anything in front of the
PEM header, even one space or newline, the match fails. fast-jwt then
decided the key wasn’t a public key and fell back to treating it as
an HMAC secret. That’s the same outcome as the 2023 bug. The code
that detects private keys trimmed whitespace first. The public-key
check didn’t.
The advisory points out that leading whitespace is common in real deployments. Keys loaded from database text columns, YAML multi-line strings, environment variables and copy-pasted config files can all pick up a leading newline.
You were affected if all of these were true:
- you verify RS256 tokens with fast-jwt 6.1.0 or earlier,
- the public key string you pass in has whitespace before the PEM header, and
- you call
createVerifierwithout an explicitalgorithmslist.
The attacker needs nothing except your public key.
The advisory was published on April 2, 2026. The fix shipped in 6.2.0 on April 7, 2026, via pull request #598, which trims the key before checking what kind of key it is. That fix turned out to be incomplete; see step 1 under “What to do”.
How to tell if you’re affected
Check which version you have. fast-jwt is often installed indirectly,
for example through @fastify/jwt, so look at the whole tree:
npm ls fast-jwt
# or
pnpm ls fast-jwt
# Affected by CVE-2026-34950: 6.1.0 and earlier
# Fixed for that CVE: 6.2.0 and later
# Later advisories: see step 1 below (use 6.3.1 or later)
npm audit also reports this advisory. One side effect can help you
spot a problem key. On an affected version, a whitespace-prefixed key
also makes fast-jwt reject your real RS256 tokens with “The token
algorithm is invalid.” The advisory’s proof of concept shows this. If
you’ve seen that error, check the key string. Don’t treat the absence
of that error as proof you’re safe.
What to do
-
Upgrade. 6.2.0 fixes CVE-2026-34950. It isn’t the last fast-jwt fix, though. Later advisories were fixed in 6.2.1 (CVE-2026-35040, CVE-2026-35041) and 6.2.4 (CVE-2026-44351, a critical bypass involving async key resolvers). Advisories published in July 2026 include GHSA-ww5h-9m49-7xx4 (the 6.2.0 fix was incomplete: keys with leading content other than whitespace could still be treated as an HMAC secret; fixed in 6.3.0) and GHSA-8wpc-h4q6-8fxv (an empty or null key with
algorithmsset could accept unsigned tokens; fixed in 6.3.1). Move to the latest release and no lower than 6.3.1. Check the advisories list for anything newer. -
Pin the algorithm. Pass
algorithmsevery time:import { createVerifier } from "fast-jwt"; const verify = createVerifier({ key: publicKey, algorithms: ["RS256"], }); const payload = verify(token);With
algorithms: ["RS256"], even an affected version refuses to treat the key as an HMAC secret. It throws “Invalid public key provided for algorithms RS256.” instead of accepting a forged token. That error is a loud failure, which is what you want. -
Load keys carefully.
publicKey.trim()is what the 6.2.0 fix does, and it costs nothing, but it turned out not to be enough (see step 1). Treat pinningalgorithmsas the main defence, and make sure the key you pass in is the PEM you expect, never an empty string or null. -
Check your logs (see below) for tokens verified with an
algyou don’t issue.
What to check in your own JWT code
The same class of bug turns up in any language when the verifier lets the token pick its algorithm.
1. Are you passing algorithms to your verifier?
Most JWT libraries have this option. Always pass it explicitly. Don’t rely on the default, because defaults differ between libraries and versions.
# PyJWT
import jwt
jwt.decode(token, public_key, algorithms=["RS256"])
// golang-jwt v5
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
return publicKey, nil
}, jwt.WithValidMethods([]string{"RS256"}))
// jsonwebtoken (Node.js)
import jwt from "jsonwebtoken";
jwt.verify(token, publicKey, { algorithms: ["RS256"] });
// jsonwebtoken (Rust)
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
let validation = Validation::new(Algorithm::RS256);
decode::<Claims>(token, &DecodingKey::from_rsa_pem(public_key_pem)?, &validation)?;
2. Are you rejecting alg: none?
none is a separate attack, not algorithm confusion. It’s a
registered value meaning “no signature,” and
RFC 7518 says
implementations “MUST NOT accept Unsecured JWSs by default.” Some
libraries used to accept them anyway. Pinning algorithms covers
this too. You can paste a token into the JWT decoder to see its
alg.
3. Does each key match one algorithm?
If you fetch keys from a JWKS endpoint, the kid in the token only
tells you which key to use. It says nothing about which algorithm is
allowed with it. Check that the key type matches the algorithm you
expect: an RSA key for RS256, an EC key for ES256. If the JWK
carries an alg, check that too. RFC 8725 also warns that kid is
untrusted input. Validate it before using it in a lookup, because it
can carry SQL or LDAP injection.
4. Would your logs show forged tokens?
If someone started sending HS256 tokens signed with your public key, would you notice?
- Log the
algheader of every token you verify, or at least of every token you reject. - Alert on any
algyou don’t issue. - Watch the rate of “algorithm not allowed” errors. A sudden rise suggests someone is probing.
Test your own verifier
You can check a verifier against this attack in a few minutes. Run it against a test instance you own, not someone else’s service.
// forge-test.mjs: prints an HS256 token signed with your RSA public key
import { createHmac } from "node:crypto";
import { readFileSync } from "node:fs";
// Use the exact string your verifier loads, including any leading whitespace
const publicKey = readFileSync("public.pem", "utf8");
const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64url");
const header = b64url({ alg: "HS256", typ: "JWT" });
const payload = b64url({ sub: "forged-test", exp: Math.floor(Date.now() / 1000) + 300 });
const sig = createHmac("sha256", publicKey).update(`${header}.${payload}`).digest("base64url");
console.log(`${header}.${payload}.${sig}`);
Send the printed token to your verifier. If it’s accepted, you have an algorithm-confusion bug. Try it once with the key as stored and once with a newline added in front.
Use a script for this rather than our Sign tool. Sign’s secret box is a single-line field, so a multi-line PEM pasted into it loses its line breaks and won’t match the bytes your server uses.
Do you need to rotate keys?
Algorithm confusion is a flaw in the verifier’s logic. It doesn’t leak your private key, so upgrading doesn’t by itself mean rotating keys. What you do need to do:
- Upgrade the library.
- Search your logs for any token that passed verification with an
algyou don’t issue. If you find one, treat it as a breach: rotate keys, end sessions and force users to sign in again.
If your private key may have been exposed some other way, such as a leaked credential or a compromised CI system, rotate it regardless.
The big picture
Bugs in JWT libraries are usually about how keys and algorithms are
matched up, not about broken cryptography. This one came down to a
single ^ in a regular expression and a missing .trim(). The same
three habits defend against the whole class in every language:
Pin the expected algorithm. Don’t let the token’s header decide. Treat every key-loading path as input that needs checking.
Sources and further reading
- GitHub advisory GHSA-mvf2-f6gm-w987 (CVE-2026-34950): affected ≤ 6.1.0, patched 6.2.0
- NVD: CVE-2026-34950
- fast-jwt v6.2.0 release notes and the fix, PR #598
- GitHub advisory GHSA-c2ff-88x2-x9pg and NVD: CVE-2023-48223, the original bug
- All fast-jwt security advisories
- RFC 8725: JWT Best Current Practices, sections 2.1 and 3.1
- Critical vulnerabilities in JSON Web Token libraries, Tim McLean’s 2015 write-up of the original attacks
- PortSwigger Web Security Academy: JWT algorithm confusion
- Our guides: What is a JWT?, JWT decode vs verify, JWT vs sessions
- Related tools: hash.tooljo.com for the underlying hash functions, base64.tooljo.com/url-safe for inspecting the raw segments of a token, epoch.tooljo.com for the timestamp claims.