Module 13 · JWT Attacks

Manish Garg
Manish Garg Associate of (ISC)² · RingSafe
Apr 19, 2026
12 min read
Read as

Last updated: May 1, 2026

100% Free

No signup. No paywall. No catch. One of our 10 most-requested practitioner modules — published in full so anyone can learn for free. We earn through consulting, not by gating knowledge.

See all 10 free modules →

JSON Web Tokens (JWT) have become the default authentication token format in modern APIs. They’re compact, stateless, and when implemented correctly, secure. When implemented poorly, they’re a source of authentication bypass and privilege escalation.

JSON Web Tokens (JWT) have become the default authentication token format in modern APIs. They’re compact, stateless, and when implemented correctly, secure. When implemented poorly, they’re a source of authentication bypass and privilege escalation. This module covers JWT structure, common attacks, and the concrete defences.

JWT structure

header.payload.signature

# Base64-decoded example:
Header:    {"alg":"HS256","typ":"JWT"}
Payload:   {"sub":"priya","role":"admin","exp":1700000000}
Signature: HMAC-SHA256(header + "." + payload, SECRET)

Signature is what makes JWT tamper-evident. Change the payload, signature no longer matches, verifier rejects.

Attack 1: alg=none

JWT spec includes an “alg” header field. One value: none — meaning no signature. Historical JWT libraries accepted alg:none at verification — trusting the header without actually checking the signature.

# Modified JWT with alg=none:
eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiJ9.
# (empty signature, decoded header says alg=none)

# Vulnerable verifier: signature check skipped because alg=none
# Attacker is now admin

Defence: explicitly reject alg: none. Whitelist allowed algorithms. Most modern libraries fixed this but legacy code still appears.

Attack 2: Algorithm confusion (HS256 vs RS256)

HS256 = HMAC with shared secret. RS256 = RSA signature with public/private keypair. If server uses RS256 but an older library uses the “verify key” inconsistently:

# Attacker knows RSA public key (typically published /.well-known/jwks.json)
# Attacker creates JWT with alg=HS256
# Signs with the public key as the "HMAC secret"
# Vulnerable verifier: "alg says HS256, so use secret for HMAC verification"
# Picks up the public key (normally used for RSA verification)
# HMAC-verifies with public key — passes!

Defence: explicit algorithm pinning in verification code. Don’t trust header’s alg; configure verifier to require specific alg.

Attack 3: Weak HMAC secret

HS256 uses a shared secret. If secret is weak (short, common word, default), attacker cracks it offline using the JWT itself:

# Capture any JWT
# Extract header.payload.signature
# Run hashcat in JWT mode
hashcat -m 16500 jwt.txt rockyou.txt

# Once secret is known, forge arbitrary JWTs

Defence: cryptographically random secrets ≥256 bits. Rotate periodically. Use RS256 / EdDSA for public-facing APIs (asymmetric — signing key kept private).

Attack 4: JWKS injection / kid confusion

kid (Key ID) header tells verifier which key to use. Attacker can:

  • Set kid to path traversal: ../../etc/passwd — if verifier reads key from disk, attempts to read that file as key
  • Set jku (JWKS URL) to attacker-controlled endpoint — if verifier fetches keys from jku, fetches attacker’s public key
  • Set x5u (X.509 URL) similarly

Defence: never trust header-specified key sources. Hard-code or whitelist JWKS URLs. Validate kid against a known-good list.

Attack 5: Token reuse / replay

JWTs are valid until expiry. If stolen, attacker uses them until expiry. No server-side revocation unless explicit denylist.

Defences:

  • Short expiry (minutes, not hours)
  • Refresh tokens (long-lived, revocable, server-tracked)
  • Token binding (bind token to TLS channel or device)
  • Denylist on logout or password reset

Attack 6: Claim injection

If application trusts JWT claims for authorization (e.g. role=admin), and JWT signing is broken (via alg=none, weak secret, or confusion), attacker injects claims. Always validate claims server-side in addition to signature — e.g. verify user exists, role matches user’s actual role in DB.

