Nesting depth that triggers the error: ~65,000 levels (Source: GitHub Advisory Database, GHSA-8wjv-2p76-3863 (September 29, 2026))
The promise developers relied on
Most security bugs break a lock. This one breaks a promise.
PyJWT is a Python library that checks login tokens. Its documented pattern tells developers to wrap the decode call in a handler for the library's own error type. Bad token in, clean rejection out. The app answers "401 unauthorized" and moves on.
A new advisory shows one input that slips past that promise. The lesson here is that a security contract is only as strong as the errors nobody planned for.
What the advisory reports
The GitHub Advisory Database published advisory GHSA-8wjv-2p76-3863 on September 29, 2026. It links to CVE-2026-102265 and rates the issue medium severity. The researchers tested PyJWT 2.13.0. They state that every version whose header parser translates only one kind of error is affected.
The fix ships in PyJWT 2.14.0, released on September 11, 2026. It is the first release containing the fix.
How it works
A login token of this type has a header, a payload and a signature. The header is JSON text. PyJWT decodes that header and parses it before it checks the signature.
That order matters. The attacker needs no key and no valid signature. An unsigned token is enough.
The parsing step catches one error type, ValueError, and turns it into the library's own DecodeError. Python's JSON parser has a different failure. If arrays are nested deeply enough, it raises RecursionError. That is not a ValueError, so it escapes the library. It also escapes any application handler written for PyJWT's own errors.
Depth is the trigger, not size. The researchers put the threshold near 65,000 nesting levels. A flat header of 346,800 bytes was rejected normally.
What it does, and what it does not
The token is too large for a standard Authorization header. Under gunicorn, such a request is rejected with a 431 error. The advisory says sending tokens in a request body is a standard pattern, and it cites RFC 7662 introspection-style endpoints. Those are the exposed case.
In the researchers' setup, Flask 3.1.3 with gunicorn 26.2.0 returned HTTP 500 instead of 401. Each request also wrote a full traceback to the error log. The worker process survived.
The advisory calls this a cheap, repeatable denial of service on the authentication path. It reports no crash and no data exposure. These results come from a reference container, not from production systems.
Why a quiet failure still matters
A 401 means the system worked. A 500 means the system is reporting a fault. Monitoring tools and on-call engineers treat those very differently.
That is the point this story raises. An attacker who can only cause errors still lands on the auth path. The people who feel it first are those watching error dashboards and reading logs. Each crafted request adds a full traceback to those logs.
The advisory does not say this has been exploited. It does show that a handler written exactly as documented did not cover this input.
Questions for your team
Which services use PyJWT, and at what version? PyJWT 2.14.0 is the first release with the fix. The advisory tested only 2.13.0, so do not assume older versions are safe.
Does any endpoint read a token from a JSON body rather than a header? Those are the ones the advisory describes as reachable.
Do your alerts separate 500 errors on login and token-check paths from errors elsewhere? A burst there deserves a different first question than a bug in a feature.
The fix itself is small. The library now translates both ValueError and RecursionError into DecodeError, and a regression test covers the case. The larger habit is to ask, for every safety net, what falls outside it.
Produced by the WebPulse Newsroom with AI assistance from the original reporting credited below, and checked against that source by our editorial review. How we use AI.
Original reporting: GitHub Advisory Database.





