Detection Engineering

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.

A hardware security key on a black workbench beside a screen showing a segmented token, one segment edged in red

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.

VariantWhat the attacker changesHeader fingerprintDefense
alg:noneSets alg to none, drops the signature"alg":"none" (any casing), empty 3rd segmentReject none unconditionally
RS256 → HS256Switches alg, signs with the public keyHS256 token at an RS256-only servicePin one algorithm family
kid injectionPoints key lookup at a file/query they controlTraversal/SQL chars in kidValidate/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.

Sigma Inbound JWT With Unexpected Algorithm
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.

Sigma JWT kid Header Containing Injection Characters
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.

SPL JWT Header Anomalies — Algorithm, kid, and Key-Source Injection
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
KQL Forged-Token Aftermath — Authenticated Action With No Issuance Event
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:

  1. Generate the new key pair and publish the public key to JWKS alongside the current one.
  2. Wait for verifier caches to pick it up — longer than your longest JWKS cache TTL.
  3. Only then start signing new tokens with the new kid.
  4. Keep the old public key published until every token signed with it has expired.
  5. 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 positiveWhich rule it hitsWhy it happensResolution
Legacy client on an old algorithmUnexpected algorithmMigration not finishedScope the rule per service, track the migration to completion
Third-party integrationUnexpected algorithmPartner signs with HS256 by agreementSeparate verifier and separate rule scope
Key rotation overlapUnknown kidNew key not yet cached by all verifiersCorrelate against scheduled rotation events
Clock skewexp/nbf failuresClient and server clocks driftAllow small leeway, alert only beyond it
Load-test and synthetic trafficIssuance/acceptance mismatchTest harness mints its own tokensExclude synthetic principals by name
Token replay after logoutAcceptance with no issuanceRevocation list lagFix the lag; do not suppress the rule
Unusual kid namingkid injectionSome IdPs use path-like key IDsAudit 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?

  1. Determine whether the token was accepted or rejected. This is the whole triage fork. A rejected alg:none is reconnaissance; an accepted one is an active compromise.
  2. 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.
  3. Identify the verifier. Which service, which library version, which configuration path. The bug is almost always one call site missing an explicit algorithm list.
  4. Pin the algorithm at that verifier and deploy. Containment here is a configuration change, not an isolation action, and it is fast.
  5. Enumerate what the forged principal could reach. The token’s claims tell you exactly what was authorized — read them rather than guessing.
  6. Rotate signing keys if key compromise is plausible, following the overlap procedure above.
  7. 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.
  8. 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:

  1. Mint a normal token and confirm it passes and generates an issuance event.
  2. Submit an alg:none token (any casing) and confirm both rejection and an alert.
  3. Submit an HS256 token signed with the public key and confirm rejection.
  4. Submit a kid with ../ 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, and jwk headers 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, and exp on 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 exp windows 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 JWTOpaque token
VerificationLocal, no network callLookup against a store or introspection endpoint
RevocationHard — valid until expImmediate, delete the record
Forgery surfacealg confusion, kid/jku injection, key leaksNone; nothing to forge
Scaling costExcellent, statelessRequires a fast shared store
Cross-service useNatural, carries claimsNeeds introspection
Best forShort-lived access tokens between servicesSessions, 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/exp check means stolen or cross-tenant tokens sail through.
  • Long-lived tokens with no revocation. A forged token is valid until it expires.
  • Honouring jku/x5u from 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:

  1. Pin the expected algorithm explicitly at every verifier — never infer it from the token.
  2. Reject alg:none unconditionally, in any casing (lowercase before comparing).
  3. Never accept both symmetric and asymmetric families on one verification path.
  4. Validate iss, aud, and exp on every token.
  5. Log the decoded JWT header (alg, kid) on every authentication event.
  6. Alert on any algorithm you do not issue, and on kid values with traversal/injection chars.
  7. Use short exp windows plus revocation for high-value sessions.
  8. Keep JWT libraries patched and pinned; track their CVEs (e.g. CVE-2022-21449).
  9. Add a CI test asserting alg:none and HS256-signed-with-public-key tokens are both rejected.
  10. Ignore jku, x5u, and jwk headers — resolve keys only from your own configured JWKS URL.
  11. Emit a jti on issuance and acceptance so forged tokens surface as sessions you never minted.
  12. Redact the signature segment before logging; never store raw tokens in the log pipeline.
  13. Rotate keys publish-before-use and retire-after-expiry, with an overlap longer than your JWKS cache TTL.
  14. Log key rotations as correlatable events so an unscheduled kid change is immediately suspicious.
  15. Cache negative JWKS lookups and rate-limit fetches so unknown kid values cannot amplify into a denial of service.
  16. 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 Training
    Start 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.