Cybersecurity-Projects/PROJECTS/beginner/jwt-auditor/learn/01-CONCEPTS.md

9.1 KiB

Core Security Concepts

This document explains the security ideas behind each check the tool runs. The goal is that by the end you could re-derive every finding yourself, without the tool.

What a JWT Actually Is

What It Is

A JSON Web Token is three pieces joined by dots:

eyJhbGciOiJIUzI1NiJ9 . eyJzdWIiOiJhZG1pbiJ9 . PGnRccPTXeax...
      header                  payload              signature

The header and payload are JSON objects, each base64url encoded. The signature is computed over the string header.payload. The whole thing is a JWS, a signed token. Decode the first two parts and you can read everything in them.

Why It Matters

The single most important fact about a JWT is that the payload is encoded, not encrypted. base64url is reversible by anyone. If you put a password or a Social Security number in a claim, every party that touches the token can read it: the browser, proxies, logs, error trackers.

How It Works

header  = {"alg": "HS256", "typ": "JWT"}
payload = {"sub": "admin", "exp": 1767225600}

signing_input = base64url(header) + "." + base64url(payload)
signature     = HMAC_SHA256(secret, signing_input)

token = signing_input + "." + base64url(signature)

Verification recomputes the signature and compares. That is the whole model, and every attack below is an attack on one step of it.

Common Attacks

  1. Read the claims - decode the payload and harvest anything sensitive.
  2. Tamper and re-sign - if the secret is weak, change the claims and sign again with the guessed secret.
  3. Tamper without re-signing - trick the verifier into skipping or weakening the signature check (the none and confusion attacks below).

Defense Strategies

Keep the secret strong and private, pin the algorithm, set a short expiry, and never put anything in the payload you would not print in a log. The checks in checks.py map one to one onto these defenses.

The alg none Downgrade

What It Is

The JWT header names the algorithm used to sign the token. One legal value in early implementations was none, meaning "this token is unsigned". A verifier that reads the algorithm from the token and honors none will accept a token with an empty signature.

Why It Matters

The attacker takes a valid token, rewrites the header to {"alg":"none"}, edits the payload to say "role":"admin", deletes the signature, and sends it. A vulnerable server treats it as authentic.

This is not hypothetical. In 2015, a wide range of JWT libraries were found to accept alg: none by default, tracked as CVE-2015-9235 for the popular jsonwebtoken Node library and echoed across many others. The disclosure by Auth0 that year is the reason "always pin the algorithm" became standard advice.

How It Works

Original (HS256, signed):
  {"alg":"HS256"}.{"sub":"alice","role":"user"}.<valid signature>

Forged (none, unsigned):
  {"alg":"none"}.{"sub":"alice","role":"admin"}.
                                                 ^ empty signature segment

The tool flags this in check_alg_none (src/jwt_auditor/checks.py:68). It compares token.algorithm.lower() to "none" and, when it matches, returns a critical finding.

Defense Strategies

Never let the token choose the algorithm. Decide server side which algorithms are acceptable and reject everything else, including none:

# the safe pattern, expressed in pseudocode
ALLOWED = {"HS256"}
if token.header["alg"] not in ALLOWED:
    reject()

Weak HMAC Secrets

What It Is

HS256, HS384, and HS512 sign with HMAC, which uses a shared secret. The security of the token rests entirely on that secret being unguessable. When a developer picks the secret by hand, they pick something like secret, changeme, or the placeholder your-256-bit-secret from the jwt.io debugger.

Why It Matters

Because the attacker holds the token, they can guess the secret offline. There is no server to rate limit them. They try a candidate, recompute the HMAC, and compare it to the signature already in the token. A match means they found the key and can now forge any token they want.

How It Works

for candidate in wordlist:
    if HMAC(candidate, signing_input) == token.signature:
        print("secret is", candidate)
        break

The tool does exactly this in crack_hmac_secret (src/jwt_auditor/signatures.py:76). The built in wordlist in wordlist.py holds the secrets that actually appear in the wild.

Common Pitfalls

Mistake: a short or human chosen secret

# Bad
SECRET = "myappsecret"

# Good
SECRET = secrets.token_bytes(32)  # 32 random bytes from a CSPRNG

