{"schema":"threatline.record/2","record":{"id":"CVE-2026-102265","aliases":["GHSA-8wjv-2p76-3863","PYSEC-2026-4142"],"summary":"PyJWT: Uncaught RecursionError in jwt.decode() on deeply nested token header","details":"## Package\n\npyjwt (PyPI)\n\n## Affected versions\n\ntested & verified on: 2.13.0. Every version whose `PyJWS._load()` translates only `ValueError` is affected\n\n## Description\n\n`PyJWS._looad()` (`jwt/api_jws.py:337`) splits the compact token, base64url decodes the header segment and hands it to `json.loads()` before `_verify_signature()` runs. The parse is wrapped in `except ValueError as e: raise DecodeError(...)`. That is what makes the documented contract sufficient for untrusted input: `except jwt.PyJWTError` (or `InvalidTokenError`) around `jwt.decode(token, key, algorithms=[...])`.\n\nCPython's JSON decoder raises `RecursionError` (a `RuntimeError` subclass, not a `ValueError`) once nesting crosses the native stack guard. That exception leaves `_load()`, then `jwt.decode()`, and matches no `PyJWTError` handlerr at all. No key and no valid signature are needed: the header is parsed first, so a single unauthenticated request with an unsigned token suffices.\n\nScope: the recursion threshold is ~65,000 nesting levels; below it the same input yields a clean `DecodeError` (a 346,800-byte flat header is rejected normally, so depth, not size, is the trigger). A token this large cannot ride in an `Authorization` header (rejected at HTTP field limits,431 under gunicorn), but body transport (RFC 7662 introspection style) is a standard pattern. Under Flask 3.1.3 / gunicorn 26.2.0 each crafted request returns HTTP 500 instead of 401 and writes a full traceback to the error log; the sync worker survives.\n\n## Proof of concept\n\nAgainst any endpoint that validates a token from its JSON body with the documented pattern:\n\n```python\nimport base64, json, http.client\n\ndef b64url(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\")\n\n# unsigned token: the header segment is 200,000 nested '[' (266,681 bytes total)\ntoken = (b64url(b\"[\" * 200_000) + b\".\" + b64url(b'{\"a\":1}') + b\".\" + b64url(b\"x\")).decode()\n\nconn = http.client.HTTPConnection(\"127.0.0.1\", 8125, timeout=30)\nconn.request(\"POST\", \"/api/protected\", body=json.dumps({\"token\": token}),\n             headers={\"Content-Type\": \"application/json\"})\nprint(conn.getresponse().status)   # 500, expected 401\n```\n\nDeterminism: 20/20 crafted requests returned 500 against the reference container (PyJWT 2.13.0, Flask 3.1.3, gunicorn 26.2.0, Python 3.14.7) in 0.12 s (about 6 ms per request in a tight loop, 14 ms for a cold request), each leaving a `RecursionError: Stack overflow ... while decoding a JSON array` traceback in the error log.\n\n## Impact\n\nAn unauthenticated attacker can turn every request to a JWT validating endpoint into a server error with a full traceback logged per request, a cheap and repeatable denial of service on the authentication path (no crash, no data exposure).\n\n## Fix\n\nCatch `RecursionError` alongside `ValueError` in `_load()` and raise `DecodeError`, or parse the header with a nonrecursive, depthbounded parser.\n\n## Reproduction\n\nA standalone package (docker-compose.yml, Dockerfile, requirements.txt, target app, driver) accompanies this report:\n\n```\ncd <package dir>\ndocker compose up -d --build\npython3 poc_check_then_attack.py\ndocker compose down -v\n```\n[poc_pyjwt_recursion.zip](https://github.com/user-attachments/files/31614987/poc_pyjwt_recursion.zip)\n\nExit 0= reproduced (control checks green, exactly one crafted request returns 500, traceback verified in `docker logs`, service alive afterwards); 1 = not reproduced; 2 = checks failed, no attack sent. The package's `run_log.txt` documents a complete proof run.\n\n## Credits\n\n- @Nivid42\n\n## Maintainer update — 2026-09-09\n\nWe reproduced the reported behavior on PyJWT 2.13.0: a deeply nested JSON array in the untrusted compact-JWS header reaches `json.loads()` before signature verification and raises `RecursionError`, which is not a `PyJWTError`. The same input is accepted by the public `jwt.decode()` path and can escape an application's normal `except PyJWTError` handling.\n\nThe fix is committed as `06573692ebcdec8831c3927513b3e87c31fbbb62`. `PyJWS._load()` now translates both `ValueError` and `RecursionError` from header JSON parsing into `DecodeError`. A regression test covers the deeply nested-header path before signature verification. The full suite passes with 373 tests and 4 intentional cryptography-environment skips.\n\nNo release containing the fix has been published yet, so the patched version remains unset pending release planning. The advisory remains medium severity and unpublished.\n\n## Maintainer update — 2026-09-11\n\nThe verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.","categories":["Libraries & packages"],"product":"pyJWT","vendor":"jpadilla","ecosystem":"pip","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-8wjv-2p76-3863","source_modified_at":"2026-09-29T23:43:07Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"pyJWT"},"range":">= 2.13.0, < 2.14.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-102265","source_modified_at":"2026-10-01T15:01:31.377Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"pyjwt","vendor":"jpadilla","versions":[{"version":">= 2.13.0, < 2.14.0","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-8wjv-2p76-3863","source_modified_at":"2026-10-01T17:55:45.157085612Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"pyjwt","ecosystem":"PyPI","purl":"pkg:pypi/pyjwt"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.13.0"},{"fixed":"2.14.0"}]}],"versions":["2.13.0"]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-8wjv-2p76-3863","source_modified_at":"2026-09-29T23:43:07Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"pyJWT"},"version":"2.14.0"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-8wjv-2p76-3863","source_modified_at":"2026-10-01T17:55:45.157085612Z","fetched_at":"2026-10-23T18:45:31.882637Z","package":{"name":"pyjwt","ecosystem":"PyPI","purl":"pkg:pypi/pyjwt"},"version":"2.14.0"}],"cvss":5.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":0.00301,"percentile":0.21679,"score_date":"2026-10-23"},"published_at":"2026-09-28T20:48:04.664Z","modified_at":"2026-10-01T17:55:45.157085612Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-8wjv-2p76-3863","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-09-29T23:43:07Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-102265","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-01T15:01:31.377Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-8wjv-2p76-3863","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-01T17:55:45.157085612Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-102265","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/jpadilla/pyjwt/security/advisories/GHSA-8wjv-2p76-3863","tags":["exploit"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-102265","source":"GitHub Advisory Database"},{"source":"CVE List","url":"https://github.com/jpadilla/pyjwt/commit/06573692ebcdec8831c3927513b3e87c31fbbb62","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/jpadilla/pyjwt/releases/tag/2.14.0","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-8wjv-2p76-3863","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"yes","technical_impact":"partial","assessed_at":"2026-10-01T15:00:55.850520Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-01T17:55:45.157085612Z","content_hash":"da20d525159783b4b34d6ac2e122ebbdadc3041665b2386179c25527999107d1"}}
