JWT Misconfiguration: Detection and Defense
JWT misconfiguration detection and defense — alg:none, RS256-to-HS256 confusion, and kid injection, with header-logging detection, Sigma rules, and MITRE mapping.
JWT misconfiguration almost always reduces to one mistake: the verifier lets the
token choose how it is checked. Three variants follow — alg:none acceptance,
RS256-to-HS256 algorithm confusion, and kid injection. The fix is a single rule:
the server pins the algorithm and key, and never reads them from the token. The
detection is equally tight: log the JWT header on every auth event and alert on
any algorithm your service does not issue.
JSON Web Tokens carry identity, so a forged token is an authentication bypass. This bug class has existed since Tim McLean’s 2015 disclosure of critical vulnerabilities in JWT libraries (CVE-2015-9235), yet new instances keep shipping.
What is a JWT misconfiguration?
A JWT misconfiguration is any verifier setup that lets an attacker influence how their own token is validated. Because a JSON Web Token is just identity the server trusts, a verifier that reads the algorithm or key from the attacker-supplied header can be tricked into accepting a forged token — an authentication bypass with no password, no MFA prompt, and no lockout.
This maps to MITRE ATT&CK T1550.001 — Use Alternate Authentication Material: Application Access Token and, for the resulting access, T1078 — Valid Accounts. The damage looks like legitimate use, which is exactly why header-level detection matters.
What are the JWT algorithm confusion attacks?
Every variant manipulates the token header to control verification, and each leaves a loggable fingerprint.
| Variant | What the attacker changes | Header fingerprint | Defense |
|---|---|---|---|
alg:none | Sets alg to none, drops the signature | "alg":"none" (any casing), empty 3rd segment | Reject none unconditionally |
| RS256 → HS256 | Switches alg, signs with the public key | HS256 token at an RS256-only service | Pin one algorithm family |
kid injection | Points key lookup at a file/query they control | Traversal/SQL chars in kid | Validate/whitelist kid |
In RS256→HS256 confusion specifically, the server verifies RSA tokens with its
public key (published at /.well-known/jwks.json by design), and the attacker
signs the forgery with that public key as the HMAC secret. A verifier that reads
the algorithm from the token validates it.
How to detect JWT attacks from auth telemetry
Your service knows which algorithm and key IDs it legitimately issues. Anything else in an inbound token header is an attack or a bug. Log the decoded JWT header on every auth event, and the variants light up. This is the same auth-event logging that surfaces broken access control.
Detect unexpected algorithms
If your service issues only RS256, an inbound HS256 or none token is, by
definition, not yours — the highest-fidelity JWT detection you can deploy.
title: Inbound JWT With Unexpected Algorithm
id: 6c1f3a92-darkpwn-illustrative
status: experimental
logsource:
product: application
service: auth
detection:
selection:
jwt_header_alg:
- 'none'
- 'None'
- 'NONE'
- 'nOnE'
- 'HS256' # for a service that only issues RS256
condition: selection
falsepositives:
- Services that legitimately accept symmetric algorithms (tune per service)
level: high Detect kid injection
A kid value containing path-traversal or SQL metacharacters is never legitimate
— it means the attacker is probing the key-lookup mechanism.
title: JWT kid Header Containing Injection Characters
id: 1e8d5b27-darkpwn-illustrative
status: experimental
logsource:
product: application
service: auth
detection:
selection:
jwt_header_kid|contains: ['../', '..\\', "'", ' or ', 'union select', '/dev/null']
condition: selection
falsepositives:
- Unusual but legitimate key-id naming schemes (audit before enforcing)
level: high Detect jku and x5u header injection
Two header parameters are more dangerous than kid and get far less attention. jku (JWK Set
URL) and x5u (X.509 URL) tell the verifier where to fetch the key from. A verifier that
honours them is asking the token to nominate its own trust anchor, which means an attacker who
sets jku to a host they control can sign a token with their own key and have it verified
successfully.
index=auth sourcetype=app:auth jwt_header_alg=*
| eval expected_alg="RS256"
| eval finding=case(
lower(jwt_header_alg)=="none", "alg_none_forgery_attempt",
jwt_header_alg!=expected_alg, "unexpected_algorithm",
isnotnull(jwt_header_jku) OR isnotnull(jwt_header_x5u), "key_source_injection",
match(jwt_header_kid, "(\.\./|\.\.\\\\|'|union\s+select|/dev/null)"), "kid_injection",
true(), null())
| where isnotnull(finding)
| stats count, values(finding) as findings, dc(src_ip) as sources by user, http_user_agent
| sort - count let window = 24h;
let issued =
AppAuthEvents
| where TimeGenerated > ago(window) and EventType == "TokenIssued"
| project JwtId = tostring(Claims.jti), IssuedAt = TimeGenerated, User = Principal;
AppAuthEvents
| where TimeGenerated > ago(window) and EventType == "TokenAccepted"
| extend JwtId = tostring(Claims.jti)
| join kind=leftanti issued on JwtId
| project TimeGenerated, Principal, JwtId, SourceIp, UserAgent, Endpoint
| order by TimeGenerated desc The second query is the one to build first if you can only build one. It does not care how the
token was forged — algorithm confusion, a leaked key, jku injection, or a technique nobody has
published yet. A token your service never minted being accepted by your service is a complete
detection for the entire forgery class, and it requires only that you log issuance and acceptance
with a shared identifier.
What telemetry do you need to detect JWT attacks?
This detection class fails most often on a missing log line rather than a missing rule.
1. The decoded JWT header on every authentication event. At minimum alg and kid; ideally
jku, x5u, and typ too. Most frameworks log nothing about the token, so this is usually a
small custom middleware change — and it is the single highest-value logging addition here.
2. A token identifier (jti) on both issuance and acceptance. This is what makes the
leftanti join above possible. Without it, you cannot distinguish a token you minted from one an
attacker forged.
3. Rejection events, not just successes. A burst of rejected alg:none attempts is
reconnaissance and is arguably more actionable than a single success, because it tells you an
attacker is probing before they find something that works.
4. Client identity alongside the token. Source IP, user agent, and the endpoint. Forged tokens frequently arrive from infrastructure that has never carried a legitimate session.
How do you rotate signing keys without breaking verification?
Key rotation is where JWT deployments most often break in production, and the failure is avoidable with one ordering rule.
The mechanism: your service publishes its public keys at a JWKS endpoint, each carrying a kid.
Tokens name their kid in the header, and verifiers select the matching key. Rotation replaces
the signing key over time.
The ordering rule is publish-before-use, retire-after-expiry:
- Generate the new key pair and publish the public key to JWKS alongside the current one.
- Wait for verifier caches to pick it up — longer than your longest JWKS cache TTL.
- Only then start signing new tokens with the new
kid. - Keep the old public key published until every token signed with it has expired.
- Remove the old key from JWKS.
A related trap: if your verifier fetches JWKS on demand and caches by kid, an attacker
supplying an unknown kid can trigger a fetch per request. Cache negative lookups and rate-limit
JWKS fetches, or the key-lookup path becomes a denial-of-service amplifier against your own
identity provider.
Which false positives will you actually see?
| False positive | Which rule it hits | Why it happens | Resolution |
|---|---|---|---|
| Legacy client on an old algorithm | Unexpected algorithm | Migration not finished | Scope the rule per service, track the migration to completion |
| Third-party integration | Unexpected algorithm | Partner signs with HS256 by agreement | Separate verifier and separate rule scope |
| Key rotation overlap | Unknown kid | New key not yet cached by all verifiers | Correlate against scheduled rotation events |
| Clock skew | exp/nbf failures | Client and server clocks drift | Allow small leeway, alert only beyond it |
| Load-test and synthetic traffic | Issuance/acceptance mismatch | Test harness mints its own tokens | Exclude synthetic principals by name |
| Token replay after logout | Acceptance with no issuance | Revocation list lag | Fix the lag; do not suppress the rule |
Unusual kid naming | kid injection | Some IdPs use path-like key IDs | Audit the naming scheme once, then anchor the regex |
The kid naming row is worth auditing rather than filtering. Some identity providers genuinely
use slash-separated key identifiers, which look like path traversal to a naive rule. Establish
what your providers actually emit, then write the pattern to match traversal sequences
specifically rather than any occurrence of a slash.
How do you triage a JWT anomaly alert?
- Determine whether the token was accepted or rejected. This is the whole triage fork. A
rejected
alg:noneis reconnaissance; an accepted one is an active compromise. - If accepted, treat it as authenticated intrusion immediately. A forged token that verified means the verifier is misconfigured and every session it issued is suspect.
- Identify the verifier. Which service, which library version, which configuration path. The bug is almost always one call site missing an explicit algorithm list.
- Pin the algorithm at that verifier and deploy. Containment here is a configuration change, not an isolation action, and it is fast.
- Enumerate what the forged principal could reach. The token’s claims tell you exactly what was authorized — read them rather than guessing.
- Rotate signing keys if key compromise is plausible, following the overlap procedure above.
- Hunt for the same header pattern across every other service. A misconfiguration in one verifier usually indicates a shared library default or a copied code pattern present elsewhere.
- Check for follow-on persistence — new API keys, OAuth grants, or registered devices created while the forged session was active, as covered in session hijacking detection.
Step 7 is what turns an incident into a fix. These bugs propagate by copy-paste and by shared internal libraries, so a single vulnerable verifier is strong evidence that others exist.
How to test your JWT detection
Validate the rules against your own staging service:
- Mint a normal token and confirm it passes and generates an issuance event.
- Submit an
alg:nonetoken (any casing) and confirm both rejection and an alert. - Submit an HS256 token signed with the public key and confirm rejection.
- Submit a
kidwith../and confirm the injection rule fires.
Bake those four cases into CI so a future library-default change fails the build, not production.
How to prevent JWT misconfiguration
One control closes the whole class; the rest are depth.
- Ignore
jku,x5u, andjwkheaders entirely. Resolve keys from your own configured JWKS URL, never from a URL the token supplies. If your library honours these by default, disable it explicitly — a token nominating its own trust anchor is not authentication. - Do not mix symmetric and asymmetric families on one verifier — that is the precondition for algorithm confusion.
- Validate
iss,aud, andexpon every token; missing issuer validation has enabled cross-realm token acceptance. - Keep JWT libraries patched and pinned — confusion bugs are fixed by upgrades, and the advisories are not always loudly announced.
- Use short
expwindows plus revocation for high-value sessions to shrink a forged token’s lifetime.
Should you be using JWTs at all?
Worth asking honestly, because a large share of JWT vulnerabilities exist only because the token
is self-validating. An opaque token — a random identifier the server looks up — has no algorithm
to confuse, no kid to inject, and no signature to forge.
| Signed JWT | Opaque token | |
|---|---|---|
| Verification | Local, no network call | Lookup against a store or introspection endpoint |
| Revocation | Hard — valid until exp | Immediate, delete the record |
| Forgery surface | alg confusion, kid/jku injection, key leaks | None; nothing to forge |
| Scaling cost | Excellent, stateless | Requires a fast shared store |
| Cross-service use | Natural, carries claims | Needs introspection |
| Best for | Short-lived access tokens between services | Sessions, refresh tokens, anything needing instant revocation |
The pragmatic split most mature systems land on: short-lived signed JWTs for access tokens where stateless verification is genuinely worth it, and opaque, server-stored refresh tokens where revocation matters more than scale. That combination keeps the performance benefit on the hot path while ensuring “log this session out right now” is a real capability rather than a promise that resolves whenever the token expires.
If you find yourself building a token blocklist to make JWT revocation work, you have reintroduced the state that JWTs were chosen to avoid — and at that point an opaque token is simpler, faster, and has a smaller attack surface.
Common JWT security mistakes
- Calling
verify(token, key)without pinning the algorithm. The library infers it from the token — the root cause of the whole class. - Accepting both HS* and RS* on one path. The precondition for confusion.
- Skipping claim validation. No
iss/aud/expcheck means stolen or cross-tenant tokens sail through. - Long-lived tokens with no revocation. A forged token is valid until it expires.
- Honouring
jku/x5ufrom the token. Letting the credential nominate its own trust anchor is not authentication. - Logging raw tokens. A live credential in the log aggregator, readable by more people than the application itself.
- Retiring a signing key before its tokens expire. Produces a burst of legitimate rejections that looks exactly like an attack and desensitises the team.
- Building a revocation blocklist. If you need one, the stateless benefit is already gone and an opaque token would be simpler and safer.
JWT security checklist
Pin this next to every verifier you own:
- Pin the expected algorithm explicitly at every verifier — never infer it from the token.
- Reject
alg:noneunconditionally, in any casing (lowercase before comparing). - Never accept both symmetric and asymmetric families on one verification path.
- Validate
iss,aud, andexpon every token. - Log the decoded JWT header (alg, kid) on every authentication event.
- Alert on any algorithm you do not issue, and on
kidvalues with traversal/injection chars. - Use short
expwindows plus revocation for high-value sessions. - Keep JWT libraries patched and pinned; track their CVEs (e.g. CVE-2022-21449).
- Add a CI test asserting
alg:noneand HS256-signed-with-public-key tokens are both rejected. - Ignore
jku,x5u, andjwkheaders — resolve keys only from your own configured JWKS URL. - Emit a
jtion issuance and acceptance so forged tokens surface as sessions you never minted. - Redact the signature segment before logging; never store raw tokens in the log pipeline.
- Rotate keys publish-before-use and retire-after-expiry, with an overlap longer than your JWKS cache TTL.
- Log key rotations as correlatable events so an unscheduled
kidchange is immediately suspicious. - Cache negative JWKS lookups and rate-limit fetches so unknown
kidvalues cannot amplify into a denial of service. - Alert on rejections as well as acceptances — a burst of rejected forgery attempts is reconnaissance worth acting on.
Items 11 and 16 are the two that most teams lack, and they are the two that provide coverage against forgery techniques nobody has published yet. Everything else on this list defends against a known variant; those two detect the outcome regardless of method. Build them first if you are prioritising, because they age well — a detection that keys on “a token exists that we never issued” does not need updating when the next library bug lands.
What should you log about token validation?
Detection here depends entirely on the validator emitting something when it rejects a token, and by default most libraries reject silently. That silence is why JWT attacks are usually invisible until an audit.
Log every rejection with the reason as a structured field — bad signature, wrong algorithm, expired, wrong issuer, wrong audience — plus the key id presented. Do not log the token itself: it is a credential, and logs are widely readable and long-retained. The key id and the failure reason are enough to investigate.
The reason a per-reason breakdown matters more than a total: the rejection classes have completely different meanings. A rise in expiry rejections is a clock or refresh problem. A rise in algorithm rejections is somebody probing your validator. Aggregated into a single “auth failures” counter, the second signal is invisible inside the first.
The takeaway
JWT security is one decision repeated everywhere: the server chooses the algorithm
and key, never the token. Pin it, reject none, log the header, and alert on any
algorithm you do not issue. JWTs rarely travel alone — see how the tokens get
issued in OAuth misconfiguration: a defensive review.
A valid token that was stolen rather than forged is a different problem entirely, covered in
session hijacking detection. Round out your coverage with
SQL injection detection and
the wider Detection Engineering pillar.
Training & tools referenced
Disclosure: Some links below are affiliate links. If you buy through them, darkpwn may earn a commission at no extra cost to you. We only recommend training and tools we actually use in our own lab, and affiliate links never influence editorial coverage.
- TryHackMeAuthorized labs to practice JWT attack detection and hardeningSecurity TrainingStart training
Frequently asked questions
What is the alg:none JWT attack?
The attacker sets the token's header algorithm to none and removes the signature. A verifier that trusts the header skips signature checking and accepts the forged token. The fix is to reject none unconditionally, in any casing, and to pin the expected algorithm.
How does RS256 to HS256 confusion work?
The server signs with an RSA private key and verifies with the public key, which is published by design. The attacker switches the header to HS256 and signs a forged token using that public key as the HMAC secret. A verifier that reads the algorithm from the token validates the forgery. Pin one algorithm family per verifier to close it.
How do you detect a forged JWT?
Log the decoded JWT header on every auth event and alert on any algorithm your service does not issue, on none in any casing, and on kid values containing traversal or injection characters. Forged sessions also show up as authenticated actions with no matching token-issuance event.
How do you prevent JWT algorithm confusion?
Always pass the expected algorithm and key explicitly to the verifier, never let the library infer them from the token, and never configure one verifier to accept both symmetric and asymmetric algorithm families. Validate iss, aud, and exp on every token.