Mistake: timing the comparison with ==

# Bad, leaks how many leading bytes matched via timing
if computed == token.signature:
    ...

# Good, constant time
if hmac.compare_digest(computed, token.signature):
    ...

That second mistake is why verify_hmac uses hmac.compare_digest (src/jwt_auditor/signatures.py:73).

Algorithm Confusion (RS256 to HS256)

What It Is

RS256 signs with a private key and verifies with a public key. The public key is meant to be public. Algorithm confusion happens when a server verifies with "whatever algorithm the token says", and an attacker changes the algorithm from RS256 to HS256.

Why It Matters

Now the server runs HMAC verification. The HMAC secret it uses is the only key it has: the RSA public key. That key is not secret. The attacker downloads it, signs a forged HS256 token with it, and the server accepts the forgery.

This class of bug has appeared repeatedly, including in widely used libraries, and is catalogued as CWE-347: Improper Verification of Cryptographic Signature. It is subtle because RS256 by itself is fine. The bug is in the verifier accepting the header's choice.

How It Works

Server has: rsa_public_key  (published, not secret)

Attacker builds:
  header  = {"alg":"HS256"}
  payload = {"sub":"admin"}
  signature = HMAC(rsa_public_key, header.payload)

Server, trusting the header, verifies with HMAC(rsa_public_key, ...) -> match

The tool proves this when you pass --public-key. key_confusion_secret (src/jwt_auditor/signatures.py:95) tries the public key bytes as an HMAC secret across the HS algorithms and reports a match.

Defense Strategies

Pin the algorithm on the verifier so an RS256 endpoint only ever runs RSA verification. Do not derive the algorithm from the token.

How These Concepts Relate

alg is attacker controlled
        ↓
   enables  →  alg none (no signature at all)
        ↓
   enables  →  RS to HS confusion (public key becomes the HMAC secret)

secret is weak
        ↓
   enables  →  offline secret cracking, then arbitrary forgery

Every one of these traces back to the same root cause: trusting data inside the token to decide how to verify the token.

Industry Standards and Frameworks

OWASP

  • OWASP API Security Top 10, API2:2023 Broken Authentication - weak or misconfigured token verification is the core of this category.
  • OWASP JWT Cheat Sheet - the source for "always use an allowlist of algorithms" and "do not accept none".

CWE

  • CWE-347: Improper Verification of Cryptographic Signature - the none and confusion attacks both live here.
  • CWE-321: Use of Hard coded Cryptographic Key - the weak secret case.
  • CWE-522: Insufficiently Protected Credentials - secrets or PII in the payload.

Real World Examples

Case Study 1: alg none in JWT libraries (2015)

Security researchers at Auth0 published a widely cited writeup showing that many JWT libraries accepted alg: none and, separately, were vulnerable to the RS to HS confusion. What made it dangerous was the default behavior: a developer calling verify(token) with no extra arguments got the insecure path. The fix across the ecosystem was to require the caller to state the expected algorithm.

Case Study 2: secrets in the payload

A recurring finding in API assessments is a JWT payload that includes an email, a phone number, or an internal role map. The developers assumed the token was opaque because it looks like random text. It is not. Anyone who captured the token from a log or a proxy read the data directly. This is why check_sensitive_data (src/jwt_auditor/checks.py:345) exists.

Testing Your Understanding

Before moving on, make sure you can answer:

  1. Why can an attacker crack an HMAC secret offline, when a login form would rate limit them?
  2. In the RS to HS confusion attack, what is the "secret" the attacker signs with, and why do they have it?
  3. A colleague says the JWT is safe to store a password in because "it is signed". What is wrong with that reasoning?

If any of these are fuzzy, re-read the matching section. The implementation will make more sense once these click.

Further Reading

Essential:

  • OWASP JSON Web Token Cheat Sheet - the practical do and do not list.
  • RFC 7519 (JWT) and RFC 7515 (JWS) - the actual specifications. Short and readable.

Deep dives:

  • The Auth0 2015 writeup on critical JWT vulnerabilities - the origin of the standard advice.
  • PortSwigger Web Security Academy, JWT attacks - hands on labs for none, weak secrets, and confusion.