Update dependency pyjwt to v2.15.0 [SECURITY] - #5564
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
September 30, 2026 23:09
28a0d7f to
7c0b64d
Compare
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.0PyJWT: 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: PyJWKClient still amplifies unauthenticated JWKS fetches on unknown kid values (incomplete fix of CVE-2026-48524)
CVE-2026-101917 / GHSA-2gx3-rcp4-g85q
More information
Details
Summary
CVE-2026-48524 (GHSA-fhv5-28vv-h8m8, "PyJWKClient unbounded JWKS endpoint requests via attacker-controlled kid values (DoS)") was fixed in 2.13.0 by stopping fetch_data() from clearing the cache on a fetch error. That closed one amplification path but did not add the mitigation the advisory's title implies: there is still no rate-limit, negative-cache, or minimum-refresh-interval for unknown kids.
At HEAD, get_signing_key(kid) (jwt/jwks_client.py:185-211), on any unknown kid, calls get_signing_keys(refresh=True), and refresh=True bypasses jwk_set_cache unconditionally and forces a fresh fetch_data(). The kid is read from the unverified token header (get_signing_key_from_jwt decodes with verify_signature=False), so no valid token and no authentication is required. lru_cache does not cache the raised exception, so even the same unknown kid repeated re-fetches on every call.
Affected
pyjwt <= 2.13.0 (the latest release; the patched release for CVE-2026-48524). No fixed version yet.
Proof of concept (verified on 2.13.0, cache enabled = realistic prod config)
import threading, http.server, socketserver, json
from jwt import PyJWKClient
hits = {'n': 0}
JWKS = json.dumps({"keys":[{"kty":"oct","kid":"real","k":"AAAA"}]}).encode()
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
hits['n'] += 1
self.send_response(200); self.send_header('Content-Type','application/json'); self.end_headers()
self.wfile.write(JWKS)
def log_message(self,*a): pass
srv = socketserver.TCPServer(('127.0.0.1',0), H); port = srv.server_address[1]
threading.Thread(target=srv.serve_forever, daemon=True).start()
c = PyJWKClient(f'http://127.0.0.1/:{port}[/jwks](tg://bot_command?command=jwks).json', cache_keys=True, lifespan=3600)
for i in range(8):
try: c.get_signing_key(f'attacker-unknown-kid-{i}')
except Exception: pass
before = hits['n']
for _ in range(5):
try: c.get_signing_key('same-unknown')
except Exception: pass
print('distinct unknown kids: 8 -> fetches:', hits['n'])
print('same unknown kid x5 -> extra fetches:', hits['n'] - before)
Output:
distinct unknown kids: 8 -> fetches: 9
same unknown kid x5 -> extra fetches: 5
Each unknown kid forces a fresh JWKS fetch; a repeated identical unknown kid still re-fetches every time against an unexpired cache. No rate-limit or negative-cache.
Impact
One unauthenticated request -> one outbound JWKS HTTP fetch + full JSON parse on the victim server. An attacker floods tokens carrying junk kids, so the victim hammers its own JWKS/IdP endpoint (amplification: attacker -> victim -> IdP), exhausting victim CPU/sockets and potentially tripping the JWKS provider's rate-limit, causing an application-wide auth outage. This is the unauthenticated DoS the parent advisory is named for, still reachable after the 2.13.0 fix.
Suggested fix
Guard the forced refresh on unknown kids: negative-cache unknown kids for a short TTL, or enforce a minimum interval between forced JWKS refreshes, so a repeated or unknown kid cannot force unbounded fetches.
Note: the same-kid-repeated result (5 identical unknown kids producing 5 fetches against an unexpired cache) shows this is request amplification, not legitimate key-rotation handling, since that refresh can never succeed.
Reported by Babakizo (Securva).
Maintainer update — 2026-09-10
The maintainer confirmed the reported behavior against PyJWT 2.13.0. With JWKS caching
enabled, an unknown
kidpreviously forced an unconditional JWKS refresh,including when the same unknown value was repeated while the cached key set was
still valid. This allowed unauthenticated token headers to cause unnecessary
outbound JWKS requests and repeated parsing work.
The fix is now on
masterin commitba4853a.PyJWKClientnow applies a30-second cooldown after successful JWKS fetches before permitting another
unknown-
kidrefresh, serializes concurrent refresh decisions per client, andallows callers to configure or disable the cooldown. Cache-disabled behavior
and immediate retry after failed fetches remain unchanged.
Regression tests cover repeated unknown kids, cooldown expiry, concurrent
misses, cache-disabled operation, and invalid cooldown values. The available
full tox matrix, Ruff, and mypy checks pass. The fix will be included in the
next released 2.x version.
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: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 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: Public keys in DER form are accepted as HMAC secrets, bypassing the CVE-2022-29217 guard
CVE-2026-102271 / GHSA-p4g4-x82p-q773
More information
Details
Summary
HMACAlgorithm.prepare_keyblocks asymmetric keys from being used as HMAC secrets by searching for text markers only. It looks for-----BEGINand for anssh-prefix. The same key in DER form is binary ASN.1 and has neither marker, so it passes the check and is used as an HMAC secret.An application that verifies tokens with an RSA or EC public key, and also allows HS256 with that same key, can be given a forged token. The attacker signs it with the public key, which is public. This is the key confusion problem CVE-2022-29217 was filed for, reachable again through a different encoding.
The reach is smaller than the original CVE. The application must already be in that misconfiguration, and it must hold its public key as DER bytes rather than PEM.
Details
The guard is at
jwt/algorithms.py:331:Both helpers are text matchers. Neither one parses the key.
jwt/utils.py:126,is_pem_format, runs a regex for----[- ]BEGIN ...----.jwt/utils.py:141,is_ssh_key, checksstartswithagainst a list ofssh-andecdsa-sha2-prefixes.A DER encoded public key starts with the bytes
0x30 0x82. It matches neither, soprepare_keyreturns it unchanged and it becomes the HMAC secret.There is no DER handling anywhere in the package.
grep -rni "\bDER\b|load_der" jwt/ tests/returns nothing.This affects three encodings that are all blocked in their PEM form today:
History of this guard:
Applications that use
PyJWKorPyJWKClientare not affected.jwt/api_jws.py:395binds the headeralgto the key's own algorithm, so HS256 never reachesHMACAlgorithm.prepare_keyon that path.Suggested fix: try to parse the bytes as a key and reject if parsing works. For example
load_der_public_key,load_der_private_keyandload_der_x509_certificatein a try/except chain, next to the checks already there. A random HMAC secret will not parse as valid DER, so real secrets should not be rejected.PoC
Tested on 2.4.0, on 2.13.0, and on main at commit
7144e4534. All three behave the same. The guard has been marker based since 2.4.0, so the versions in between are very likely affected as well.No special configuration is needed. The script builds its own key.
Output:
The token is signed with plain
hmac, so this exercises the verify path only. The PEM and the DER values come from the same key object, so the encoding is the only thing that changes.We also have a regression test written in your pytest style. It uses your own
tests/keysfixtures, covers the DER certificate case too, and fails on current main. Glad to send it or open a PR.Impact
Key confusion, CWE-347, improper verification of a cryptographic signature. Same class as CVE-2022-29217.
Who is affected: applications that verify tokens with an asymmetric public key, also list an HS* algorithm pass that public key to
jwt.decodeas DER bytes.What an attacker gets: they can mint tokens with any claims they want, so they can log in as any user. They is public, and they re-encode it to DER. Nothing secret has to be stolen first.
What limits it: the application must already be in the mixed HS* and RS* misconfiguration. It must also hoEM is the more common form and is still blocked. Users of
PyJWKandPyJWKClientare not affected.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: PyJWKClient follows redirects when fetching JWKS
CVE-2026-102267 / GHSA-9v7f-9g4p-ffgj
More information
Details
Summary
PyJWT 2.13.0
PyJWKClientfollowed HTTP redirects while fetching a JWKS,without validating the redirect destination. A configured trusted endpoint
could therefore redirect the client to a different host.
Impact
When an application uses
PyJWKClientwith caller-supplied request headers andan attacker can influence the configured endpoint's response, the redirected
request could expose those headers and the redirected response could be used as
authoritative key material. This could cause JWKS trust poisoning and, in
affected mixed-configuration applications, forged JWT acceptance. The issue
requires an attacker-influenced redirect from the configured JWKS endpoint; it
is not triggered by a token
kidalone.Affected versions
PyJWT
<= 2.13.0.Fix
The issue is fixed on
masterin commit0a795b8e1f6ef08f634aa7086fc41cc6d5ce3e56.PyJWKClientnow disables automatic redirects for JWKS fetches. Regressiontests verify that a redirect is rejected without contacting its destination,
while normal fetches, headers, caching, errors, timeouts, and SSL context remain
covered.
The fix is present in the unreleased development branch. The patched version
will be recorded after a released PyJWT 2.x version containing the fix is
confirmed.
Credit
Credit: the original reporter of GHSA-9v7f-9g4p-ffgj. Additional redirect
header-leak and cache-poisoning evidence from the newer duplicate report
GHSA-43g3-98cx-x446 is preserved in the duplicate record and informed this
canonical advisory update.
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: Asymmetric-PEM detection bypass: whitespace/line-ending-mutated public keys skip the HS/asymmetric confusion guard
CVE-2026-102268 / GHSA-ffc3-869f-jxw9
More information
Details
Prerequisites (both conditions must hold; both are deployment properties, not attacker-controlled at request time):
jwt.decodeallow-list mixes an HMAC algorithm with an asymmetric one, e.g.algorithms=["ES256", "HS256"](the RFC 8725 footgun the guard exists to backstop).PyJWKpath, in a byte-form thatcryptography's loader accepts but PyJWT'sis_pem_formatregex does not recognize (marker-adjacent whitespace/indentation, CR-only line terminators, or the PEM folded to a single line). Such forms arise naturally from an indented YAML/JSON block, a single-line environment variable or JSON string, or a CR/LF round-trip through config tooling.The attacker additionally needs the public verification key, which is public by definition, and
cryptographymust be installed.The fix for CVE-2022-29217 rejects an asymmetric key handed to an HMAC algorithm, but only when
is_pem_format()recognizes the key as PEM. That recognizer accepts strictly fewer byte-forms than the loader that later parses the key, so a PEM the guard misses still loads as a valid public key and is then used as an HMAC secret. This is an incomplete-guard bypass of the CVE-2022-29217 family.Summary
A PEM public key with marker-adjacent whitespace, CR-only line terminators, or folded to a single line makes PyJWT's
is_pem_format()returnFalsewhilecryptography.load_pem_public_key()accepts the identical bytes. The asymmetric-key rejection inHMACAlgorithm.prepare_keyis skipped, the public key becomes the HMAC secret, and an attacker who knows the public key mints a validHS256token — universal forgery — whenever the verify allow-list mixes an HMAC and an asymmetric algorithm.Details
At
jwt/algorithms.py:331-335,HMACAlgorithm.prepare_keycontains the sole family-mismatch guard:If neither predicate fires,
:357returnskey_bytesunchanged — the PEM text is used directly as the HMAC secret.is_pem_format(jwt/utils.py:116-127) isbool(_PEM_RE.search(key)), where_PEM_RErequires----[- ]BEGIN (...)[- ]----\r?\n, then.+?\r?\n, then the END marker. The LF in each\r?\nis mandatory, the markers are anchored directly after a newline, and only[- ]is tolerated adjacent to them — not arbitrary whitespace. So a key with a tab/space before the END marker, with bare\rterminators, or folded onto one line is not recognized as PEM.cryptography'sload_pem_public_keyis tolerant of exactly these forms and still returns the key.Reach:
jwt/api_jws.py:386performs the allow-list check (passes whenHS256is in the list) and takes the non-PyJWKbranch toalg_obj.prepare_key(key)at:407. The mismatch guard above is the only thing standing between a mixed allow-list and using the public key as an HMAC secret.PoC
Vulnerable path:
jwt/algorithms.py:331(guard gated onis_pem_format) ->is_pem_formatreturnsFalsefor a loader-accepted PEM ->jwt/algorithms.py:357returns the public-key bytes as the HMAC secret ->HS256verification succeeds.Reproduced on PyJWT 2.13.0 (commit
7144e453) withcryptography49.0.0, using only the public API. For each of an EC (ES256) and an RSA-2048 (RS256) key: start from the correct public-key PEM, apply a mutation, confirmis_pem_formatnow returnsFalsewhilecryptographystill loads the bytes, then verify a token signedalg=HS256with the public-key text as the HMAC secret, underalgorithms=["ES256","HS256"](resp.["RS256","HS256"]).Observed output:
Each mutated form on both key types forged a token accepted as
superadmin. Controls: the unmodified PEM is correctly rejected withInvalidKeyError(the guard works and the mutation is load-bearing); a single-algorithm allow-list["ES256"]rejects the forgedHS256token withInvalidAlgorithmError(the mixed allow-list is a necessary precondition).Steps to reproduce:
-----END, convert terminators to bare\r, or join all lines into one.jwt.utils.is_pem_format(mutated) is Falseandcryptography.hazmat.primitives.serialization.load_pem_public_key(mutated)succeeds.jwt.encode({"sub":"superadmin"}, mutated, algorithm="HS256"), thenjwt.decode(token, mutated, algorithms=["ES256","HS256"])— verification succeeds.Impact
Cryptographic signature-verification bypass (CWE-347): algorithm confusion re-enabled by an incomplete asymmetric-key guard. An attacker who knows only the public verification key forges arbitrary-claim tokens that verify as authentic, subject to the two deployment preconditions above. The
PyJWKverification path binds a single algorithm and is unaffected;enforce_minimum_key_length(off by default) does not block a 2048-bit/P-256 PEM. Impact when the preconditions hold is critical (universal forgery); the compound precondition is realistic but was not observed in a specific real-world deployment, so this is rated Critical, with the deployment precondition captured in CVSS.Maintainer update — 2026-09-10
We reproduced the reported asymmetric-key guard bypass on PyJWT 2.13.0: PEM public keys with loader-accepted formatting mutations were missed by
is_pem_format, then accepted as HMAC secrets when a verification call mixed symmetric and asymmetric algorithms. Canonical PEM controls remained blocked, and a single-algorithm allow-list rejected the forged HS256 token.The fix is committed as
8b4e233a22206b34ec1186e912e75c0b2396ac07. PyJWT now scans supported PEM BEGIN/END markers in one pass, preserving matching labels and handling overlapping markers without regex backtracking. Regression coverage includes RSA loader-accepted mutations, incomplete repeated markers, later valid PEM blocks, and overlapping END/BEGIN markers. The fix does not broaden DER-key classification or change the caller's algorithm allow-list policy.Verification on the signed commit passes with 400 tests and 4 intentional cryptography-environment skips; Ruff formatting/lint and the Python 3.9 mypy tox target pass. Fresh Astra/max independent review accepted the final snapshot and confirmed O(n) scanning, bounded storage, and no blocking compatibility or security finding. The fix has not been released; the advisory remains Critical with CVSS 3.1 score 9.1 and CWE-347.
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:L/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: Non-canonical signature segments enable raw-token revocation bypass
CVE-2026-102269 / GHSA-hxm8-2xgr-2p9m
More information
Details
Summary
PyJWT 2.13.0 accepts compact JWS signature segments containing characters that
are not in the Base64URL alphabet. Appending
!!!!to a valid signature doesnot change the decoded signature bytes or authenticated claims, but it changes
the serialized token and its SHA-256 hash. An application that indexes logout
or revocation state by the raw token therefore rejects the logged-out token and
accepts its no-key alternate serialization.
This report separates two issues: permissive compact-JWS decoding is the
library behavior, while using raw serialized JWT text as a security identity is
a dangerous application composition. A
jti-indexed control blocks bothrepresentations.
Affected Version
Confirmed with PyJWT 2.13.0 on CPython 3.13. The mutation changes only the
signature segment and requires no signing key.
End-to-End Reproduction
The loopback-only lab performs:
/protectedsuccessfully.SHA256(raw_token).!!!!to only the signature segment.a different raw hash, and receives HTTP 200.
jti-indexed revocation and confirm both forms return HTTP 401.Observed statuses for raw-token revocation were
200 -> logout -> 401for thecanonical token and
200for the alternate serialization. Thejticontrolreturned
401for both replays after logout.Security Boundaries and Severity
This is not signature forgery: the attacker starts with a valid token and
cannot alter its claims. Impact requires a consumer to bind revocation, replay
state, rate limits, or cache identity to the raw compact string. Under that
composition, an authenticated user can bypass logout or token-specific
revocation without the signing key. Medium is suggested for triage because the
security impact is real but application-dependent.
Recommended Fix
[A-Za-z0-9_-].during verification.
jti, notthe raw serialized token, for revocation and replay state.
Maintainer update — 2026-09-09
We reproduced the reported behavior on the verified
mastercommit8915570: appending!!!!to a valid compact-JWS signature segment is accepted, produces the same decoded signature bytes and claims, and changes the serialized token hash. Existing JWS verification controls pass. The behavior is present in the tested release range represented by tags2.4.0,2.8.0,2.10.1,2.12.1, and2.13.0.The security impact is conditional on an application using the raw serialized token as the identity for logout, revocation, replay, rate-limit, or cache state. Under that composition, an attacker who already holds a valid token can use an alternate serialization after the canonical string is revoked. This is not signature forgery or claim alteration; applications should use a semantic identifier such as
jtifor token identity.The report is ready to be accepted as a draft for continued remediation. A narrow fix assessment recommends rejecting non-canonical Base64URL characters and padding in compact-JWS segments. No fix commit or release is claimed yet.
Maintainer update — 2026-09-09
The fix has been rebased onto the current
masterase6f48401001609a8f99e71fcaf355fb895d508a8and is ready to be pushed. Compact-JWS header, payload, and signature segments are now required to use the unpadded Base64URL alphabet and canonical re-encoding; a signature segment containing non-canonical characters such as!!!!is rejected before verification. Valid detachedb64=falsehandling remains supported.Verification on the rebased fix commit passed 372 tests with 4 intentional cryptography-environment skips; Ruff formatting/lint and
git diff --checkpassed. The compressed-payload regression fixture was updated to valid canonical compact-JWS encoding. No release containing the fix has been published yet, so the patched version remains unset pending release planning.The advisory remains medium severity and unpublished.
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:L/I:L/A:Nand the proposed CWE classification is CWE-180. The issue permits alternate non-canonical serializations of an already valid signed token to bypass applications that identify tokens by their raw serialization; it does not forge signatures or alter claims. CWE-180 captures validation/canonicalization ordering.These metadata changes 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:L/I:L/A:NReferences
This data is provided by the GitHub Advisory Database (CC-BY 4.0).
PyJWT: Uncaught RecursionError in jwt.decode() on deeply nested token header
CVE-2026-102265 / GHSA-8wjv-2p76-3863
More information
Details
Package
pyjwt (PyPI)
Affected versions
tested & verified on: 2.13.0. Every version whose
PyJWS._load()translates onlyValueErroris affectedDescription
PyJWS._looad()(jwt/api_jws.py:337) splits the compact token, base64url decodes the header segment and hands it tojson.loads()before_verify_signature()runs. The parse is wrapped inexcept ValueError as e: raise DecodeError(...). That is what makes the documented contract sufficient for untrusted input:except jwt.PyJWTError(orInvalidTokenError) aroundjwt.decode(token, key, algorithms=[...]).CPython's JSON decoder raises
RecursionError(aRuntimeErrorsubclass, not aValueError) once nesting crosses the native stack guard. That exception leaves_load(), thenjwt.decode(), and matches noPyJWTErrorhandlerr 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 anAuthorizationheader (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:
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 arraytraceback 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
RecursionErroralongsideValueErrorin_load()and raiseDecodeError, 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:
poc_pyjwt_recursion.zip
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'srun_log.txtdocuments a complete proof run.Credits
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 raisesRecursionError, which is not aPyJWTError. The same input is accepted by the publicjwt.decode()path and can escape an application's normalexcept PyJWTErrorhandling.The fix is committed as
06573692ebcdec8831c3927513b3e87c31fbbb62.PyJWS._load()now translates bothValueErrorandRecursionErrorfrom header JSON parsing intoDecodeError. A regression test covers the deeply nested-header path before signature verification. The full suite passes with 373 tests and 4 intentional cryptography-environment skips.No release containing the fix has been published yet, so the patched version remains unset pending release planning. The advisory remains medium severity and unpublished.
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