Storage — the client-side question

  • localStorage — JS-accessible, vulnerable to XSS but not CSRF
  • HttpOnly cookie — not JS-accessible, vulnerable to CSRF but not XSS
  • Memory (SPA) — safest but lost on page reload

Best practice: HttpOnly + Secure + SameSite=Lax cookie for session tokens; implement CSRF token for state-changing actions.

Defences summary

  1. Explicit algorithm whitelist at verification
  2. Strong random secrets for HS* algorithms (≥256 bits)
  3. Prefer asymmetric (RS256, ES256, EdDSA) for public APIs
  4. Short expiry + refresh token pattern
  5. Claims validation beyond signature (check against server truth)
  6. Never trust header-specified key sources (jku, x5u)
  7. Implement revocation/denylist for critical scenarios
  8. Use established libraries (jsonwebtoken, PyJWT with explicit options) — don’t roll your own

Quick reference summary

  • JWT = header.payload.signature, base64-encoded
  • Attacks: alg=none, HS/RS confusion, weak secrets, JWKS injection, token replay
  • Modern libraries patched alg=none; rare in current code, common in legacy
  • Defences: explicit alg whitelist, strong secrets, asymmetric for public APIs, short expiry, claims validation, never trust header-specified keys
  • Storage trade-offs: localStorage (XSS risk) vs HttpOnly cookie (CSRF risk)
🧠
Check your understanding

Module Quiz · 20 questions

Pass with 80%+ to mark this module complete. Unlimited retries. Each question shows an explanation.

Real-World Case Study: The “alg: none” generation

The story. Between 2015 and 2019, a generation of OAuth and JWT libraries shipped with a critical implementation flaw: they accepted "alg": "none" as a valid signature algorithm. The list of affected libraries reads like a who’s-who of authentication: jsonwebtoken (Node), pyjwt, jjwt (Java), Ruby JWT, Auth0’s own libs, and dozens more. Every one of them, at some point, would accept an unsigned token as valid.

The technical chain.

  1. JWT spec defines an "alg" header field — HS256, RS256, etc — that names the signing algorithm.
  2. The spec also defines "alg": "none" — meant for unsigned tokens in trusted environments.
  3. Naïve library implementations validated the signature using whatever alg the token claimed. If alg was none, no signature check happened.
  4. Attacker forges any JWT they like — change the "sub" to admin’s user ID — set "alg" to none — strip the signature — submit. The library accepts it.

The exploit, in a single curl.

HEADER='{"alg":"none","typ":"JWT"}'
PAYLOAD='{"sub":"admin","exp":9999999999}'
TOKEN=$(echo -n "$HEADER" | base64 | tr -d '=' | tr '/+' '_-').$(echo -n "$PAYLOAD" | base64 | tr -d '=' | tr '/+' '_-').
curl -H "Authorization: Bearer $TOKEN" https://api.target.com/admin/users

Algorithm confusion (HS256 ↔ RS256). A second-generation attack: take a public-key-signed token (RS256), change alg to HS256, and use the server’s public key as the HMAC secret. Vulnerable libraries use the public key string as the symmetric secret. The forged signature validates.

The takeaway. Pin the algorithm server-side. Never trust the alg header. Reject none at the application layer. Test your auth flow with the jwt_tool nightly.


⚙ Optimisation · Performance · Security — extended

Practical depth on what to tune, what to harden, and how this maps to Indian regulatory expectations.

JWT pitfalls — what goes wrong in 80% of audits

1alg=none — server accepts unsigned tokens. Library bug from 2015 still present in poorly-maintained code.
2RS256→HS256 confusion — server expects RS256 but library signature-verifies with the public key as HMAC secret.
3Weak HS256 secret — easily brute-forced; recovered secret allows arbitrary token forgery.
4kid header SQLi / path traversalkid claim used to look up signing key in DB / filesystem without sanitisation.
5jku / x5u — server fetches signing key from URL in token; attacker hosts their own key.
6No expiry / very long expiry — stolen tokens valid for weeks.
7No revocation list — logged-out tokens still work.
8Sensitive data in payload — JWT is base64, not encrypted; payload visible to anyone with the token.

