Update dependency pyjwt to v2.15.0 [SECURITY] - #841
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
Generated by renovateBot
renovate
Bot
requested review from
changt,
cloudwebrtc,
lukasIO and
xianshijing-lk
as code owners
October 1, 2026 12:00
Contributor
There was a problem hiding this comment.
🔍 Devin Review: 1 flag
Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
This branch has not been 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.13.0→2.15.0Warning
Some dependencies could not be looked up. Check the Dependency Dashboard for more information.
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: 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).
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 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).
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: Malformed RSA JWK aborts parsing of an entire JWK Set
CVE-2026-102274 / GHSA-w6j9-cwv2-h6wq
More information
Details
Summary
A malformed RSA JWK inside a JWK Set aborts parsing of the entire set instead of being skipped, because
RSAAlgorithm.from_jwkcan raise a plainValueErrorthat isn't caught byPyJWKSet's per-key error-skipping logic.Affected component / version
PyJWT(PyPI, ecosystempip)jwt/api_jwk.py(PyJWK.__init__,PyJWKSet.__init__),jwt/algorithms.py(RSAAlgorithm.from_jwk)masterbranch as of 2026-09-05 (commit7144e4534c34810f4525dc4578a32addd8212cff, tag2.13.0). Directly verified identical in tags2.9.0,2.10.0,2.11.0,2.12.0,2.12.1,2.13.0-- the vulnerable call and theexcept PyJWTErrorguard are unchanged across all six releases. Not verified against any release prior to2.9.0.Details
PyJWKSet.__init__(jwt/api_jwk.py:145-152) iterates each key in a JWK Set:PyJWK.__init__(api_jwk.py:82) callsself.Algorithm.from_jwk(self._jwk_data)with no try/except of its own. For an RSA JWK, this dispatches toRSAAlgorithm.from_jwk(jwt/algorithms.py:539-586). When the JWK suppliesd,e,nwithout the CRT parameters (p,q,dp,dq,qi),from_jwkcallscryptography'srsa_recover_prime_factors(public_numbers.n, d, public_numbers.e)(algorithms.py:572-574) to derive the key. Ifdis not the correct private exponent for thatn/epair,rsa_recover_prime_factorsraises a plainValueError.ValueErroris a built-in Python exception and is not a subclass ofjwt.exceptions.PyJWTError(PyJWTError(Exception)is the root of PyJWT's own exception hierarchy). It is therefore not caught byPyJWKSet.__init__'sexcept PyJWTError, and propagates out of the constructor, aborting thefor key in keys:loop before any subsequent key in the list is processed.PyJWKSet.from_dict/from_jsonandPyJWK.from_dict/from_jsonare exported public API (jwt/__init__.py).PyJWKClient.get_jwk_set(jwt/jwks_client.py:158) feeds a fetched JWKS HTTP response directly intoPyJWKSet.from_dictwith no per-key pre-validation, so this is reachable through the documentedPyJWKClientflow whenever the fetched JWKS contains a malformed key alongside valid ones.Proof of concept
Impact
PyJWKSet.__init__raises before completing, so no key in the JWK Set is added to the resulting set, including keys unrelated to the malformed entry. This requires the malformed key to already be present in a JWK Set the application parses (e.g. one entry in an aggregated/federated key set, or a key affected by transit corruption before signature verification of the JWKS transport itself). Applications that vet each key individually before adding it to a trusted set are not affected. The failure is an uncaughtValueError, not one of PyJWT's documentedjwt.exceptions.*types, so exception handling written against PyJWT's documented contract (except jwt.exceptions.PyJWTError) will not catch it either.Suggested remediation
Wrap the key-construction call in
PyJWK.__init__(api_jwk.py:82) so aValueErroris converted intoInvalidKeyError(aPyJWTErrorsubclass):This lets
PyJWKSet.__init__'s existingexcept PyJWTError: continueskip the one malformed key as its own comment already states is intended.Responsible Disclosure Timeline
Per the OWASP Vulnerability Disclosure Cheat Sheet and Google Project Zero's 2020 disclosure policy:
created_attimestamp when this report is submitted. If submitted on the date of this draft (2026-09-05), Day 0 = 2026-09-05.Try It Yourself (Sandbox)
Reproduce the PoC above, and attempt your own fix, in an isolated sandbox with no access to production systems, secrets, or real data.
Credit
Discovered and reported by SecDim Security Research: secdim.com, @secdim, security@secdim.com.
Maintainer update — 2026-09-08
We reproduced the reported behavior on PyJWT 2.13.0 with
cryptographyinstalled: a malformed RSA private JWK containing an invaliddvalue and no CRT parameters raised a plainValueErrorwhile parsing a JWK set. BecausePyJWKSetskipsPyJWTErrorinstances only, that exception aborted parsing before subsequent valid keys were loaded. The impact is a conditional availability failure for applications that parse a key set containing a malformed entry; it is not a signature-forgery or claims-verification bypass.The independently verified affected range is
>= 2.9.0, <= 2.13.0; earlier releases were not checked. A narrow fix has been prepared in commit8915570based on master commit5fa7594:PyJWKconverts key-constructionValueErrorexceptions toInvalidKeyError, allowing the existingPyJWKSetskip path to continue. Regression coverage verifies that a malformed RSA key is skipped while a valid key in the same set remains usable. The full suite passes with 370 tests and 4 intentional cryptography-environment skips; formatting, lint, and targeted type checks pass. The advisory remains in triage while the fix goes through release planning.Severity
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:HReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
PyJWT BOM Bypass
CVE-2026-102272 / GHSA-r6x4-923q-g947
More information
Details
Affected Package
pyjwton PyPI)Root Cause
PyJWT 2.13.0 introduced a guard in
HMACAlgorithm.prepare_key()(filejwt/algorithms.py, approximately line 344) to prevent RSA public key material from being used as an HMAC secret — the root cause of CVE-2026-48526.The guard uses
bytes.lstrip()before callingstartswith(b"{"):bytes.lstrip()with no argument removes only bytes in the ASCII whitespace set (\x20 \t \n \r \x0b \x0c). A UTF-8 BOM prefix (\xef\xbb\xbf) is not stripped, sostripped.startswith(b"{")returnsFalsefor any BOM-prefixed JWK JSON. The JWK detection block is never entered, and the RSA public key bytes are silently accepted as the HMAC-SHA256 secret.PoC Sketch (pseudocode — not a weaponized payload)
Impact
An unauthenticated network attacker who knows the target application's RSA public key — which is public by design and obtainable from a JWKS endpoint or certificate — can forge JWT tokens containing arbitrary claims and have them accepted by a PyJWT 2.13.0 verifier configured with a mixed algorithm set (
algorithms=["HS256", "RS256"]or equivalent). The resulting impactis complete authentication and authorization bypass (
C:H/I:H). Attack Complexity is High (AC:H) because the attacker must obtain the RSA public key and the verifier must use a mixed-algorithm configuration; no authentication is required (PR:N). This is a patch bypass: users who upgraded to 2.13.0 specifically to remediate CVE-2026-48526 remain vulnerable.Suggested Fix
Option A (minimal): Replace
lstrip()with an explicit strip of knownBOM prefixes before the JSON detection check:
Option B (more robust): Use
json.loads()as the detection mechanisminstead of a byte-prefix check, so encoding variants and whitespace are
handled by the JSON parser:
Option B is preferred because it is resilient to any future encoding variant.
Maintainer update — 2026-09-09
We reproduced the reported algorithm-confusion path on PyJWT 2.13.0. When an application passes a raw public RSA JWK as the key and allows both an asymmetric and HMAC algorithm, a forged HS256 token signed with the known public JWK bytes is accepted when the JWK is represented in encodings accepted by Python's JSON decoder. The normal raw-JWK, asymmetric-only, and algorithm-bound
PyJWKcontrols reject the token. This is an application configuration precondition, but the bypass is in PyJWT's own raw-JWK validation guard and is in scope under the PyJWT security policy.The fix is committed as
180783930de91876bc0d601f826a1f2956057291.HMACAlgorithm.prepare_key()now checks parsed JSON objects forktyacross accepted UTF-8/16/32 representations, preserves non-JWK HMAC key bytes, and conservatively rejects deeply nested JSON objects even when parsing reaches the recursion guard. Regression coverage includes BOM and BOM-less encodings, deep non-JWK keys, deep JWK objects, and unpaired-surrogate cases.The full suite passes with 391 tests and 4 intentional cryptography-environment skips. A fresh Astra/max independent review accepted commit
180783930de91876bc0d601f826a1f2956057291. It independently confirmed encoding/BOM handling, recursion and surrogate behavior, preservation of non-object key compatibility, and the reported test results.The fix has not been released. The advisory remains High with its existing CVSS 3.1 score of 7.4, and the patched version remains unset pending release planning.
Classification update — 2026-09-10
We completed the advisory classification review. The proposed CVSS 3.1 vector is
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:Nand the proposed CWE classification is CWE-347. These classifications reflect the documented impact and do not alter the affected range, fix status, or lifecycle state.Maintainer update — 2026-09-11
The verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.
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).
PyJWT accepts public JWK containers as HMAC secrets
CVE-2026-102273 / GHSA-w2cx-738m-mc7w
More information
Details
Summary
PyJWT 2.13.0 contains an incomplete defense against algorithm confusion when
an application mixes symmetric and asymmetric algorithms in one verification
path. A public RSA, EC, or OKP JWK can be accepted as an HMAC secret when it
is wrapped in a JWKS object, nested in an array, or represented in another
container form that does not expose a top-level
ktymember.Impact
An attacker who knows the public key material can forge HS256/HS384/HS512
tokens if the application simultaneously:
key=; andThis can allow forged JWT claims in affected application configurations. The
issue does not affect applications that keep symmetric and asymmetric
verification paths separate and follow PyJWT's algorithm-selection guidance.
Fix status
The fix is on
masterin commit801cd12(fix: reject public JWK container HMAC keys).HMACAlgorithm.prepare_keynow rejects public JWK members found inobjects, arrays, nested containers, BOM/UTF variants, and recursion-limit
inputs. It also recognizes escaped JSON member names without treating ordinary
string values as JWKs. Ordinary JSON secrets remain accepted byte-for-byte.
The change was tested with focused regression tests and the full local tox
matrix. Available Python 3.9, 3.12, and 3.13 crypto/no-crypto suites, mypy,
package metadata, and coverage passed; unavailable interpreters were skipped
by the project configuration. A fresh independent Astra/max security review
accepted the final diff with no blocking findings.
The affected range is
= 2.13.0. The fix is on the unreleased developmentbranch; the patched version will be recorded when a released 2.x version
containing the fix is available. This advisory is being moved to draft pending
that release.
Reporter credit
Credit: Charles Vosburgh / Trilobyte.
Original report
The original report and reproduction package are retained in the private
advisory record.
Maintainer update — 2026-09-11
The verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.
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).
PyJWT: PyJWK accepts empty HMAC keys, bypassing PyJWT's empty-key validation
CVE-2026-102266 / GHSA-9j54-fg26-wv3r
More information
Details
Summary
A service that verifies HS256 tokens using an empty oct JWK through PyJWK, including a PyJWK obtained from PyJWKSet, can therefore accept attacker-generated tokens as authenticated.
PyJWT 2.13.0 rejects an empty HMAC key when it is supplied through the raw
str/byteskey path, but accepts the same zero-length key when it is supplied as a symmetric `PyJWK