alg: none JWT Vulnerability: Why It Still Bites in 2026
The JWT alg: none vulnerability lets attackers forge tokens without a secret. Here's how the exploit works and why libraries still trip on it in 2026.
alg: none JWT Vulnerability: Why It Still Bites in 2026
A JWT is supposed to be tamper-proof. The signature on the third segment is the entire reason anyone trusts the claims on the second. Then there's alg: none — three words that tell a verifier to skip the signature check, which a surprising number of production systems still honor. The bug was disclosed in 2015 and still appears in bounty reports a decade later. Here's why it works and how to make your stack immune.
TL;DR
alg: nonetells a JWT verifier to accept a token with no signature.- Vulnerable verifiers let an attacker forge any claim — admin role, another user ID, a fresh expiry.
- The bug is almost never in the JWT library; it's in how application code calls the verifier.
- Always pass an explicit algorithm allowlist and never derive the algorithm from the token header.
- Decode tokens locally with the iKit JWT Decoder — pasting them into a server tool leaks them.
What "alg: none" Actually Means
A Quick JWT Header Refresher
A JWT is three base64url-encoded segments joined by dots: header.payload.signature. The header is a tiny JSON object whose alg field tells the verifier which cryptographic algorithm produced the signature in the third segment. Most production tokens carry HS256 (HMAC with SHA-256) or RS256 (RSA-SHA-256). The verifier reads alg, picks the corresponding crypto primitive, recomputes the signature over header.payload, and compares it to the bytes after the second dot.
The Spec Loophole
RFC 7515 — JSON Web Signature — defines a special algorithm identifier called none. Its purpose is the Unsecured JWS: a token that travels inside a transport that already guarantees integrity, so an extra signature would be redundant. When alg is none, the signature segment is empty, and the dot before it is the last character. The spec is explicit that this mode should only be enabled when the application has consciously chosen it. The vulnerability appears when a verifier reads alg: none straight from the token and treats it as a binding instruction instead of a hostile suggestion.
Why It Exists
The original sin is symmetry. JWT verifiers were written to look at the header, look up the matching crypto routine, and run it. none was just another entry in the dispatch table. Treating attacker-controlled bytes as a routing key is a classic injection pattern, and JWT inherited it from the same family of mistakes that gave us SQL injection in the 90s and prototype pollution in the 2010s. The fix everywhere is the same: trust the verifier's configuration, not the token's claim about itself.
The Anatomy of an alg: none Attack
The attack has three steps, each one straightforward if you can see the bytes. The iKit JWT Decoder runs the decoding locally so you can practice on your own tokens without leaking them to a third-party site.
Step 1: Capture a Valid Token
Any logged-in user can copy their own JWT from the browser's DevTools Network panel — it's the Authorization: Bearer … header on any authenticated request. The attacker doesn't need the signing key. They just need the structure of a token your backend issues, including the claim names you use. A token in the wild is plenty.
Step 2: Strip the Signature and Rewrite the Header
The attacker replaces the header with one that names a different algorithm and then drops the third segment entirely. The new header in plain JSON looks like this:
{
"alg": "none",
"typ": "JWT"
}
Base64url-encode it, paste it back as the first segment of the token, and leave a trailing dot where the signature used to be.
Step 3: Edit the Claims and Send It
Now the payload. The attacker decodes the original payload, edits the fields that the backend trusts — sub to another user's ID, role to admin, exp to a year from now — re-encodes it, and assembles the new three-segment string. The result looks like this:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.
eyJzdWIiOiIxMjM0IiwiZXhwIjoxNzkwMDAwMDAwLCJyb2xlIjoiYWRtaW4ifQ.
That trailing dot is mandatory — RFC 7515 says an unsecured JWS still has three segments, the last one just empty. A vulnerable verifier reads the header, sees none, skips the signature check, and hands the forged claims to your authorization code. The attacker is now whoever they say they are.
Why Libraries Are Still Vulnerable in 2026
The headline JWT libraries fixed the default years ago. PyJWT raised an exception on none in 2017, jsonwebtoken added the explicit-algorithms parameter the same year, and Microsoft's Microsoft.IdentityModel.Tokens has never accepted it. So why does the bug keep showing up in bounty reports? Because the vulnerability migrated from the library to the application code that calls it.
The "Verify with the Public Key" Trap
This is the canonical re-introduction. The application code does something like this:
const header = jwt.decode(token, { complete: true }).header;
const verified = jwt.verify(token, publicKey, {
algorithms: [header.alg],
});
The intent is to support both RS256 and ES256 from the same verifier. The effect is that the attacker controls the verification algorithm. Pass none in the header and the algorithm allowlist becomes ["none"], which skips the check. The same pattern shows up when a developer reads alg from the JWKS lookup logic and threads it back into verify(). Trust the verifier's static config, never the token's runtime header.
The Default-Algorithm Pitfall
Older library versions and some less-maintained ports default to "accept whatever the header says." A team upgrades the library but pins to an old major version because of a breaking-change anxiety, and the default never tightens. The mitigation is to audit your lockfile every quarter. Compare versions across jsonwebtoken, pyjwt, jose, python-jose, and java-jwt against their security advisories. If you're more than two majors behind, that's a finding all by itself.
Casing Variants (NONE, None, nOnE)
The original 2015 disclosure also covered case-insensitive matching. A verifier that compares alg == "none" rejects the lowercase string but accepts None, NONE, or nOnE. RFC 7518 defines alg values as case-sensitive — but several historical libraries did case-folding before the comparison. A modern verifier should never accept any variant of none, regardless of casing, unless an Unsecured JWS is an explicit application requirement. Test the four obvious casings in CI.
A Comparison of Verification Patterns
| Pattern | Safe? |
|---|---|
verify(token, key, { algorithms: ['HS256'] }) |
Safe — explicit allowlist |
verify(token, key, { algorithms: [header.alg] }) |
Vulnerable — attacker chooses |
verify(token, key) with no algorithms option |
Depends on library default; audit |
decode(token) then trust claims directly |
Always wrong — no verification at all |
The pattern audit is more productive than scanning for the literal string none. If your verifier call doesn't carry an explicit algorithm allowlist, treat that line as a bug regardless of whether the library is safe today.
How to Make Your Stack Immune
Three changes cover almost every real production codebase, and they line up with the OWASP JWT cheat sheet guidance. Apply them in order; the first one alone neutralizes the original CVE.
1. Pin the Algorithm Allowlist
Every call to your JWT verifier must pass an explicit list of acceptable algorithms. Pick one — HS256 or RS256 — and stick to it. If you genuinely issue tokens with two different algorithms (for example, internal service tokens with HS256 and customer tokens with RS256), use two distinct verifier instances with distinct configurations rather than a single verifier that accepts both. The two-verifier pattern makes it impossible for a token issued for one trust boundary to be accepted by the other.
2. Whitelist in Auth0, Firebase, and Cognito
If you're using a managed identity provider, the same rule applies one layer up. Auth0's tokens.json config has a signingAlg field; Firebase Admin SDK accepts an algorithms array on verifyIdToken(); Cognito's hosted UI lets you select the algorithm in the App Client settings. Set them explicitly, even if the default looks correct today. A vendor changing a default in a future release is exactly the kind of silent regression that won't show up until it's exploited.
3. Write a Forged-Token Regression Test
This is the highest-leverage change you can make in five minutes. Add a test that builds a token with {"alg":"none"} and a junk payload, hands it to your verifier, and asserts the verifier rejects it. The test should also cover three other negative cases:
- A token with a flipped signature byte
- A token signed with the wrong algorithm (HS256 token when RS256 is expected)
- A token whose
kidpoints to a key that doesn't exist
Each one of these is a real CVE pattern. Together they form a four-line guardrail that fails loudly the moment someone re-introduces the vulnerability while refactoring auth.
import jwt from 'jsonwebtoken';
import { verifyToken } from '../auth';
test('rejects alg: none', () => {
const header = Buffer.from(
'{"alg":"none","typ":"JWT"}'
).toString('base64url');
const payload = Buffer.from(
'{"sub":"1","role":"admin"}'
).toString('base64url');
const forged = `${header}.${payload}.`;
expect(() => verifyToken(forged)).toThrow();
});
Run it on every PR. Once the test exists, the entire class of bugs becomes a CI failure instead of a production incident.
Real-World CVEs and Bug Bounties
The vulnerability has a long tail. A non-exhaustive list of the disclosures developers still cite when teaching JWT security:
| Year | Product | Identifier |
|---|---|---|
| 2015 | Multiple JWT libraries (original disclosure by Tim McLean) | Auth0 advisory |
| 2018 | Inversoft prime-jwt | CVE-2018-1000531 |
| 2022 | jsonwebtoken (custom verify patterns) | CVE-2022-23529 |
| 2024 | python-jose (algorithm confusion) | CVE-2024-33663 |
Each one is a slightly different shape of the same root cause: the verifier honored attacker-controlled bytes when picking the verification algorithm. The 2024 entry is the most instructive — it's nine years after the original disclosure, and the underlying mistake is recognizable from the 2015 write-up. The lesson is operational, not cryptographic. Defaults drift. New code paths get added. Allowlists get widened during refactors. The forged-token test is the single thing that keeps the regression from shipping.
Where to Go Next
If alg: none is your first hard look at JWT security, the next two questions worth answering are which algorithm to standardize on and how to read the claims your verifier actually trusts. Pin a single algorithm with the HS256 vs RS256 guide, then walk through the registered claims so you know which fields your verifier should be enforcing alongside the signature check.
When you need to decode a real token to verify a fix, do it locally. The iKit JWT Decoder runs entirely in your browser — the token never crosses the network — and the rest of the iKit suite covers the related primitives you'll reach for in the same debugging session, from SHA-256 verification to base64 decoding for inspecting raw header bytes.
References
- RFC 7515: JSON Web Signature (JWS) — Section 2 defines the Unsecured JWS used when
algisnone. - RFC 7518: JSON Web Algorithms (JWA), Section 3.6 — Specifies the
nonealgorithm and its case-sensitive identifier. - RFC 8725: JSON Web Token Best Current Practices — Sections 2.1 and 3.1 cover the
alg:noneand algorithm-confusion mitigations. - OWASP JSON Web Token Cheat Sheet — Recommends explicit algorithm allowlists and library-level rejection of
none. - Critical vulnerabilities in JSON Web Token libraries — Auth0 — Tim McLean's 2015 disclosure of the original
alg:nonebypass. - CVE-2018-1000531: Inversoft prime-jwt signature bypass — Concrete example of a verifier accepting a token with
alg:none.
Related on iKit
- HS256 vs RS256: Which JWT Algorithm — HS256 vs RS256 is the decision that makes the
alg: nonefix sticky; the algorithm-allowlist rule only works if you've committed to a single value first. - JWT Standard Claims Explained — once your verifier rejects unsigned tokens, the next failure mode is a valid signature over a payload your code shouldn't trust; the claims guide is the field-by-field follow-up.
- Decode JWT: Auth0 & Firebase Examples — practical companion for inspecting the headers and payloads referenced in this post against real production tokens.
Related posts
URL Encode Online: Stop Pasting Sensitive URLs (2026)
URL encode online without leaking auth tokens, S3 links, or password-reset codes. Here's where pasted URLs actually end up — and how to encode privately.
How to Create Strong Passwords You'll Actually Remember
Strong passwords don't need to be random gibberish. Learn four memorable patterns that meet NIST rules — plus when to let a generator do the work.