JWT defensive design

1Pin algorithm server-side — never accept the token’s alg field; reject unexpected algs.
2RS256 or EdDSA for production; HS256 only when client and server share infra.
3Short access-token expiry (5-15 min) + refresh-token rotation.
4Audience / issuer validation on every verify.
5Revocation: track JWT IDs (jti) in Redis; check on every request for revoked.
6Claim-minimal payload: user-id + role + expiry; load other data server-side.
7Use a battle-tested library — never roll your own. For Indian fintech: RBI master direction on digital banking expects token revocation and short expiry; long-lived JWTs are an audit finding.

Detection + India context

JWT-attack indicators in logs: 401s with valid-format-but-bad-signature tokens; tokens with unusual alg values; kid values containing path traversal; tokens issued in the past replayed long after natural expiry.

SOC playbookalert on cluster of these from one source.

India contextJWT-based authentication is now standard in Indian fintech, e-commerce, and government portals (DigiLocker, UMANG, Aadhaar e-KYC tokens). Misconfiguration leads to account takeover; under DPDP that is a notifiable breach. Review JWT handling as part of every Indian VAPT.

JWT in microservices — the propagation problem

In a microservice mesh, the client gets one JWT and the request flows through many services. Three patterns:

1Forward the original JWT — every service validates independently; signature key must be available everywhere. Concern: if the JWT contains the original audience, downstream services may reject.
2Token exchange (RFC 8693) — gateway exchanges client JWT for service-specific tokens with narrowed scope; cleaner audit trail.
3mTLS + identity header — the mesh authenticates services via mTLS; per-request user identity propagates as a signed header trusted only inside the mesh boundary. Common pitfalls: services that trust headers without verifying mesh-side mTLS; gateway issuing JWTs with overly broad scope; services that cache validation results without re-checking expiry. For Indian banking microservices: regulator expectations now include per-service JWT validation; “trusted internal network” is increasingly insufficient justification.

JWT signing key management at scale

Production JWT systems must rotate signing keys without disruption. Standard pattern: maintain a JWKS endpoint that lists all currently-valid keys with key IDs (kid); JWTs include the kid header so verifiers know which key to use. Key rotation flow:

1Generate new key, add to JWKS;
2Issuer starts signing with new key;
3Old key remains in JWKS for the maximum token lifetime;
4After lifetime expires, old key removed from JWKS. Operational concerns: (a) JWKS endpoint availability — if it goes down, all verifications fail; cache aggressively (10-30 min) but support kid-not-found refresh. (b) Cross-region key consistency — KMS-backed signing with regional caches. (c) Compromised-key revocation — emergency removal from JWKS; expect short-term verification storm. (d) Key-storage hygiene — HSM / KMS, never in environment variables, never in code, never in git. For Indian fintech: RBI inspection has called out JWT rotation cadence as a control deficiency when keys lived years; quarterly rotation is the new baseline expectation. Large operators move to AWS KMS / Azure Key Vault / HashiCorp Vault for the signing-key lifecycle.

Performance discipline that strengthens security

A practical performance discipline that applies regardless of the specific vulnerability class is to instrument every endpoint with three SLOs: latency at the 50th, 95th, and 99th percentiles. The 50th percentile reflects typical user experience, the 95th catches tail behaviour visible to a meaningful fraction of users, and the 99th captures the long-tail outliers that often hide bugs (timeouts on a stale dependency, locking contention under load, garbage-collection pauses on a hot endpoint). Every release should compare these percentiles against the previous baseline; regressions of more than ten percent on the 95th percentile are typically worth investigating before shipping. Modern observability stacks make this routine — Datadog APM, New Relic, Honeycomb, Grafana Tempo with OpenTelemetry instrumentation, and on the open-source side Jaeger plus Prometheus plus Grafana — but the discipline matters more than the tool. Indian product teams that have adopted this percentile-driven culture report fewer surprise outages and a smaller incident-response load over time, which compounds into a stronger security posture: the same instrumentation that catches a slow endpoint also catches an exploitation attempt that forces unusual code paths.

