| 1 | # Schism |
| 2 | |
| 3 | Differential JWT verification harness. Feeds the same `(token, key, alg-allowlist)` triple into N JWT libraries and surfaces any verifier disagreement. Disagreements at the verification boundary are auth-bypass primitives. |
| 4 | |
| 5 | ## Premise |
| 6 | |
| 7 | Auth bypass primitives in JWT libraries propagate to every program that uses the library. One finding maps to many bounty programs. The bug class is well known (alg confusion, kid injection, jku spoof, JWS critical header, JWE/JWS confusion, ECDSA r/s edge cases, padding, JSON-parser quirks at the header layer) but the libraries diverge in subtle ways that aren't covered by any single test suite. Wycheproof has static vectors; nobody runs live differential fuzzing across the modern JWT ecosystem. |
| 8 | |
| 9 | ## Targets (v1) |
| 10 | |
| 11 | | ID | Lib | Lang | Why | |
| 12 | |----|-----|------|-----| |
| 13 | | `nodejwt` | `jsonwebtoken` (Auth0) | Node | Most-used npm JWT lib. ~10M weekly DLs. | |
| 14 | | `pyjwt` | `PyJWT` | Py | Most-used Python lib. Historical alg-confusion CVEs. | |
| 15 | | `pyjose` | `python-jose` | Py | Different impl, looser parsing. CVE-2024-33663 territory. | |
| 16 | | `panva` | `jose` (Panva) | Node | Most spec-compliant JS lib. Useful as oracle. | |
| 17 | | `gojwt` | `golang-jwt/jwt` v5 | Go | Most-used Go lib. Used in K8s, Helm, etc. | |
| 18 | |
| 19 | v2: `nimbus-jose-jwt` (Java), `jsonwebtoken` (Rust), `lcobucci/jwt` (PHP). |
| 20 | |
| 21 | ## Architecture |
| 22 | |
| 23 | ``` |
| 24 | ┌────────────────────────────────┐ |
| 25 | │ orchestrator/differ.py │ |
| 26 | │ (corpus → fanout → compare) │ |
| 27 | └─────────────┬──────────────────┘ |
| 28 | │ HTTP /verify |
| 29 | ┌───────────┬───────┼────────┬───────────┐ |
| 30 | │ │ │ │ │ |
| 31 | ┌────▼───┐ ┌────▼───┐ ┌─▼─────┐ ┌▼──────┐ ┌─▼─────┐ |
| 32 | │nodejwt │ │ pyjwt │ │pyjose │ │ panva │ │ gojwt │ |
| 33 | │:7001 │ │ :7002 │ │ :7003 │ │ :7004 │ │ :7005 │ |
| 34 | └────────┘ └────────┘ └───────┘ └───────┘ └───────┘ |
| 35 | each is a thin HTTP wrapper over the lib's verify() call, |
| 36 | running in its own container, isolated, reproducible |
| 37 | ``` |
| 38 | |
| 39 | Each target container exposes a single endpoint: |
| 40 | |
| 41 | ``` |
| 42 | POST /verify |
| 43 | { |
| 44 | "token": "<compact JWT>", |
| 45 | "key": "<PEM | symmetric | JWK>", |
| 46 | "algs": ["RS256", "HS256"] // allowlist, mimics real-world API |
| 47 | } |
| 48 | → { "valid": bool, "claims": {...} | null, "error": "<class>" | null } |
| 49 | ``` |
| 50 | |
| 51 | The orchestrator submits each corpus case to all targets in parallel, computes the verdict tuple `(valid_per_target)`, and flags any tuple that isn't unanimous. |
| 52 | |
| 53 | ## Corpus |
| 54 | |
| 55 | `corpus/seed.json` carries the v1 test cases. Each case has: |
| 56 | - `id` - short slug |
| 57 | - `class` - bug-class tag (`alg-confusion`, `none-alg`, `kid-injection`, `jku-spoof`, `crit-header`, `jws-jwe-confusion`, `ecdsa-r-s`, `header-json-quirk`, `dup-keys`, etc.) |
| 58 | - `token`, `key`, `algs` - the inputs |
| 59 | - `expected_unanimous` - what *should* happen if every lib agreed (`reject`) |
| 60 | - `notes` - pointer to RFC clause or prior CVE |
| 61 | |
| 62 | Triage rule: any case where `valid` differs across targets is an auto-flag. Errors are bucketed by class so different error wording doesn't pollute results. |
| 63 | |
| 64 | ## Workflow |
| 65 | |
| 66 | 1. `docker compose up -d` - bring up all target wrappers |
| 67 | 2. `python3 orchestrator/differ.py --corpus corpus/seed.json` - run the diff |
| 68 | 3. `findings/<timestamp>.jsonl` - per-case verdict tuples; disagreements highlighted |
| 69 | 4. Manual triage of disagreements → reproducer → CVE candidate |
| 70 | 5. Bounty fanout: for each finding, enumerate downstream consumers via npm/PyPI/Go module dependency graph + Sourcegraph code search, prioritize by program payout |
| 71 | |
| 72 | ## Disclosure order |
| 73 | |
| 74 | 1. Upstream lib maintainer first (the bug source) |
| 75 | 2. Then bounty programs whose scope includes the affected lib |
| 76 | 3. Schism itself stays private until first finding wave is submitted |
| 77 | |
| 78 | ## Out of scope (explicitly) |
| 79 | |
| 80 | - JWE encryption oracles (cover in v2) |
| 81 | - JWKS endpoint SSRF (different tool - covered by Flywheel's existing `test_ssrf_in_headers`) |
| 82 | - HTTP-level token transport (Authorization header parsing, cookie handling) - those are server-level, not library-level |