Differential JWT verification harness. Feeds the same (token, key, alg-allowlist) triple into N JWT libraries simultaneously and surfaces any disagreement in the valid field. Disagreements at the verification boundary are auth-bypass primitives.
Differential JWT verification harness. Feeds the same
(token, key, alg-allowlist) triple into N JWT libraries simultaneously
and surfaces any disagreement in the valid field. Disagreements at the
verification boundary are auth-bypass primitives.
A JWT library that accepts a token another major library rejects, given identical inputs, is either misimplementing the spec or interpreting it differently than the rest of the ecosystem. Either way, applications that share tokens across services written in different languages can be split between accepting and rejecting verifiers, and that asymmetry is exploitable.
Wycheproof has static test vectors. This harness runs the libraries live, in matched containers, against a corpus that grows over time.
See PLAN.md for the full architecture writeup. See findings/ for advisories produced by this harness.
| ID | Library | Language | Why |
|---|---|---|---|
nodejwt |
jsonwebtoken (Auth0) |
Node | ~10M weekly npm downloads |
pyjwt |
PyJWT |
Python | Historical alg-confusion CVEs |
pyjose |
python-jose |
Python | Looser parser, CVE-2024-33663 territory |
panva |
jose (panva) |
Node | Most spec-compliant JS lib; oracle |
gojwt |
golang-jwt/jwt v5 |
Go | Used in K8s, Helm, etc. |
Each runs as an HTTP server inside a minimal Docker container exposing a
single POST /verify endpoint that returns
{"lib": "...", "valid": bool, "error": "..."}.
+------------------------------+
| orchestrator/differ.py |
| (corpus -> fanout -> compare)|
+---------------+--------------+
|
| HTTP /verify (parallel fanout)
v
+----------+ +----------+ +----------+ +----------+ +----------+
| nodejwt | | pyjwt | | pyjose | | panva | | gojwt |
| :7001 | | :7002 | | :7003 | | :7004 | | :7005 |
+----------+ +----------+ +----------+ +----------+ +----------+
|
v
+-----------------------------------+
| BYPASS rows |
| (libs disagree on valid) |
+-----------------------------------+
The orchestrator submits every corpus case to every running target in
parallel, then collapses the responses by valid. If the set of "accept"
verifiers and the set of "reject" verifiers are both non-empty, the row
is a BYPASS-class disagreement. Errors are bucketed (not literal-string
compared) so different wording across libs doesn't cause false positives.
corpus/seed.json ships with baseline positive controls (RS256, HS256,
ES256 happy paths) plus a growing set of bug-class cases:
scripts/build_corpus.py can extend the corpus from generators.
git clone https://github.com/zionboggan/jwt-differential-fuzzer
cd jwt-differential-fuzzer
scripts/up.sh
python3 orchestrator/differ.py --corpus corpus/seed.json
scripts/up.sh brings the 5 targets up via Docker Compose; the orchestrator
prints one row per case with the per-library verdict and flags any BYPASS-
class disagreements. scripts/down.sh tears the targets down.
For environments without Docker, scripts/up_native.sh runs each target
natively against a managed Python venv / npm install / go build under
.native/.
Single case:
python3 orchestrator/differ.py --corpus corpus/seed.json --only crit-crit-eca
Run against a subset of targets:
python3 orchestrator/differ.py --corpus corpus/seed.json --targets nodejwt,panva
Each disagreement that reproduces with a working spec citation gets a
write-up in findings/ and a coordinated disclosure attempt
upstream. The findings/ directory is the audit trail of confirmed
issues, with PoC code, sister-advisory comparisons, and a disclosure
timeline section.
Filing follows responsible disclosure norms:
The advisories currently in findings/ are public-disclosure-stage; their
sister advisories at other libraries are already CVE'd.
MIT. See LICENSE.