Observability that catches attacks alongside outages

Observability for security overlaps with observability for reliability but adds a few specific signals worth instrumenting. First, application-layer authentication and authorisation events should produce structured logs with the user identifier, the action attempted, the source IP, the User-Agent fingerprint, and the outcome. Second, every state-changing API call should log its idempotency key, the request body shape, and the resulting object identifier. Third, every external dependency call should log the destination, the response code, and the latency. Together these three categories produce a clean event stream that a SIEM can correlate to detect both functional regressions and active attacks. The cost is moderate — log storage in Elasticsearch or Splunk runs perhaps thirty to sixty rupees per gigabyte per year at typical Indian-tier prices — and the payoff is dramatic: incidents that previously took days to investigate are typically resolved in hours when the relevant evidence has been pre-indexed. Indian regulators increasingly expect this level of operational logging, particularly RBI under the Cyber Security Framework Annex 1 and SEBI under the CSCRF master circular.

DPDP and sectoral obligations a practitioner cannot ignore

Under India’s Digital Personal Data Protection Act 2023, the operational obligations on any web application that handles personal data extend beyond simple consent collection. Section 8 requires that data fiduciaries implement reasonable security safeguards — interpreted in practice as reflecting a documented information security programme, regular vulnerability assessment, and demonstrable controls aligned to a recognised standard such as ISO 27001 or NIST CSF. Section 9 imposes data principal rights including access, correction, and erasure, which means the application must be architected so that these requests can be fulfilled within the prescribed timeframes (currently expected to be on the order of thirty days). Section 8(6) imposes breach notification: when a personal-data breach occurs, the data fiduciary must notify the Data Protection Board and affected data principals within prescribed timeframes (the draft Rules suggest seventy-two hours for material breaches). For the practitioner this translates into specific engineering tasks: data inventory and lineage tracking, retention controls and automated deletion at end-of-purpose, audit-grade logging of every access to personal data, breach-detection telemetry, and a formal incident-response playbook with regulatory-notification templates. The 2024-2025 inspections by RBI and SEBI of regulated entities have already cited DPDP-overlay obligations, so the alignment is not theoretical; it shapes what auditors expect to see in the next inspection cycle.

A small threat-modelling routine before every release

A short threat-modelling exercise specific to this vulnerability class is worth running before each release. The classic STRIDE framework — Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege — provides a useful checklist when applied to the specific data flow at hand. Begin by drawing the data flow for the feature being changed: external actors at the edge, the trust boundaries they cross, the back-end services involved, the data stores read or written, and the third-party integrations called. For each trust boundary crossing, ask which STRIDE categories apply and what controls mitigate them. The output is a small set of testable assertions: this endpoint must not allow tenant A to read tenant B’s records, this mutation must require step-up authentication, this third-party call must validate the response signature, and so on. Convert each assertion into an automated test or a manual verification step in the release checklist. The discipline pays off in two ways: first, it catches design-level bugs before they reach code review; second, it produces an auditable record of security thinking that regulators value. Indian product teams that have institutionalised this practice report fewer security incidents and shorter mean-time-to-remediate when issues are found, which compounds into better customer trust and lower regulatory friction over time.

Further reading

Additional FAQs

Should I encrypt JWTs (JWE)?

Only if the payload genuinely needs confidentiality from the client itself — rare. Most apps just need integrity (signed JWT, JWS) and avoid putting secrets in the payload.

How do I revoke a JWT before it expires?

Maintain a server-side revocation list keyed by jti; check on every request. The cost is small with Redis; the benefit is real. Long-lived JWTs without revocation are a security debt.

Want this for your team?

Custom team training + practitioner advisory

Beyond the free academy — we run private workshops, vCISO advisory, and red-team exercises tailored to your stack. For Indian SMBs scaling past their first hire.

Book team training call Replies in 4 working hrs · India-only · Senior consultants