CVE-2026-102265
PyJWT: Uncaught RecursionError in jwt.decode() on deeply nested token header
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- Low
Exploit likelihood
What each source reports
CVE List Score used
5.3
PyJWT: Uncaught RecursionError in jwt.decode() on deeply nested token header
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.
- Record
CVE-2026-102265- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-674
- Updated
- 2026-11-02
EUVDEUVD-2026-88549
5.3
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.
- Record
EUVD-2026-88549- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-8wjv-2p76-3863
5.3
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 survives.
Proof of concept
Against any endpoint that validates a token from its JSON body with the documented pattern:
import base64, json, http.client
def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=")
# unsigned token: the header segment is 200,000 nested '[' (266,681 bytes total)
token = (b64url(b"[" * 200_000) + b"." + b64url(b'{"a":1}') + b"." + b64url(b"x")).decode()
conn = http.client.HTTPConnection("127.0.0.1", 8125, timeout=30)
conn.request("POST", "/api/protected", body=json.dumps({"token": token}),
headers={"Content-Type": "application/json"})
print(conn.getresponse().status) # 500, expected 401
Determinism: 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.
Impact
An 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).
Fix
Catch RecursionError alongside ValueError in _load() and raise DecodeError, or parse the header with a nonrecursive, depthbounded parser.
Reproduction
A standalone package (docker-compose.yml, Dockerfile, requirements.txt, target app, driver) accompanies this report:
cd <package dir>
docker compose up -d --build
python3 poc_check_then_attack.py
docker compose down -v
Exit 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.
Credits
- @Nivid42
Maintainer update — 2026-09-09
We 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.
The fix is committed as…
- Record
GHSA-8wjv-2p76-3863- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-674
- Updated
- 2026-11-02
NVD
5.3
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.
- Record
CVE-2026-102265- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-674
- Updated
- 2026-11-02
OSVBELL-CVE-2026-102265
5.3
- Record
BELL-CVE-2026-102265- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-07
OSV
5.3
PyJWT: Uncaught RecursionError in jwt.decode() on deeply nested token header
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.
- Record
CVE-2026-102265- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-674
- Updated
- 2026-11-02
Show 12 moreShow less
OSVDEBIAN-CVE-2026-102265
5.3
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.
- Record
DEBIAN-CVE-2026-102265- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
OSVGHSA-8wjv-2p76-3863
5.3
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 survives.
Proof of concept
Against any endpoint that validates a token from its JSON body with the documented pattern:
import base64, json, http.client
def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=")
# unsigned token: the header segment is 200,000 nested '[' (266,681 bytes total)
token = (b64url(b"[" * 200_000) + b"." + b64url(b'{"a":1}') + b"." + b64url(b"x")).decode()
conn = http.client.HTTPConnection("127.0.0.1", 8125, timeout=30)
conn.request("POST", "/api/protected", body=json.dumps({"token": token}),
headers={"Content-Type": "application/json"})
print(conn.getresponse().status) # 500, expected 401
Determinism: 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.
Impact
An 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).
Fix
Catch RecursionError alongside ValueError in _load() and raise DecodeError, or parse the header with a nonrecursive, depthbounded parser.
Reproduction
A standalone package (docker-compose.yml, Dockerfile, requirements.txt, target app, driver) accompanies this report:
cd <package dir>
docker compose up -d --build
python3 poc_check_then_attack.py
docker compose down -v
Exit 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.
Credits
- @Nivid42
Maintainer update — 2026-09-09
We 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.
The fix is committed as…
- Record
GHSA-8wjv-2p76-3863- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Weakness
- CWE-674
- Updated
- 2026-11-02
OSVOESA-2026-4453
6.5
python-jwt security update
setup.py test %endif
Security Fix(es):
PyJWT is a Python implementation of JSON Web Token standards. Prior to 2.14.0, PyJWT get_signing_key_from_jwt is affected because unknown kid misses force refreshes without a negative cache or minimum refresh interval. This occurs when unauthenticated tokens repeatedly use the same unknown kid or varying kid values absent from the cached JWKS. As a result, each cache miss causes PyJWKClient to refresh the JWKS. Consequently, attacker traffic can amplify outbound requests to the configured JWKS endpoint. This issue is fixed in version 2.14.0.(CVE-2026-101917)
PyJWT is a Python implementation of JSON Web Token standards. From 2.0.0a1 until 2.15.0, PyJWT PyJWKClient.get_signing_key_from_jwt is affected because payload parser catches ValueError but not RecursionError. This occurs when an attacker-controlled recursively nested payload reaches json.loads. As a result, documented PyJWT exception handling does not contain the failure. Consequently, an unauthenticated request can raise an exception that may produce an HTTP 500 response. The advisory-defined affected implementation also includes jwt/api_jwt.py, verify_signature=False. This issue is fixed in version 2.15.0.(CVE-2026-101918)
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.(CVE-2026-102265)
PyJWT is a Python implementation of JSON Web Token standards. Prior to 2.14.0, PyJWT signature segment is affected because signature segment decoding accepts characters outside the canonical Base64URL representation. This occurs when non-Base64URL characters are appended to a valid compact JWS signature segment. As a result, base64url_decode produces the same signature bytes for different serialized segments. Consequently, raw-token revocation checks can fail to recognize an equivalent modified token. This issue is fixed in version 2.14.0.(CVE-2026-102269)
PyJWT is a Python implementation of JSON Web Token standards. From 2.1.0 until 2.15.0, PyJWT OKPAlgorithm.from_jwk in jwt/algorithms.py is affected because private-JWK import path does not compare the public key derived from d with x. This occurs when an OKP private JWK supplies non-corresponding x and d components. As a result, identity derived from x can differ from operations performed with d. Consequently, if an integration also accepts private key parameters from a proof header without rejecting them, an attacker may use a stolen sender-constrained token without the legitimate private key. This issue is fixed in version 2.15.0.(CVE-2026-102275)
- Record
OESA-2026-4453- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:N - Updated
- 2026-11-02
OSVOESA-2026-4454
6.5
python-jwt security update
setup.py test %endif
Security Fix(es):
PyJWT is a Python implementation of JSON Web Token standards. From 2.0.0a1 until 2.15.0, PyJWT PyJWKClient.get_signing_key_from_jwt is affected because payload parser catches ValueError but not RecursionError. This occurs when an attacker-controlled recursively nested payload reaches json.loads. As a result, documented PyJWT exception handling does not contain the failure. Consequently, an unauthenticated request can raise an exception that may produce an HTTP 500 response. The advisory-defined affected implementation also includes jwt/api_jwt.py, verify_signature=False. This issue is fixed in version 2.15.0.(CVE-2026-101918)
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.(CVE-2026-102265)
PyJWT is a Python implementation of JSON Web Token standards. Prior to 2.14.0, PyJWT signature segment is affected because signature segment decoding accepts characters outside the canonical Base64URL representation. This occurs when non-Base64URL characters are appended to a valid compact JWS signature segment. As a result, base64url_decode produces the same signature bytes for different serialized segments. Consequently, raw-token revocation checks can fail to recognize an equivalent modified token. This issue is fixed in version 2.14.0.(CVE-2026-102269)
PyJWT is a Python implementation of JSON Web Token standards. From 2.1.0 until 2.15.0, PyJWT OKPAlgorithm.from_jwk in jwt/algorithms.py is affected because private-JWK import path does not compare the public key derived from d with x. This occurs when an OKP private JWK supplies non-corresponding x and d components. As a result, identity derived from x can differ from operations performed with d. Consequently, if an integration also accepts private key parameters from a proof header without rejecting them, an attacker may use a stolen sender-constrained token without the legitimate private key. This issue is fixed in version 2.15.0.(CVE-2026-102275)
- Record
OESA-2026-4454- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:N - Updated
- 2026-11-02
OSVOESA-2026-4455
6.5
python-jwt security update
setup.py test %endif
Security Fix(es):
PyJWT is a Python implementation of JSON Web Token standards. Prior to 2.14.0, PyJWT get_signing_key_from_jwt is affected because unknown kid misses force refreshes without a negative cache or minimum refresh interval. This occurs when unauthenticated tokens repeatedly use the same unknown kid or varying kid values absent from the cached JWKS. As a result, each cache miss causes PyJWKClient to refresh the JWKS. Consequently, attacker traffic can amplify outbound requests to the configured JWKS endpoint. This issue is fixed in version 2.14.0.(CVE-2026-101917)
PyJWT is a Python implementation of JSON Web Token standards. From 2.0.0a1 until 2.15.0, PyJWT PyJWKClient.get_signing_key_from_jwt is affected because payload parser catches ValueError but not RecursionError. This occurs when an attacker-controlled recursively nested payload reaches json.loads. As a result, documented PyJWT exception handling does not contain the failure. Consequently, an unauthenticated request can raise an exception that may produce an HTTP 500 response. The advisory-defined affected implementation also includes jwt/api_jwt.py, verify_signature=False. This issue is fixed in version 2.15.0.(CVE-2026-101918)
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.(CVE-2026-102265)
PyJWT is a Python implementation of JSON Web Token standards. Prior to 2.14.0, PyJWT signature segment is affected because signature segment decoding accepts characters outside the canonical Base64URL representation. This occurs when non-Base64URL characters are appended to a valid compact JWS signature segment. As a result, base64url_decode produces the same signature bytes for different serialized segments. Consequently, raw-token revocation checks can fail to recognize an equivalent modified token. This issue is fixed in version 2.14.0.(CVE-2026-102269)
PyJWT is a Python implementation of JSON Web Token standards. From 2.1.0 until 2.15.0, PyJWT OKPAlgorithm.from_jwk in jwt/algorithms.py is affected because private-JWK import path does not compare the public key derived from d with x. This occurs when an OKP private JWK supplies non-corresponding x and d components. As a result, identity derived from x can differ from operations performed with d. Consequently, if an integration also accepts private key parameters from a proof header without rejecting them, an attacker may use a stolen sender-constrained token without the legitimate private key. This issue is fixed in version 2.15.0.(CVE-2026-102275)
- Record
OESA-2026-4455- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:N - Updated
- 2026-11-02
OSVOESA-2026-4456
6.5
python-jwt security update
setup.py test %endif
Security Fix(es):
PyJWT is a Python implementation of JSON Web Token standards. Prior to 2.14.0, PyJWT get_signing_key_from_jwt is affected because unknown kid misses force refreshes without a negative cache or minimum refresh interval. This occurs when unauthenticated tokens repeatedly use the same unknown kid or varying kid values absent from the cached JWKS. As a result, each cache miss causes PyJWKClient to refresh the JWKS. Consequently, attacker traffic can amplify outbound requests to the configured JWKS endpoint. This issue is fixed in version 2.14.0.(CVE-2026-101917)
PyJWT is a Python implementation of JSON Web Token standards. From 2.0.0a1 until 2.15.0, PyJWT PyJWKClient.get_signing_key_from_jwt is affected because payload parser catches ValueError but not RecursionError. This occurs when an attacker-controlled recursively nested payload reaches json.loads. As a result, documented PyJWT exception handling does not contain the failure. Consequently, an unauthenticated request can raise an exception that may produce an HTTP 500 response. The advisory-defined affected implementation also includes jwt/api_jwt.py, verify_signature=False. This issue is fixed in version 2.15.0.(CVE-2026-101918)
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.(CVE-2026-102265)
PyJWT is a Python implementation of JSON Web Token standards. Prior to 2.14.0, PyJWT signature segment is affected because signature segment decoding accepts characters outside the canonical Base64URL representation. This occurs when non-Base64URL characters are appended to a valid compact JWS signature segment. As a result, base64url_decode produces the same signature bytes for different serialized segments. Consequently, raw-token revocation checks can fail to recognize an equivalent modified token. This issue is fixed in version 2.14.0.(CVE-2026-102269)
PyJWT is a Python implementation of JSON Web Token standards. From 2.1.0 until 2.15.0, PyJWT OKPAlgorithm.from_jwk in jwt/algorithms.py is affected because private-JWK import path does not compare the public key derived from d with x. This occurs when an OKP private JWK supplies non-corresponding x and d components. As a result, identity derived from x can differ from operations performed with d. Consequently, if an integration also accepts private key parameters from a proof header without rejecting them, an attacker may use a stolen sender-constrained token without the legitimate private key. This issue is fixed in version 2.15.0.(CVE-2026-102275)
- Record
OESA-2026-4456- CVSS 3.1
- 6.5 Medium
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:N - Updated
- 2026-11-02
OSVPYSEC-2026-4142
5.3
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 survives.
Proof of concept
Against any endpoint that validates a token from its JSON body with the documented pattern:
import base64, json, http.client
def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=")
# unsigned token: the header segment is 200,000 nested '[' (266,681 bytes total)
token = (b64url(b"[" * 200_000) + b"." + b64url(b'{"a":1}') + b"." + b64url(b"x")).decode()
conn = http.client.HTTPConnection("127.0.0.1", 8125, timeout=30)
conn.request("POST", "/api/protected", body=json.dumps({"token": token}),
headers={"Content-Type": "application/json"})
print(conn.getresponse().status) # 500, expected 401
Determinism: 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.
Impact
An 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).
Fix
Catch RecursionError alongside ValueError in _load() and raise DecodeError, or parse the header with a nonrecursive, depthbounded parser.
Reproduction
A standalone package (docker-compose.yml, Dockerfile, requirements.txt, target app, driver) accompanies this report:
cd <package dir>
docker compose up -d --build
python3 poc_check_then_attack.py
docker compose down -v
Exit 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.
Credits
- @Nivid42
Maintainer update — 2026-09-09
We 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.
The fix is committed as…
- Record
PYSEC-2026-4142- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-102265
5.3
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.
- Record
UBUNTU-CVE-2026-102265- CVSS 3.1
- 5.3 Medium
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L - Updated
- 2026-11-02
OSVAZL-105230 No score
CVE-2026-102265 affecting package python-jwt for versions less than 2.15.1-1
PyJWT is a Python implementation of JSON Web Token standards. From 2.13.0 until 2.14.0, PyJWS._load in jwt/api_jws.py is affected because parser catches ValueError but not RecursionError. This occurs when a deeply nested token header reaches json.loads. As a result, RecursionError escapes the documented PyJWT error hierarchy. Consequently, an unauthenticated malformed token can cause a request-level failure and HTTP 500. This issue is fixed in version 2.14.0.
- Record
AZL-105230- Updated
- 2026-11-02
OSVCLSA-2026-1793606235 No score
TuxCare security update for pyjwt (3 CVEs)
TuxCare rebuilt pyjwt to address 3 CVEs. Fixed in 2.10.1.post8+tuxcare.
- Record
CLSA-2026-1793606235- Updated
- 2026-11-02
OSVCLSA-2026-1793698392 No score
TuxCare security update for pyjwt (4 CVEs)
TuxCare rebuilt pyjwt to address 4 CVEs. Fixed in 2.8.0.post6+tuxcare.
- Record
CLSA-2026-1793698392- Updated
- 2026-11-03
OSVCLSA-2026-1793869522 No score
TuxCare security update for pyjwt (3 CVEs)
TuxCare rebuilt pyjwt to address 3 CVEs. Fixed in 2.3.0.post6+tuxcare.
- Record
CLSA-2026-1793869522- Updated
- 2026-11-05
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
pyjwtPyPI |
2.13.0 up to 2.14.0 | 2.14.0 |
python-jwtAzure Linux:3 |
before 2.15.1-1 | 2.15.1-1 |
py3-jwtAlpaquita:25 |
2.10.1-r0 up to 2.15.1-r0 | 2.15.1-r0 |
py3-jwtAlpaquita:stream |
2.8.0-r1 up to 2.15.1-r0 | 2.15.1-r0 |
pyjwtTuxCare:PyPI From an advisory covering 3 CVEs |
2.10.1 up to 2.10.1.post8+tuxcare, plus listed versions 2.10.1.post1+tuxcare2.10.1.post2+tuxcare2.10.1.post3+tuxcare2.10.1.post4+tuxcare2.10.1.post5+tuxcare2.10.1.post6+tuxcare2.10.1.post7+tuxcare |
2.10.1.post8+tuxcare |
pyjwtTuxCare:PyPI From an advisory covering 4 CVEs |
2.8.0 up to 2.8.0.post6+tuxcare, plus listed versions 2.8.0.post1+tuxcare2.8.0.post2+tuxcare2.8.0.post3+tuxcare2.8.0.post4+tuxcare2.8.0.post5+tuxcare |
2.8.0.post6+tuxcare |
pyjwtTuxCare:PyPI From an advisory covering 3 CVEs |
2.3.0 up to 2.3.0.post6+tuxcare, plus listed versions 2.3.0.post1+tuxcare2.3.0.post2+tuxcare2.3.0.post3+tuxcare2.3.0.post4+tuxcare2.3.0.post5+tuxcare |
2.3.0.post6+tuxcare |
pyjwtDebian:12 |
All versions, plus listed versions 2.10.1-12.10.1-22.10.1-32.10.1-42.11.0-2 Show 8 moreShow less2.12.1-12.13.0-12.14.0-12.15.0-12.15.1-12.6.0-12.6.0-1+deb12u12.7.0-1 |
None known |
pyjwtDebian:13 |
All versions, plus listed versions 2.10.1-22.10.1-2+deb13u12.10.1-32.10.1-42.11.0-2 Show 5 moreShow less2.12.1-12.13.0-12.14.0-12.15.0-12.15.1-1 |
None known |
pyjwtDebian:14 |
before 2.14.0-1, plus listed versions 2.10.1-22.10.1-32.10.1-42.11.0-22.12.1-12.13.0-1 |
2.14.0-1 |
Show 11 moreShow less
| Package | Affected | Fixed |
|---|---|---|
python-jwtopenEuler:24.03-LTS-SP4 From an advisory covering 5 CVEs |
before 2.8.0-11.oe2403sp4 | 2.8.0-11.oe2403sp4 |
python-jwtopenEuler:22.03-LTS-SP4 From an advisory covering 4 CVEs |
before 2.3.0-12.oe2203sp4 | 2.3.0-12.oe2203sp4 |
python-jwtopenEuler:24.03-LTS-SP1 From an advisory covering 5 CVEs |
before 2.8.0-11.oe2403sp1 | 2.8.0-11.oe2403sp1 |
python-jwtopenEuler:24.03-LTS-SP3 From an advisory covering 5 CVEs |
before 2.8.0-11.oe2403sp3 | 2.8.0-11.oe2403sp3 |
pyjwtUbuntu:Pro:16.04:LTS |
All versions, plus listed versions 1.0.0-0ubuntu11.3.0-11.3.0-1ubuntu0.11.3.0-1ubuntu0.1+esm11.3.0-1ubuntu0.1+esm21.3.0-1ubuntu0.1+esm31.3.0-1ubuntu0.1+esm4 |
None known |
pyjwtUbuntu:Pro:18.04:LTS |
All versions, plus listed versions 1.4.2-1.11.5.3+ds1-11.5.3+ds1-1ubuntu0.11.5.3+ds1-1ubuntu0.1+esm11.5.3+ds1-1ubuntu0.1+esm21.5.3+ds1-1ubuntu0.1+esm31.5.3+ds1-1ubuntu0.1+esm4 |
None known |
pyjwtUbuntu:Pro:20.04:LTS |
All versions, plus listed versions 1.7.0-21.7.1-11.7.1-2ubuntu11.7.1-2ubuntu21.7.1-2ubuntu2.1 Show 4 moreShow less1.7.1-2ubuntu2.1+esm11.7.1-2ubuntu2.1+esm21.7.1-2ubuntu2.1+esm31.7.1-2ubuntu2.1+esm4 |
None known |
pyjwtUbuntu:22.04:LTS |
All versions, plus listed versions 1.7.1-2ubuntu22.1.0-12.3.0-12.3.0-1ubuntu0.12.3.0-1ubuntu0.2 Show 4 moreShow less2.3.0-1ubuntu0.32.3.0-1ubuntu0.42.3.0-1ubuntu0.52.3.0-1ubuntu0.6 |
None known |
pyjwtUbuntu:24.04:LTS |
All versions, plus listed versions 2.7.0-12.7.0-1ubuntu0.12.7.0-1ubuntu0.22.7.0-1ubuntu0.32.7.0-1ubuntu0.4 |
None known |
pyjwtUbuntu:26.10 |
All versions, plus listed versions 2.10.1-4ubuntu12.12.1-12.13.0-12.13.0-1ubuntu12.13.0-1ubuntu1.1 |
None known |
pyjwtUbuntu:26.04:LTS |
All versions, plus listed versions 2.10.1-22.10.1-32.10.1-3build12.10.1-42.10.1-4ubuntu12.10.1-4ubuntu1.12.10.1-4ubuntu1.22.10.1-4ubuntu1.3 |
None known |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected, aliases, references | OSV | |
| affected | ||
| affected, aliases, facts | OSV | |
| affected, aliases | OSV | |
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-102265 is a medium vulnerability (CVSS 5.3) in Jpadilla Pyjwt, published September 28, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.34%, the 28th percentile. Fixed in pyjwt 2.14.0.
References
github.com/jpadilla/pyjwt/security/advisories/GHSA-8wjv-2p76-3863
github.com/jpadilla/pyjwt/commit/06573692ebcdec8831c3927513b3e87c31fbbb62
github.com/jpadilla/pyjwt/releases/tag/2.14.0
nvd.nist.gov/vuln/detail/CVE-2026-102265
github.com/advisories/GHSA-8wjv-2p76-3863
docs.bell-sw.com/security/cves/CVE-2026-102265
Show 14 moreShow less
github.com/CVEProject/cvelistV5/tree/main/cves/2026/102xxx/CVE-2026-102265.json
security-tracker.debian.org/tracker/CVE-2026-102265
github.com/jpadilla/pyjwt
openeuler.org/zh/security/security-bulletins/detail?id=openEuler-SA-2026-4453
nvd.nist.gov/vuln/detail/CVE-2026-101917
nvd.nist.gov/vuln/detail/CVE-2026-101918
nvd.nist.gov/vuln/detail/CVE-2026-102269
nvd.nist.gov/vuln/detail/CVE-2026-102275
openeuler.org/zh/security/security-bulletins/detail?id=openEuler-SA-2026-4454
openeuler.org/zh/security/security-bulletins/detail?id=openEuler-SA-2026-4455
openeuler.org/zh/security/security-bulletins/detail?id=openEuler-SA-2026-4456
pypi.org/project/pyjwt
ubuntu.com/security/CVE-2026-102265
cve.org/CVERecord?id=CVE-2026-102265
Left out here: the rest of 3 source descriptions. The JSON record has them.