chore(deps): update dependency pyjwt to v2.13.0 [security]#184
Open
renovate[bot] wants to merge 1 commit into
Open
chore(deps): update dependency pyjwt to v2.13.0 [security]#184renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
295c875 to
4332a20
Compare
4332a20 to
d1afcd9
Compare
d1afcd9 to
d3ab0a7
Compare
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.10.1→==2.13.0PyJWT accepts unknown
critheader extensionsCVE-2026-32597 / GHSA-752w-5fwx-jx9f
More information
Details
Summary
PyJWT does not validate the
crit(Critical) Header Parameter defined inRFC 7515 §4.1.11. When a JWS token contains a
critarray listingextensions that PyJWT does not understand, the library accepts the token
instead of rejecting it. This violates the MUST requirement in the RFC.
This is the same class of vulnerability as CVE-2025-59420 (Authlib),
which received CVSS 7.5 (HIGH).
RFC Requirement
RFC 7515 §4.1.11:
Proof of Concept
Expected:
jwt.exceptions.InvalidTokenError: Unsupported critical extension: x-custom-policyActual: Token accepted, payload returned.
Comparison with RFC-compliant library
Impact
gateway using jwcrypto rejects, backend using PyJWT accepts)
critcarries enforcement semantics(MFA, token binding, scope restrictions)
cnf(Proof-of-Possession) can besilently ignored
Suggested Fix
In
jwt/api_jwt.py, add validation in_validate_headers()ordecode():CWE
References
Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
PyJWKClient unbounded JWKS endpoint requests via attacker-controlled kid values (DoS)
CVE-2026-48524 / GHSA-fhv5-28vv-h8m8
More information
Details
Summary
PyJWKClient.get_signing_key() forces a fresh HTTP request to the JWKS endpoint for every JWT with an unknown kid value, with no rate limiting. Since kid comes from the unverified token header, an attacker can trigger unlimited outbound requests.
Additionally, fetch_data() finally block clears the JWKS cache on network error.
Root Cause
jwt/jwks_client.py:172-198 - get_signing_key(kid) calls get_signing_keys(refresh=True) for unknown kids, bypassing TTL cache with no cooldown.
jwt/jwks_client.py:120-122 - finally block writes None to cache on error, clearing valid data.
Impact
Suggested Fix
Affected Versions
All versions with PyJWKClient (2.4.0 through 2.12.1)
Severity
CVSS:3.1/AV:N/AC:H/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: Algorithm allow-list bypass when decoding with
PyJWK/PyJWKClientkeysCVE-2026-48523 / GHSA-jq35-7prp-9v3f
More information
Details
PyJWT
2.9.0through2.12.1allows a verifier-side algorithm allow-list bypass whenjwt.decode()orjwt.decode_complete()are called with aPyJWKkey. The token headeralgis checked against the caller-suppliedalgorithmsallow-list, but signature verification is performed with the algorithm bound to thePyJWKobject instead of the header algorithm. An attacker who controls a registered JWK/JWKS private key can sign with a disallowed algorithm, advertise an allowed algorithm in the JWT header, and still be accepted. The issue affects the documentedPyJWKClient.get_signing_key_from_jwt(...)flow.Summary
PyJWT's
PyJWKverification path allows a verifier-side algorithm allow-list bypass.In affected versions, when a JWT is decoded with a
PyJWKobject, PyJWT verifies that the headeralgstring is present in the caller'salgorithms=[...]list, but it does not actually use the header algorithm to verify the signature. Instead, it verifies with the algorithm already bound to thePyJWKobject.This lets an attacker who controls a registered JWK/JWKS private key sign with a disallowed algorithm and have the token accepted as long as the JWT header advertises an allowed algorithm. This affects the documented
PyJWKClientusage flow and does not require any non-default flags or unsafe configuration.Details
In
jwt/api_jws.pyin2.12.1,_verify_signature()treatsPyJWKkeys differently from normal PEM/public-key inputs:This logic means:
algis checked only as a string against the caller-supplied allow-list.PyJWK, the actual verifier is not selected from the header algorithm.key.Algorithm, which is fixed when thePyJWKobject is created.PyJWKbinds its algorithm injwt/api_jwk.pyfrom the JWK'salgfield or from key-type defaults:So once a
PyJWKis constructed, the verifier uses thePyJWK's bound algorithm, not the JWT header algorithm.The issue is reachable through the documented JWKS flow. In
docs/usage.rst, the project documents:PyJWKClient.get_signing_key_from_jwt()returns aPyJWK, so this documented path is affected.This is not a "no-key forgery" issue. The attacker still needs control of an accepted JWK/JWKS private key. However, that is realistic in deployments such as:
In those cases, the attacker can bypass verifier-side algorithm policy. For example, if the server intends to only accept
PS256, an attacker controlling an accepted RSA JWK can sign withRS256, setalg=PS256in the JWT header, and still be accepted through thePyJWKpath.The same forged token is rejected through the normal PEM/public-key verification path, which shows the bug is specific to
PyJWKverification rather than expected JWT behavior.This behavior was introduced by commit
ab8176abe21e550dbc1c9a6bb7e78ad80853bfb1(Decode with PyJWK (#​886)), which is present in tagged releases2.9.0,2.10.0,2.10.1,2.11.0,2.12.0, and2.12.1.PoC
Tested locally against PyJWT
2.12.1on Python3.12.10withcryptography 45.0.6.Install dependencies:
Run the following script:
Observed output:
The token is accepted when the verification key is a
PyJWK, even though:["RS512"]RS256The same token is rejected when verified through the normal PEM/public-key path.
Impact
This is an algorithm allow-list bypass affecting
jwt.decode()andjwt.decode_complete()when the verification key is aPyJWK, including keys returned byPyJWKClient.The impact depends on the deployment model:
Impacted deployments include:
algorithms=[...]to enforce a crypto policy against externally controlled signing keysWhat an attacker can do:
PS256" or "onlyRS512"What this issue does not do by itself:
Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
PyJWT: Unauthenticated DoS via unbounded Base64URL decoding of unused payload segment in b64=false detached JWS
CVE-2026-48525 / GHSA-w7vc-732c-9m39
More information
Details
When verifying detached JWS tokens using the unencoded-payload option (
"b64": false, RFC 7797), PyJWT performs Base64URL decoding of the compact-serialization payload segment before enforcing the detached-payload rules.For
b64=false, PyJWT later discards that decoded payload and replaces it with the caller-provideddetached_payload. In practice, this turns the middle segment into an attacker-controlled “work amplifier”: a remote client can supply an arbitrarily large Base64URL payload segment that forces CPU work + memory allocations even if the signature is invalid.This creates an unauthenticated DoS vector against any endpoint that verifies detached JWS using PyJWT.
Affected Component(s)
jwt/api_jws.pyPyJWS.decode()/PyJWS.decode_complete()_load()(parsing and Base64URL decoding)Root Cause (exact logic flaw)
What happens in the code
In
jwt/api_jws.py,decode_complete()does the following (order matters):_load(jwt)first, which decodes the token segmentsheader.get("b64")and ifFalse, it replacespayload = detached_payloadand rebuilds the signing inputThis behavior is visible in
decode_complete():_load(jwt)happens before theb64=falsehandlingpayload = detached_payloadandsigning_input = ... detached_payloadhappens afterward ([GitHub][1])Inside
_load(), PyJWT unconditionally performs:payload = base64url_decode(payload_segment)This is the expensive step the attacker can amplify ([GitHub][1])
Why this becomes a vulnerability
For
b64=falsedetached JWS, the payload segment in compact form is effectively not needed for verification in PyJWT’s own logic (since the library usesdetached_payloadas the real payload). Yet PyJWT still decodes it first, meaning:Impact (evidence-driven)
Security impact
Standards context (RFC 7797)
RFC 7797 explicitly notes this option is used when payload is large and/or detached, and discusses interoperability requirements around marking it critical (“crit” with “b64”). ([IETF Datatracker][2])
(PyJWT supports
critvalidation, but the issue here is decode order / unbounded decode of an unused segment.)Affected Versions
(For GHSA, this phrasing is strong: “confirmed” + “likely since feature introduction”.)
Threat Model
Typical real deployment
A service verifies signed HTTP requests or webhooks using detached JWS:
detached_payloadAttacker
Attack chain
"b64": falseandcrit:["b64"].PyJWS.decode(...detached_payload=...).Proof of Concept - file names + results
PoC placement
server_localhost.py
client_localhost.py
flood_localhost.py
PoC # 1 - Localhost verification server
File: server_localhost.py
Purpose: real HTTP endpoint (
POST /verify) that calls PyJWT detached verification and prints:ok / time_ms / peak_bytes / token_len / error.Results (server console output)
Key takeaways from these results
At 8,000,000 chars, a single invalid-signature request still causes:
PoC # 2 - Localhost network client
File: client_localhost.py
Purpose: generates baseline + (invalid signature) + (valid signature) tokens and sends them over HTTP to localhost server.
Results (client output)
payload-chars = 500,000
payload-chars = 2,000,000
payload-chars = 8,000,000
Why this is strong evidence
PoC # 3 - Localhost flood / burst concurrency
File: flood_localhost.py
Purpose: sends N concurrent invalid-signature requests over HTTP to demonstrate queueing/worker starvation.
Results (your run: 20 concurrent @ 8,000,000 chars)
Interpretation
Fix
Goal
Prevent unbounded resource consumption from an attacker-controlled payload segment that is unused in
b64=falsedetached flow.Minimal change strategy
In
_load()(or by refactoring parse order), do not Base64-decodepayload_segmentuntil after you know whetherb64=falseapplies.Two safe options:
Reject non-empty payload segment when
b64=falseb64is false andpayload_segmentis non-empty → raiseDecodeErrorbefore decodingdetached_payloadonlySkip decoding payload segment entirely when
b64=falseThis aligns with the idea that detached payload is the trusted payload input for verification; the compact payload segment should not become a resource amplification vector.
(Implementation context: the current decode order and unconditional
base64url_decode(payload_segment)are visible in the file and line region around_load()anddecode_complete()([GitHub][1]).)Workarounds
b64=false) is not needed in your app, reject tokens where header includes"b64": false.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).
PyJWKClient: missing scheme allowlist enables CVE-2024-21643-class SSRF + token forgery via file://, ftp://, data: schemes
CVE-2026-48522 / GHSA-993g-76c3-p5m4
More information
Details
Summary
PyJWKClient passes its
uriargument directly tourllib.request.urlopen()which uses Python stdlib's defaultOpenerDirectorregisteringHTTPHandler,HTTPSHandler,FTPHandler,FileHandler, andDataHandler. There is currently no documented option to restrict which schemes PyJWKClient will fetch.If an application's
jkuURL ingestion path accepts attacker-influenced URLs (e.g., from JWT header, configuration file, OAuth flow parameter), the attacker can:file://(SSRF on local filesystem) — the file's contents are passed tojson.load.jwt.decode()accepts.Affected versions
Tested and reproducible on PyJWT 2.11.0 and 2.12.1. Likely all versions back to PyJWKClient introduction.
Reproducer (full attack chain — verified empirically)
Cross-library evidence — PyJWT is the outlier
The same composition pattern is structurally safe in 4 other mainstream JWT libraries:
jku=file://...fetch()rejects non-http(s) at fetch-spec layerhttp.DefaultTransportonly registers http/httpsHttpDocumentRetrieverdefaultsRequireHttps=truePyJWT is the only library of these 5 where the default behavior allows
file://to reach the fetch layer.Recommended fix
Add
allowed_schemes: tuple[str, ...] = ("https", "http")kwarg toPyJWKClient.__init__. Pre-validate URL scheme before invokingurllib.request.urlopen. URLs with disallowed schemes raisePyJWKClientErrorbefore any fetch is attempted.Diff sketch against
jwt/jwks_client.pyTests to add
Compatibility
allowed_schemes=("https", "http")preserves backwards compatibility for the overwhelming majority of callers using HTTP/HTTPS JWKS endpointsClass precedent
This is the same class as CVE-2024-21643 (Apache Jena JKU-trust: attacker-supplied JKU URL fetched without scheme validation). NVD-rated CVSS 7.5.
Prior art (verified 2026-05-06)
Confirmed via live recon (NVD direct, OSV.dev, PyJWT GitHub Security Advisories, issue/PR keyword search, CHANGELOG inspection):
Credit
Reported by Keijo Tuominen — independent security research at CMHT.tech (https://cmht.tech).
Reproduction artifacts available on request: full multi-language probe pack (5 wrappers × 25 fixtures × 125 cells) demonstrating cross-library divergence at the URL-scheme boundary.
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
PyJWT: Public-key JWK accepted as HMAC secret enables forged HS256 tokens when mixed families are allowed
CVE-2026-48526 / GHSA-xgmm-8j9v-c9wx
More information
Details
Summary
When the verifier is decoding JSON Web Tokens, while supporting both asymmetric and HMAC algorithms, the library does not validate use of JSON Web Keys in HMAC algorithm, allowing attacker to use the issuer public key as the secret key for HMAC algorithm.
Details
In JWT algorithm confusion attack, the verifier is mistakenly use of public key to be used as the shared secret in symmetric algorithms.
In pyjwt case, when the verifier is supporting both HMAC with other asymmetric algorithm and mistakenly using the public key of the issuer to verify the token as demonstrated in the following example:
jws.decode(token, key=rsa_jwk_json, algorithms=["HS256","RS256"]))An attacker who specifies in the token header to use HMAC, will cause the verifier to accept the JWK as the secret key in HMAC algorithm.
The attacker will be able to forge JWT signed with the public key of the issuer to impersonate any user.
If we look on current protections implemented in the library, at class HMACAlgorithm:
We can observe that there is a protection against this type of attacks but only when the verifier is using PEM format or SSH key to verify the token. JSON Web Keys, on the other hand will pass the validation.
In The following example:
jws.decode(token, key=rsa_jwk_json, algorithms=["HS256","RS256"]))There is indeed a wrong implementation of the verifier, but a stronger protection in the library side will prevent and protect against those type of misconfiugrations.
The bypass happens only if the verifier:
(a) allows HS* and an asymmetric algorithm in the same call and (b) passes a public-key value as key.
PoC
Please run the code and observe the payload printed in clear text({"sub":"alice","admin":true}')
Impact
Unauthenticated token forgery → full identity/role impersonation at the resource server (authorization bypass).
Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
Release Notes
jpadilla/pyjwt (PyJWT)
v2.13.0Compare Source
v2.12.1Compare Source
Changed
Remove algorithm requirement from JWT API, instead relying on JWS API for enforcement, by @luhn in
#​975 <https://github.com/jpadilla/pyjwt/pull/975>__Use
Sequencefor parameter types rather thanListwhere applicable by @imnotjames in#​970 <https://github.com/jpadilla/pyjwt/pull/970>__Add JWK support to JWT encode by @luhn in
#​979 <https://github.com/jpadilla/pyjwt/pull/979>__Encoding and decoding payloads using the
nonealgorithm by @jpadilla in#c2629f6 <https://github.com/jpadilla/pyjwt/commit/c2629f66c593459e02616048443231ccbe18be16>__Before:
.. code-block:: pycon
After:
.. code-block:: pycon
Added validation for 'sub' (subject) and 'jti' (JWT ID) claims in tokens by @Divan009 in
#​1005 <https://github.com/jpadilla/pyjwt/pull/1005>__Refactor project configuration files from
setup.cfgtopyproject.tomlby @cleder in#​995 <https://github.com/jpadilla/pyjwt/pull/995>__Ruff linter and formatter changes by @gagandeepp in
#​1001 <https://github.com/jpadilla/pyjwt/pull/1001>__Drop support for Python 3.8 (EOL) by @kkirsche in
#​1007 <https://github.com/jpadilla/pyjwt/pull/1007>__Fixed
#​972 <https://github.com/jpadilla/pyjwt/pull/972>__#​973 <https://github.com/jpadilla/pyjwt/pull/973>__#​992 <https://github.com/jpadilla/pyjwt/pull/992>__#​980 <https://github.com/jpadilla/pyjwt/pull/980>__#​993 <https://github.com/jpadilla/pyjwt/pull/993>__pyproject.tomlinpre-commitby @cleder in#​1002 <https://github.com/jpadilla/pyjwt/pull/1002>__#​1003 <https://github.com/jpadilla/pyjwt/pull/1003>__v2.12.0Compare Source
Security
What's Changed
New Contributors
Full Changelog: jpadilla/pyjwt@2.11.0...2.12.0
v2.11.0Compare Source
Fixed
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 this update again.
This PR was generated by Mend Renovate. View the repository job log.