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

281 lines
9.1 KiB
Markdown

# 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`:
```python
# 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**
```python
# Bad
SECRET = "myappsecret"
# Good
SECRET = secrets.token_bytes(32) # 32 random bytes from a CSPRNG
```
**Mistake: timing the comparison with ==**
```python
# 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.