Repository navigation
chore(deps): update dependency pyjwt to v2.15.0 [security] - #366
Open
renovate[bot] wants to merge 1 commit into
Open
renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
renovate
Bot
force-pushed
the
renovate/pypi-pyjwt-vulnerability
branch
from
October 4, 2026 11:31
dd73281 to
2da1043
Compare
❌MegaLinter analysis: Error❌ REPOSITORY / grype - 3 errors❌ REPOSITORY / osv-scanner - 3 errors❌ REPOSITORY / trivy - 1 error
|
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

This PR contains the following updates:
==2.14.0→==2.15.02.14.0→2.15.0Warning
Some dependencies could not be looked up. Check the Dependency Dashboard for more information.
PyJWT: Unauthenticated RecursionError DoS in pre-verification payload parse (PyJWKClient.get_signing_key_from_jwt / verify_signature=False)
CVE-2026-101918 / GHSA-42vr-xj54-vc7v
More information
Details
Summary
PyJWKClient.get_signing_key_from_jwt(token)— the first step of the JWKSverification flow documented in
docs/usage.rst— must decode a token'spayload before its signature can be checked, via
jwt.api_jwt.decode_complete(token, options={"verify_signature": False}).That call parses the payload with
json.loadsinPyJWT._decode_payload(
jwt/api_jwt.py:297-300), whoseexceptclause catches onlyValueError.A payload that is valid JSON but nested ~20,000 levels deep makes
json.loadsraiseRecursionError, which is not aValueErrorandescapes as a raw, undocumented exception type — not
DecodeError,InvalidTokenError, orPyJWTError, so every documented error-handlingpattern in
docs/usage.rstmisses it.The token needs no valid signature and no network access — the crash
happens during payload parsing, before the
kidis even looked up. Asingle unauthenticated ~50KB request crashes the caller's auth handler
(HTTP 500 / dead worker), repeatably. The same path is reachable through
plain
jwt.decode(token, options={"verify_signature": False})too.Notably, this project already fixed the identical bug class for the
JWS header path —
PyJWS._loadcatches(ValueError, RecursionError)and wraps it in
DecodeError(jwt/api_jws.py:360-361), andCHANGELOG.rst(v2.14.0, "Security") states "Handle deeply nested andmalformed JWS/JWK input without uncaught recursion errors." The payload
path — the one part of a forged token an unauthenticated attacker fully
controls — was left catching
ValueErroralone, so that security fix doesnot fully hold.
A second, related instance:
PyJWKClient.fetch_data(jwt/jwks_client.py:168)parses a JWKS endpoint's response with
json.load(response)inside atrythat only catches(URLError, TimeoutError, http.client.HTTPException)— a deeply-nested JSON response from a JWKS endpoint raises the same raw
RecursionError, uncaught entirely (worse than the payload path, which atleast caught plain
ValueError).Affected versions: confirmed present in the current release, 2.14.0,
and on the current
masterbranch (commit4adcd02722f5011c60079d3978dfc167b9a8eaa5).Reproduction
Control: the same deeply-nested structure moved into the token's header
instead of its payload is correctly converted to
jwt.DecodeErrorby thealready-hardened
jwt/api_jws.py:360— confirming the payload path isspecifically the missed half of the v2.14.0 hardening, not a general gap.
Impact assessment
Unauthenticated denial-of-service via unhandled exception on an auth path.
Not an auth bypass — rated medium. With signature verification enabled,
this parse runs after
_verify_signature, so only the pre-verificationpaths (
get_signing_key_from_jwt, explicitverify_signature=False) areattacker-reachable pre-auth; the JWKS-endpoint variant requires control of
(or a MITM on) the configured JWKS endpoint rather than being reachable
from an arbitrary client token.
Suggested fix
In
jwt/api_jwt.py:299, changeexcept ValueError as e:toexcept (ValueError, RecursionError) as e:, mirroringjwt/api_jws.py:360exactly. Injwt/jwks_client.py, add the same(ValueError, RecursionError)handling aroundjson.load(response)infetch_data, raisingPyJWKClientError(consistent with the method'sexisting documented error contract). A patch implementing both, verified
against the full test suite (457 passed, 4 skipped — pre-existing,
environment-related) and against the reproduction above (now correctly
raises
DecodeError), is attached (fix.patch).Discovery method
Found and verified using
scopegrep(https://github.com/not-ekalabya/scopegrep)— a semantic code-retrieval tool that surfaces every other call site of a
symbol alongside relevance-ranked results, which is what surfaced the
already-hardened header path as the direct comparison here — paired with
an LLM coding agent (GLM-5.3) run as an open-ended security review of this
repository. Independently reproduced against the exact commit above before
this report was written. Happy to share the full session transcript on
request.
Disclosure status
Not shared with any other party or published. Submitting through this
private channel per the project's stated security policy; no planned
public/conference disclosure ahead of a coordinated timeline.
Maintainer triage update (2026-09-22)
Confirmed finding and scope
We confirmed that an attacker-controlled recursively nested JWT payload can cause a raw Python
RecursionErrorto escape PyJWT at PyJWT 2.14.0 and the tested current source. The confirmed in-scope paths are direct decoding withverify_signature=FalseandPyJWKClient.get_signing_key_from_jwt, where payload parsing occurs before key lookup.The demonstrated impact is limited to an uncaught exception for the affected call, which may surface as an application HTTP 500 when the application does not catch it. Testing did not demonstrate a worker or process crash, persistent resource exhaustion, resource amplification, authentication bypass, or confidentiality or integrity impact.
The separate JWKS-response subclaim is out of scope under the policy boundary that requires the application to trust its configured JWKS source and transport. The confirmed payload finding is not a duplicate of the earlier protected-header parser finding: it occurs in a distinct payload parsing path that remained affected after the earlier header-only fix.
Version and remediation status
Historical testing reproduced the confirmed payload behavior in all 20 official supported PyJWT 2.x releases from 2.0.0a1 through 2.14.0, inclusive. Unsupported PyJWT 1.7.1 also reproduces the behavior, but it is excluded from the advisory range under the supported-2.x policy. No supported unaffected release and no patched release exists. The exact evidence is
/Users/jpadilla/.codex/security-advisories/pyjwt/GHSA-42vr-xj54-vc7v-historical-range-20260922.md(SHA-256c48f1ac35641e9382d53e6a879a71ad9a8fb4432bbe871f2b8330f8d166a2d81) and/Users/jpadilla/.codex/security-advisories/pyjwt/historical-range-42vr-20260922/historical-range-results.json(SHA-256324997f231561bf6da39b8f9d1e272f69bfde8871a7863f5cb7b70e22e91f076). The evidence-backed supported affected range is>= 2.0.0a1, <= 2.14.0.Historical range verification is complete. A local fix commit exists and has passed independent review and the complete local CI suite, but it has not been merged into
masteror released. Consequently,patched_versionsremains empty and this advisory remains intriage. The remaining next step is a maintainer decision on remediation and merge. After a fix lands inmasterand is released,patched_versionsand advisory lifecycle can be updated in a separate approved batch.Maintainer remediation update (2026-09-23)
The confirmed payload-parser finding has been fixed on
master. Commit5fde08a6cf906aa7698de2d6391d88b73006b17bconverts a recursivepayload parse failure to the expected
DecodeError; commit9bc06658f875b9b40091539140bbbdc4639161c3makes the regression testsdeterministic across supported Python versions. This update supersedes the
2026-09-22 remediation-status statement that the fix had not landed.
The original pre-verification payload reproducer and the
PyJWKClient.get_signing_key_from_jwtpath were covered by regression tests.The exact landed tree passed the full 39-environment local tox matrix and
GitHub CI for commit
9bc06658f875b9b40091539140bbbdc4639161c3passedall 25 jobs, including Ubuntu Python 3.14 and 3.15. The demonstrated impact
remains an uncaught request-level exception in affected releases; there is
still no evidence of process termination, persistent resource exhaustion,
authentication bypass, or confidentiality or integrity impact.
The supported affected range remains
>= 2.0.0a1, <= 2.14.0. The latestreleased version is 2.14.0, which predates these commits; no released
patched version exists yet, so
patched_versionsremains unset. CVSS v3.15.3 (Medium) and CWE-248 remain unchanged. The advisory may now move from
triage to private draft. The next lifecycle step is a new supported 2.x
release containing the fix, followed by separately approved patched-version
and publication updates after release verification.
Maintainer release update (2026-09-23)
PyJWT 2.15.0 is the first released version containing the payload-parser fix. Its GitHub release tag points to commit
1d41a6478e1562e68ff667fcd703356acf085f68, which includes fix commit5fde08a6cf906aa7698de2d6391d88b73006b17band deterministic regression-test commit9bc06658f875b9b40091539140bbbdc4639161c3. PyPI published both the wheel and source distribution from that release commit through the successful production workflow at https://github.com/jpadilla/pyjwt/actions/runs/35891602963.The published PyPI wheel (
pyjwt-2.15.0-py3-none-any.whl, SHA-2567a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8) was checked directly: the deeply nested unsigned payload now raisesDecodeError, while an ordinary unsigned payload still decodes. The supported affected range remains>= 2.0.0a1, <= 2.14.0; the patched version is2.15.0. Earlier statements in this advisory that no released patched version exists are superseded by this update.The confirmed impact remains an uncaught request-level exception in affected versions, not a demonstrated process crash or authentication bypass. The separate JWKS-response subclaim remains outside this advisory's confirmed PyJWT-owned scope.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
PyJWT: Unauthenticated RecursionError DoS in pre-verification payload parse (PyJWKClient.get_signing_key_from_jwt / verify_signature=False)
CVE-2026-101918 / GHSA-42vr-xj54-vc7v / PYSEC-2026-4141
More information
Details
Summary
PyJWKClient.get_signing_key_from_jwt(token)— the first step of the JWKSverification flow documented in
docs/usage.rst— must decode a token'spayload before its signature can be checked, via
jwt.api_jwt.decode_complete(token, options={"verify_signature": False}).That call parses the payload with
json.loadsinPyJWT._decode_payload(
jwt/api_jwt.py:297-300), whoseexceptclause catches onlyValueError.A payload that is valid JSON but nested ~20,000 levels deep makes
json.loadsraiseRecursionError, which is not aValueErrorandescapes as a raw, undocumented exception type — not
DecodeError,InvalidTokenError, orPyJWTError, so every documented error-handlingpattern in
docs/usage.rstmisses it.The token needs no valid signature and no network access — the crash
happens during payload parsing, before the
kidis even looked up. Asingle unauthenticated ~50KB request crashes the caller's auth handler
(HTTP 500 / dead worker), repeatably. The same path is reachable through
plain
jwt.decode(token, options={"verify_signature": False})too.Notably, this project already fixed the identical bug class for the
JWS header path —
PyJWS._loadcatches(ValueError, RecursionError)and wraps it in
DecodeError(jwt/api_jws.py:360-361), andCHANGELOG.rst(v2.14.0, "Security") states "Handle deeply nested andmalformed JWS/JWK input without uncaught recursion errors." The payload
path — the one part of a forged token an unauthenticated attacker fully
controls — was left catching
ValueErroralone, so that security fix doesnot fully hold.
A second, related instance:
PyJWKClient.fetch_data(jwt/jwks_client.py:168)parses a JWKS endpoint's response with
json.load(response)inside atrythat only catches(URLError, TimeoutError, http.client.HTTPException)— a deeply-nested JSON response from a JWKS endpoint raises the same raw
RecursionError, uncaught entirely (worse than the payload path, which atleast caught plain
ValueError).Affected versions: confirmed present in the current release, 2.14.0,
and on the current
masterbranch (commit4adcd02722f5011c60079d3978dfc167b9a8eaa5).Reproduction
Control: the same deeply-nested structure moved into the token's header
instead of its payload is correctly converted to
jwt.DecodeErrorby thealready-hardened
jwt/api_jws.py:360— confirming the payload path isspecifically the missed half of the v2.14.0 hardening, not a general gap.
Impact assessment
Unauthenticated denial-of-service via unhandled exception on an auth path.
Not an auth bypass — rated medium. With signature verification enabled,
this parse runs after
_verify_signature, so only the pre-verificationpaths (
get_signing_key_from_jwt, explicitverify_signature=False) areattacker-reachable pre-auth; the JWKS-endpoint variant requires control of
(or a MITM on) the configured JWKS endpoint rather than being reachable
from an arbitrary client token.
Suggested fix
In
jwt/api_jwt.py:299, changeexcept ValueError as e:toexcept (ValueError, RecursionError) as e:, mirroringjwt/api_jws.py:360exactly. Injwt/jwks_client.py, add the same(ValueError, RecursionError)handling aroundjson.load(response)infetch_data, raisingPyJWKClientError(consistent with the method'sexisting documented error contract). A patch implementing both, verified
against the full test suite (457 passed, 4 skipped — pre-existing,
environment-related) and against the reproduction above (now correctly
raises
DecodeError), is attached (fix.patch).Discovery method
Found and verified using
scopegrep(https://github.com/not-ekalabya/scopegrep)— a semantic code-retrieval tool that surfaces every other call site of a
symbol alongside relevance-ranked results, which is what surfaced the
already-hardened header path as the direct comparison here — paired with
an LLM coding agent (GLM-5.3) run as an open-ended security review of this
repository. Independently reproduced against the exact commit above before
this report was written. Happy to share the full session transcript on
request.
Disclosure status
Not shared with any other party or published. Submitting through this
private channel per the project's stated security policy; no planned
public/conference disclosure ahead of a coordinated timeline.
Maintainer triage update (2026-09-22)
Confirmed finding and scope
We confirmed that an attacker-controlled recursively nested JWT payload can cause a raw Python
RecursionErrorto escape PyJWT at PyJWT 2.14.0 and the tested current source. The confirmed in-scope paths are direct decoding withverify_signature=FalseandPyJWKClient.get_signing_key_from_jwt, where payload parsing occurs before key lookup.The demonstrated impact is limited to an uncaught exception for the affected call, which may surface as an application HTTP 500 when the application does not catch it. Testing did not demonstrate a worker or process crash, persistent resource exhaustion, resource amplification, authentication bypass, or confidentiality or integrity impact.
The separate JWKS-response subclaim is out of scope under the policy boundary that requires the application to trust its configured JWKS source and transport. The confirmed payload finding is not a duplicate of the earlier protected-header parser finding: it occurs in a distinct payload parsing path that remained affected after the earlier header-only fix.
Version and remediation status
Historical testing reproduced the confirmed payload behavior in all 20 official supported PyJWT 2.x releases from 2.0.0a1 through 2.14.0, inclusive. Unsupported PyJWT 1.7.1 also reproduces the behavior, but it is excluded from the advisory range under the supported-2.x policy. No supported unaffected release and no patched release exists. The exact evidence is
/Users/jpadilla/.codex/security-advisories/pyjwt/GHSA-42vr-xj54-vc7v-historical-range-20260922.md(SHA-256c48f1ac35641e9382d53e6a879a71ad9a8fb4432bbe871f2b8330f8d166a2d81) and/Users/jpadilla/.codex/security-advisories/pyjwt/historical-range-42vr-20260922/historical-range-results.json(SHA-256324997f231561bf6da39b8f9d1e272f69bfde8871a7863f5cb7b70e22e91f076). The evidence-backed supported affected range is>= 2.0.0a1, <= 2.14.0.Historical range verification is complete. A local fix commit exists and has passed independent review and the complete local CI suite, but it has not been merged into
masteror released. Consequently,patched_versionsremains empty and this advisory remains intriage. The remaining next step is a maintainer decision on remediation and merge. After a fix lands inmasterand is released,patched_versionsand advisory lifecycle can be updated in a separate approved batch.Maintainer remediation update (2026-09-23)
The confirmed payload-parser finding has been fixed on
master. Commit5fde08a6cf906aa7698de2d6391d88b73006b17bconverts a recursivepayload parse failure to the expected
DecodeError; commit9bc06658f875b9b40091539140bbbdc4639161c3makes the regression testsdeterministic across supported Python versions. This update supersedes the
2026-09-22 remediation-status statement that the fix had not landed.
The original pre-verification payload reproducer and the
PyJWKClient.get_signing_key_from_jwtpath were covered by regression tests.The exact landed tree passed the full 39-environment local tox matrix and
GitHub CI for commit
9bc06658f875b9b40091539140bbbdc4639161c3passedall 25 jobs, including Ubuntu Python 3.14 and 3.15. The demonstrated impact
remains an uncaught request-level exception in affected releases; there is
still no evidence of process termination, persistent resource exhaustion,
authentication bypass, or confidentiality or integrity impact.
The supported affected range remains
>= 2.0.0a1, <= 2.14.0. The latestreleased version is 2.14.0, which predates these commits; no released
patched version exists yet, so
patched_versionsremains unset. CVSS v3.15.3 (Medium) and CWE-248 remain unchanged. The advisory may now move from
triage to private draft. The next lifecycle step is a new supported 2.x
release containing the fix, followed by separately approved patched-version
and publication updates after release verification.
Maintainer release update (2026-09-23)
PyJWT 2.15.0 is the first released version containing the payload-parser fix. Its GitHub release tag points to commit
1d41a6478e1562e68ff667fcd703356acf085f68, which includes fix commit5fde08a6cf906aa7698de2d6391d88b73006b17band deterministic regression-test commit9bc06658f875b9b40091539140bbbdc4639161c3. PyPI published both the wheel and source distribution from that release commit through the successful production workflow at https://github.com/jpadilla/pyjwt/actions/runs/35891602963.The published PyPI wheel (
pyjwt-2.15.0-py3-none-any.whl, SHA-2567a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8) was checked directly: the deeply nested unsigned payload now raisesDecodeError, while an ordinary unsigned payload still decodes. The supported affected range remains>= 2.0.0a1, <= 2.14.0; the patched version is2.15.0. Earlier statements in this advisory that no released patched version exists are superseded by this update.The confirmed impact remains an uncaught request-level exception in affected versions, not a demonstrated process crash or authentication bypass. The separate JWKS-response subclaim remains outside this advisory's confirmed PyJWT-owned scope.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
PyJWT accepts inconsistent OKP x/d JWKs, causing public/private key identity confusion
CVE-2026-102275 / GHSA-x33g-cr3x-6449 / PYSEC-2026-4183
More information
Details
Summary
PyJWT accepts internally inconsistent OKP private JWKs where the declared public key
xdoes not correspond to the supplied private keyd.When
dis present,OKPAlgorithm.from_jwk()constructs the Ed25519/Ed448 private key fromdwithout verifying that the public key derived fromdmatchesx. As a result, the original JWK can contain one public key identity while PyJWT performs cryptographic operations using a different key.In affected DPoP integrations, this can allow a stolen sender-constrained access token to be used without possession of the legitimate holder's private key.
Details
The vulnerable logic is in
jwt/algorithms.py:When
dis present,xis parsed but never compared with the public key derived fromd.For example, PyJWT accepts:
{ "kty": "OKP", "crv": "Ed25519", "x": "<legitimate-user-public-key>", "d": "<attacker-private-key>" }even though
xanddbelong to different key pairs.The behavior has existed since OKP private-JWK import was introduced:
A correct import should derive the public key from
dand reject the JWK when it does not equal the suppliedx.PoC
The reproducer creates two Ed25519 keypairs:
It then:
cnf.jktis bound to the legitimate public key.xtogether with the attacker'sd.x, which still matches the access-token binding.PyJWK.from_dict().d.Observed result:
Tested on PyJWT 2.13.0, PyJWT 2.14.0, and current master.
The complete reproducer is:
A public example of the affected composition exists in StrongDM's
agentic-authFlask middleware: it computes an OKP thumbprint fromxand subsequently passes the entire JWK toPyJWK.from_dict()for EdDSA DPoP verification without rejectingd.Impact
The PyJWT-owned issue is acceptance of internally inconsistent cryptographic key material: the JWK can declare one public key while the operative private key corresponds to another.
In a DPoP verifier that:
cnf.jkt/ JWK thumbprints from the declaredx,an attacker who steals a sender-constrained access token can use the token without possessing the legitimate holder's private key.
RFC 9449 §4.2 requires DPoP implementations to reject private key parameters in proof JWKs. Therefore the demonstrated authentication bypass additionally requires a DPoP implementation that omits this check. RFC-compliant DPoP implementations that reject
dare not vulnerable to the demonstrated attack.Maintainer assessment (2026-09-12)
We independently reproduced the PyJWT-owned issue: importing a private OKP JWK with non-corresponding
xanddcomponents returned the private key derived fromdwhile accepting the conflicting declared public identity. The precise library impact is loss of JWK key-component consistency; a caller that usesxas the JWK identity and the returned key for cryptographic operations can act on two different key identities.Exact boundary checks found Ed25519 JWK import absent in 2.0.1 and the inconsistent-pair behavior present in 2.1.0. Ed448 JWK import was absent in 2.1.0 and the behavior was present in 2.2.0. Both curves reproduced in 2.14.0 and the tested pre-fix source. These samples support the existing affected metadata (
>= 2.1.0, <= 2.14.0) and the per-curve introduction points, but do not claim exhaustive testing of every intervening release.The fix is on
masterin commit3cd9ceec33ced359decbad75b413ad668ae6332c. It derives the public bytes fromd, compares them withx, and rejects a mismatch. Fresh independent Astra/max review accepted the exact tree9f219601e39026c7094d48f3fbb6678951878aa8with no blocking findings or required revisions. Targeted red/green and sibling-path checks passed, and the exact repository CI commandpython -m toxpassed all 35 required environments both before and after the commit.The reported DPoP stolen-token scenario additionally requires an application to pass a private JWK from the proof header without rejecting private-key parameters. RFC 9449 sections 4.2 and 4.3 require such parameters to be absent or rejected, so that bypass is application-dependent and outside PyJWT's security-policy scope; it is not used to characterize the PyJWT-owned impact.
The fix is not yet contained in a released PyJWT 2.x version, so
patched_versionsremains empty and this advisory remains unpublished. The next lifecycle step is to move the advisory from triage to draft now that the verified fix is onmaster; publication must wait until a new released 2.x version is confirmed to contain the fix and is recorded as the patched boundary.Maintainer release update (2026-09-23)
PyJWT 2.15.0 is the first released version containing the OKP JWK consistency fix. Its GitHub release tag points to commit
1d41a6478e1562e68ff667fcd703356acf085f68, which includes fix commit3cd9ceec33ced359decbad75b413ad668ae6332c. PyPI published both the wheel and source distribution from that release commit through the successful production workflow at https://github.com/jpadilla/pyjwt/actions/runs/35891602963.The published PyPI wheel (
pyjwt-2.15.0-py3-none-any.whl, SHA-2567a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8) was checked directly: matching Ed25519 and Ed448x/dprivate JWK components import successfully, while mismatched components raiseInvalidKeyError. The supported affected range remains>= 2.1.0, <= 2.14.0; the patched version is2.15.0. The earlier statement that no released patched version exists is superseded by this update.The PyJWT-owned impact is acceptance of internally inconsistent OKP key material in affected versions. The reported DPoP stolen-token scenario additionally requires an application to accept private key parameters from a proof header, contrary to RFC 9449; it is not the basis for this advisory's classification. CVSS v3.1 6.5 (Medium), CWE-345, and CWE-348 are unchanged.
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
PyJWT: Unauthenticated RecursionError DoS in pre-verification payload parse (PyJWKClient.get_signing_key_from_jwt / verify_signature=False)
CVE-2026-101918 / GHSA-42vr-xj54-vc7v / PYSEC-2026-4141
More information
Details
Summary
PyJWKClient.get_signing_key_from_jwt(token)— the first step of the JWKSverification flow documented in
docs/usage.rst— must decode a token'spayload before its signature can be checked, via
jwt.api_jwt.decode_complete(token, options={"verify_signature": False}).That call parses the payload with
json.loadsinPyJWT._decode_payload(
jwt/api_jwt.py:297-300), whoseexceptclause catches onlyValueError.A payload that is valid JSON but nested ~20,000 levels deep makes
json.loadsraiseRecursionError, which is not aValueErrorandescapes as a raw, undocumented exception type — not
DecodeError,InvalidTokenError, orPyJWTError, so every documented error-handlingpattern in
docs/usage.rstmisses it.The token needs no valid signature and no network access — the crash
happens during payload parsing, before the
kidis even looked up. Asingle unauthenticated ~50KB request crashes the caller's auth handler
(HTTP 500 / dead worker), repeatably. The same path is reachable through
plain
jwt.decode(token, options={"verify_signature": False})too.Notably, this project already fixed the identical bug class for the
JWS header path —
PyJWS._loadcatches(ValueError, RecursionError)and wraps it in
DecodeError(jwt/api_jws.py:360-361), andCHANGELOG.rst(v2.14.0, "Security") states "Handle deeply nested andmalformed JWS/JWK input without uncaught recursion errors." The payload
path — the one part of a forged token an unauthenticated attacker fully
controls — was left catching
ValueErroralone, so that security fix doesnot fully hold.
A second, related instance:
PyJWKClient.fetch_data(jwt/jwks_client.py:168)parses a JWKS endpoint's response with
json.load(response)inside atrythat only catches(URLError, TimeoutError, http.client.HTTPException)— a deeply-nested JSON response from a JWKS endpoint raises the same raw
RecursionError, uncaught entirely (worse than the payload path, which atleast caught plain
ValueError).Affected versions: confirmed present in the current release, 2.14.0,
and on the current
masterbranch (commit4adcd02722f5011c60079d3978dfc167b9a8eaa5).Reproduction
Control: the same deeply-nested structure moved into the token's header
instead of its payload is correctly converted to
jwt.DecodeErrorby thealready-hardened
jwt/api_jws.py:360— confirming the payload path isspecifically the missed half of the v2.14.0 hardening, not a general gap.
Impact assessment
Unauthenticated denial-of-service via unhandled exception on an auth path.
Not an auth bypass — rated medium. With signature verification enabled,
this parse runs after
_verify_signature, so only the pre-verificationpaths (
get_signing_key_from_jwt, explicitverify_signature=False) areattacker-reachable pre-auth; the JWKS-endpoint variant requires control of
(or a MITM on) the configured JWKS endpoint rather than being reachable
from an arbitrary client token.
Suggested fix
In
jwt/api_jwt.py:299, changeexcept ValueError as e:toexcept (ValueError, RecursionError) as e:, mirroringjwt/api_jws.py:360exactly. Injwt/jwks_client.py, add the same(ValueError, RecursionError)handling aroundjson.load(response)infetch_data, raisingPyJWKClientError(consistent with the method'sexisting documented error contract). A patch implementing both, verified
against the full test suite (457 passed, 4 skipped — pre-existing,
environment-related) and against the reproduction above (now correctly
raises
DecodeError), is attached (fix.patch).Discovery method
Found and verified using
scopegrep(https://github.com/not-ekalabya/scopegrep)— a semantic code-retrieval tool that surfaces every other call site of a
symbol alongside relevance-ranked results, which is what surfaced the
already-hardened header path as the direct comparison here — paired with
an LLM coding agent (GLM-5.3) run as an open-ended security review of this
repository. Independently reproduced against the exact commit above before
this report was written. Happy to share the full session transcript on
request.
Disclosure status
Not shared with any other party or published. Submitting through this
private channel per the project's stated security policy; no planned
public/conference disclosure ahead of a coordinated timeline.
Maintainer triage update (2026-09-22)
Confirmed finding and scope
We confirmed that an attacker-controlled recursively nested JWT payload can cause a raw Python
RecursionErrorto escape PyJWT at PyJWT 2.14.0 and the tested current source. The confirmed in-scope paths are direct decoding withverify_signature=FalseandPyJWKClient.get_signing_key_from_jwt, where payload parsing occurs before key lookup.The demonstrated impact is limited to an uncaught exception for the affected call, which may surface as an application HTTP 500 when the application does not catch it. Testing did not demonstrate a worker or process crash, persistent resource exhaustion, resource amplification, authentication bypass, or confidentiality or integrity impact.
The separate JWKS-response subclaim is out of scope under the policy boundary that requires the application to trust its configured JWKS source and transport. The confirmed payload finding is not a duplicate of the earlier protected-header parser finding: it occurs in a distinct payload parsing path that remained affected after the earlier header-only fix.
Version and remediation status
Historical testing reproduced the confirmed payload behavior in all 20 official supported PyJWT 2.x releases from 2.0.0a1 through 2.14.0, inclusive. Unsupported PyJWT 1.7.1 also reproduces the behavior, but it is excluded from the advisory range under the supported-2.x policy. No supported unaffected release and no patched release exists. The exact evidence is
/Users/jpadilla/.codex/security-advisories/pyjwt/GHSA-42vr-xj54-vc7v-historical-range-20260922.md(SHA-256c48f1ac35641e9382d53e6a879a71ad9a8fb4432bbe871f2b8330f8d166a2d81) and/Users/jpadilla/.codex/security-advisories/pyjwt/historical-range-42vr-20260922/historical-range-results.json(SHA-256324997f231561bf6da39b8f9d1e272f69bfde8871a7863f5cb7b70e22e91f076). The evidence-backed supported affected range is>= 2.0.0a1, <= 2.14.0.Historical range verification is complete. A local fix commit exists and has passed independent review and the complete local CI suite, but it has not been merged into
masteror released. Consequently,patched_versionsremains empty and this advisory remains intriage. The remaining next step is a maintainer decision on remediation and merge. After a fix lands inmasterand is released,patched_versionsand advisory lifecycle can be updated in a separate approved batch.Maintainer remediation update (2026-09-23)
The confirmed payload-parser finding has been fixed on
master. Commit5fde08a6cf906aa7698de2d6391d88b73006b17bconverts a recursivepayload parse failure to the expected
DecodeError; commit9bc06658f875b9b40091539140bbbdc4639161c3makes the regression testsdeterministic across supported Python versions. This update supersedes the
2026-09-22 remediation-status statement that the fix had not landed.
The original pre-verification payload reproducer and the
PyJWKClient.get_signing_key_from_jwtpath were covered by regression tests.The exact landed tree passed the full 39-environment local tox matrix and
GitHub CI for commit
9bc06658f875b9b40091539140bbbdc4639161c3passedall 25 jobs, including Ubuntu Python 3.14 and 3.15. The demonstrated impact
remains an uncaught request-level exception in affected releases; there is
still no evidence of process termination, persistent resource exhaustion,
authentication bypass, or confidentiality or integrity impact.
The supported affected range remains
>= 2.0.0a1, <= 2.14.0. The latestreleased version is 2.14.0, which predates these commits; no released
patched version exists yet, so
patched_versionsremains unset. CVSS v3.15.3 (Medium) and CWE-248 remain unchanged. The advisory may now move from
triage to private draft. The next lifecycle step is a new supported 2.x
release containing the fix, followed by separately approved patched-version
and publication updates after release verification.
Maintainer release update (2026-09-23)
PyJWT 2.15.0 is the first released version containing the payload-parser fix. Its GitHub release tag points to commit
1d41a6478e1562e68ff667fcd703356acf085f68, which includes fix commit5fde08a6cf906aa7698de2d6391d88b73006b17band deterministic regression-test commit9bc06658f875b9b40091539140bbbdc4639161c3. PyPI published both the wheel and source distribution from that release commit through the successful production workflow at https://github.com/jpadilla/pyjwt/actions/runs/35891602963.The published PyPI wheel (
pyjwt-2.15.0-py3-none-any.whl, SHA-2567a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8) was checked directly: the deeply nested unsigned payload now raisesDecodeError, while an ordinary unsigned payload still decodes. The supported affected range remains>= 2.0.0a1, <= 2.14.0; the patched version is2.15.0. Earlier statements in this advisory that no released patched version exists are superseded by this update.The confirmed impact remains an uncaught request-level exception in affected versions, not a demonstrated process crash or authentication bypass. The separate JWKS-response subclaim remains outside this advisory's confirmed PyJWT-owned scope.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:LReferences
This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).
CVE-2026-102275 / GHSA-x33g-cr3x-6449 / PYSEC-2026-4183
More information
Details
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.
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:NReferences
This data is provided by OSV and the PyPI Advisory Database (CC-BY 4.0).
Release Notes
jpadilla/pyjwt (pyjwt)
v2.15.0Compare Source
See the 2.15.0 changelog for complete release details.
Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about these updates again.
This PR was generated by Mend Renovate. View the repository job log.