Zion Boggan
repos/jwt-differential-fuzzer

jwt-differential-fuzzer

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.

2 commits First commit Apr 25, 2026 Last commit Jun 22, 2026 (43 minutes ago)
JSON 38.1%Python 27.2%Markdown 17.5%JavaScript 6.6%Go 5.2%Shell 4.1%YAML 1.1%
Files 28 entries
README.md

jwt-differential-fuzzer

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.

Libraries under test (v1)

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": "..."}.

Architecture

                +------------------------------+
                |    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.

Test corpus

corpus/seed.json ships with baseline positive controls (RS256, HS256, ES256 happy paths) plus a growing set of bug-class cases:

  • alg confusion - HS256 token signed against the RSA public key
  • kid injection - SQL-i/path traversal patterns in kid
  • jku spoof - external jku URL pointing at attacker-controlled JWKS
  • crit handling - RFC 7515 §4.1.11 critical-header enforcement
  • JWE/JWS confusion - JWE token sent into a JWS verifier
  • ECDSA edge cases - r/s of zero, n, n-1
  • header JSON quirks - duplicate keys, NUL bytes, BOM, unicode

scripts/build_corpus.py can extend the corpus from generators.

Running

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

Findings

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:

  1. Confirm the disagreement is reproducible against the latest released version of each affected library.
  2. Confirm a spec citation that picks a winner (i.e., the RFC says X, library Y does not implement X).
  3. File a GitHub Security Advisory at the affected repository.
  4. Request a CVE via the repository's CNA or MITRE.
  5. Wait for the upstream patch or the embargo window expiration before broadening publication.

The advisories currently in findings/ are public-disclosure-stage; their sister advisories at other libraries are already CVE'd.

License

MIT. See LICENSE.