Skip to content
Heist/ feed
MediumFix available

PyJWT: Uncaught RecursionError in jwt.decode() on deeply nested token header

Type: advisorySource: OSV2 source records

## Package pyjwt (PyPI) ## Affected versions tested & verified on: 2.13.0. Every version whose `PyJWS._load()` translates only `ValueError` is affected ## Description `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=[...])`. CPython'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. Scope: 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 sur

Announced Group revision 1
Original source ↗

PYSEC-2026-4142

Revision 1

PyJWT: Uncaught RecursionError in jwt.decode() on deeply nested token header

## Package pyjwt (PyPI) ## Affected versions tested & verified on: 2.13.0. Every version whose `PyJWS._load()` translates only `ValueError` is affected ## Description `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=[...])`. CPython'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. Scope: 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 sur

Imported from structured advisory data. Source references identify upstream records or fields; verify affected versions and your deployed configuration.

Security context

Source reference: osv:PYSEC-2026-4142

pyjwt

Source reference: /affected/0/package/name

Affected versions
2.13.0

Source reference: /affected/0/versions

Fixed versions
2.14.0

Source reference: /affected/0/ranges

Prerequisites
Not stated in this record

Structured affected-version data

Original package identities and version rules from this source.

[
  {
    "ranges": [
      {
        "type": "ECOSYSTEM",
        "events": [
          {
            "introduced": "2.13.0"
          },
          {
            "fixed": "2.14.0"
          }
        ]
      }
    ],
    "package": {
      "name": "pyjwt",
      "purl": "pkg:pypi/pyjwt",
      "ecosystem": "PyPI"
    },
    "versions": [
      "2.13.0"
    ],
    "database_specific": {
      "source": "https://github.com/pypa/advisory-database/blob/main/vulns/pyjwt/PYSEC-2026-4142.yaml"
    }
  }
]
Related CVEs
CVE-2026-102265

Source reference: /aliases

Publisher severity

No explicit severity was provided.

Linked CVE context

These ratings describe the referenced vulnerabilities and products. They are not a severity rating for every application using this dependency.

Event
#62517 · Revision 1
Added to Heist Feed
Publisher date
Read the original announcement

GHSA-8wjv-2p76-3863

Primary source · Revision 1

Imported from structured advisory data. Source references identify upstream records or fields; verify affected versions and your deployed configuration.

Security context

Source reference: osv:GHSA-8wjv-2p76-3863

pyjwt

Source reference: /affected/0/package/name

Affected versions
2.13.0

Source reference: /affected/0/versions

Fixed versions
2.14.0

Source reference: /affected/0/ranges

Prerequisites
Not stated in this record

Structured affected-version data

Original package identities and version rules from this source.

[
  {
    "ranges": [
      {
        "type": "ECOSYSTEM",
        "events": [
          {
            "introduced": "2.13.0"
          },
          {
            "fixed": "2.14.0"
          }
        ]
      }
    ],
    "package": {
      "name": "pyjwt",
      "purl": "pkg:pypi/pyjwt",
      "ecosystem": "PyPI"
    },
    "versions": [
      "2.13.0"
    ],
    "database_specific": {
      "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-8wjv-2p76-3863/GHSA-8wjv-2p76-3863.json"
    }
  }
]
Related CVEs
CVE-2026-102265

Source reference: /aliases

Publisher severity

No explicit severity was provided.

Linked CVE context

These ratings describe the referenced vulnerabilities and products. They are not a severity rating for every application using this dependency.

Event
#62535 · Revision 1
Added to Heist Feed
Publisher date
Read the original announcement