python-jose 空 HMAC 密钥导致 JWT 伪造 (CVE-2026-120459)
python-jose 3.5.0 及更早版本接受空 HMAC 密钥,当应用程序的密钥为空时伪造的 JWT 会通过验证;暂无修复版本。
TL;DR
- python-jose 接受长度为零的 HMAC 密钥。当应用程序的 HS256、HS384 或 HS512 验证密钥解析为空时,攻击者可以用该空密钥计算签名,并让伪造的声明通过验证。
- 3.5.0 及之前的所有版本都受影响。最新版本仍是 2025-05-28 发布的 3.5.0,截至 2026-11-09 没有修复版本。
- CVE 记录将其评为 CVSS v4.0 9.1 严重和 CVSS v3.1 7.4 高。报告者的概念验证已在 issue #398 中公开;没有已知利用,截至 2026-11-09 不在 CISA KEV 中。
- 在您自己的代码中拒绝空密钥,或迁移到会拒绝它的库。PyJWT 2.15.1 会以
InvalidKeyError拒绝空 HMAC 密钥。
我受影响吗?
以下条件必须全部成立:
- 使用
python-jose3.5.0 或更早版本,以 HS256、HS384 或 HS512 验证 JSON Web Token。 - 验证密钥可以解析为空值:空字符串、空字节,或
k成员为空的octJWK。 - 应用程序信任攻击者可以提供的令牌中的声明。
固定且非空的密钥不会被利用。该缺陷在密钥于运行时组装时才有影响。示例包括默认值为空的环境变量、返回空 k 的 JWKS 端点,或从未填充的数据库列。
报告者的 issue 还指出,jwt.decode() 默认将 algorithms 设为 None,从而跳过算法允许列表。这是另一个弱点,不是空密钥缺陷,但应用程序可以将两者结合,让攻击者选择算法。
| 库 | 受影响版本 | 修复版本 |
|---|---|---|
| python-jose | 0 至 3.5.0 | 截至 2026-11-09 无 |
检查已安装的版本:
pip show python-jose
pip freeze | grep -i jose
grep -rn "python-jose" requirements*.txt pyproject.toml Pipfile.lock poetry.lock 2>/dev/null
然后找出每个验证调用及其密钥来源:
grep -rn "jwt.decode\|jws.verify" --include="*.py" .
如何修复?
没有 python-jose 的修复版本。实际修复是停止向库传递空密钥材料,并迁移到会拒绝它的维护中库。
- 在每个验证位置添加检查,在
jwt.decode()运行之前拒绝空字符串、空字节或k为空的octJWK:
# Reject empty HMAC key material before python-jose sees it.
from jose.exceptions import JWKError
def require_nonempty_hmac_key(key):
if isinstance(key, (str, bytes)) and len(key) == 0:
raise JWKError("HMAC key must not be empty")
if isinstance(key, dict) and key.get("kty") == "oct":
if not key.get("k"):
raise JWKError("oct JWK k must not be empty")
return key
- 在每次调用中传入明确的算法允许列表,即使这不是空密钥缺陷本身:
claims = jwt.decode(token, require_nonempty_hmac_key(key), algorithms=["HS256"])
- 迁移到 PyJWT,它在准备密钥时拒绝空密钥。检查位于
HMACAlgorithm.prepare_key,并抛出InvalidKeyError("HMAC key must not be empty.")。
import jwt as pyjwt
claims = pyjwt.decode(token, key, algorithms=["HS256"])
- 如果密钥可能来自 JWKS 文档,请在获取时验证文档,并拒绝任何
k为空的oct密钥。不要等到验证调用失败。
即使在迁移之后,验证 JWKS 文档也值得做。返回 {"kty":"oct","k":""} 的 JWKS 端点本身就是配置错误,任何将其接受为密钥材料的验证器都会失败开放。当对称密钥的 k 为空时,拒绝该文档并发出警报。
如果无法立即更改代码,请将受影响的验证密钥更换为强随机值。只有在每个部署都收到新值时才能关闭暴露;在配置仍解析为空的任何地方,空密钥仍然可被利用。
根本原因是什么?
python-jose 验证 HMAC 密钥的类型,却从不验证长度,而 JWK 路径会把空的 k 成员转换为空字节。HMAC 构造随后使用该空密钥,任何人都能计算其签名。
在 3.5.0 的 jose/backends/native.py 中,HMACKey.__init__ 接受任何字符串或字节值:
def __init__(self, key, algorithm):
if algorithm not in ALGORITHMS.HMAC:
raise JWKError("hash_alg: %s is not a valid hash algorithm" % algorithm)
self._algorithm = algorithm
self._hash_alg = self.HASHES.get(algorithm)
if isinstance(key, dict):
self.prepared_key = self._process_jwk(key)
return
if not isinstance(key, str) and not isinstance(key, bytes):
raise JWKError("Expecting a string- or bytes-formatted key.")
if isinstance(key, str):
key = key.encode("utf-8")
if is_pem_format(key) or is_ssh_key(key):
raise JWKError(
"The specified key is an asymmetric key or x509 certificate and"
" should not be used as an HMAC secret."
)
self.prepared_key = key
没有 len(key) == 0 检查。_process_jwk 用 base64url_decode 解码 k 成员,而该函数对空输入返回空字节:
def _process_jwk(self, jwk_dict):
if not jwk_dict.get("kty") == "oct":
raise JWKError("Incorrect key type. Expected: 'oct', Received: %s" % jwk_dict.get("kty"))
k = jwk_dict.get("k")
k = k.encode("utf-8")
k = bytes(k)
k = base64url_decode(k)
return k
jose/backends/cryptography_backend.py 中的 CryptographyHMACKey 重复相同的构造逻辑,也有同样的缺口。两个后端随后计算 hmac.new(self.prepared_key, msg, self._hash_alg),并用 hmac.compare_digest 或 h.verify 比较结果。使用空密钥时,该计算是确定且公开的,因此攻击者生成的签名也能通过比较。
JWK 路径使该缺陷可以从配置触发,而不是来自明显的空字符串。jws._get_keys() 将带 kty 的映射视为单个 JWK,jwk.construct() 为 HS 算法返回 HMAC 密钥类,空的 k 在验证前变为 b""。
jws._verify_signature() 从令牌头读取 alg,为每个候选构建密钥并调用 key.verify()。使用空密钥时,该调用对攻击者的签名返回真,JWT 验证的其余部分继续处理攻击者选择的声明。
如何被利用?
攻击者需要一个令牌端点或任何携带 JWT 的请求,并需要知道或推测部署的密钥为空:
- 攻击者找到一个使用 python-jose 验证 HS256 令牌且密钥解析为空的应用程序,例如
JWT_SECRET未设置而代码传入os.environ.get("JWT_SECRET", "")。 - 攻击者构造一个带有应用程序所信任声明的令牌,例如
{"sub": "admin", "role": "admin"},并用空 HMAC 密钥签名。 - 攻击者发送该令牌。python-jose 构造
HMACKey(b"", "HS256"),计算相同的签名并接受该令牌。 - 应用程序根据伪造的声明采取行动,因为它看不到密钥问题。
报告者的概念验证已在 issue #398 中公开,并按原样运行:
import jose.jwt as jwt
# This succeeds and should raise an error.
token = jwt.encode({"admin": True}, "", algorithm="HS256")
jwt.decode(token, "", algorithms=["HS256"]) # Returns {"admin": True}
JWK 形式同样有效,这是由 JWKS 驱动的部署所使用的形式:
jwt.decode(token, {"kty": "oct", "k": ""}, algorithms=["HS256"])
攻击如何展开?
让 HMAC 验证密钥解析为空的应用程序将长度为零的密钥交给 python-jose,而 python-jose 用它进行签名和验证,而不是拒绝该配置。
flowchart LR
A["应用密钥解析为空"] --> B["HMACKey 接受长度为零的密钥"]
B --> C["攻击者用空密钥计算 HMAC"]
C --> D["jwt.decode 比较签名"]
D --> E["伪造声明被接受"]
攻击者能做什么?
伪造令牌被接受后,攻击者就获得了应用程序所信任的每项声明。这可能包括:
- 当应用程序根据声明授权时,任何用户身份,包括管理员。
- 应用程序用于界定数据范围的任何租户或组织标识符。
- 延长的过期时间,因为攻击者选择
exp声明。 - 应用程序从令牌读取的任何权限或角色。
攻击者无法恢复真实的签名密钥、解密其他令牌,也无法签署配置正确的部署会接受的令牌。影响取决于应用程序如何处理声明,以及是否存在空密钥条件。
服务到服务的令牌也很重要。为内部 API 伪造的令牌可以跨越从不接触用户的信任边界。后台服务中的空密钥因此可能比登录端点暴露更多。
任何携带 bearer 令牌的请求都是候选,包括 CI 任务和内部服务调用。具有强非空密钥和明确算法允许列表的部署不会通过此缺陷被利用。
为什么修复有效?
长度为零的 HMAC 密钥没有秘密:任何人都能计算相同的 MAC,因此收到它的验证器无法认证任何东西。修复方法是在应用程序侧进行长度检查,或迁移到执行该检查的库。
修复版本应添加的检查很小。在两个后端中,紧接类型检查之后:
if not isinstance(key, str) and not isinstance(key, bytes):
raise JWKError("Expecting a string- or bytes-formatted key.")
+ if len(key) == 0:
+ raise JWKError("HMAC key must not be empty.")
+
if isinstance(key, str):
key = key.encode("utf-8")
JWK 路径需要在 base64url_decode 之后单独检查,因为空的 k 永远不会到达字符串分支。
PyJWT 执行相同的长度检查,并于 2026-11-09 通过 HS256 编码和解码在本地验证。这使迁移成为该缺陷的修复,而不仅仅是更换库。
python-jose 的 issue 在同一报告中还列出另外四项发现,包括 DER 密钥算法混淆和 algorithms=None 默认值。迁移修复了空密钥缺陷,并避免在其他问题上依赖一个缺乏维护的项目。
如何检查是否已遭入侵?
伪造令牌看起来像有效令牌,因此签名检查无法告诉您它被伪造了。先查找配置条件,然后查找真实用户未执行的账户活动。
- 确认是否有部署可能传入空密钥。检查配置默认值和密钥加载代码,并查询您使用的 JWKS 端点。
- 解码服务在暴露窗口期间接受的令牌,查找与账户不匹配的声明,例如意外的
sub、角色或租户。载荷无需密钥即可读取。
# Show a token's header and claims without verifying it.
python - <<'PY'
import base64, json, sys
token = sys.stdin.read().strip()
for part in token.split(".")[:2]:
part += "=" * (-len(part) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(part)), indent=2))
PY
- 在认证日志中查找该账户异常
iat、exp或sub的令牌。然后查找无法与真实登录关联的会话所执行的管理操作。 - 将验证密钥更换为强随机值并使现有会话失效。如果密钥为空,该期间签署的每个令牌都必须视为不可信。
- 迁移到 PyJWT 或添加检查,然后重新测试空密钥会被拒绝。
失败的伪造尝试除了请求本身外可能不留痕迹,因为被拒绝的签名和伪造的签名在比较之前走相同的代码路径。
时间线
- 2025-05-28:python-jose 3.5.0 发布,是截至 2026-11-09 的最新版本。
- 2026-03-08:报告者在 issue #398 中公开空密钥发现和概念验证。
- 2026-11-02:VulnCheck 发布 CVE-2026-120459。
常见问题
如果我的 HMAC 密钥是强随机值,应用程序会受影响吗?
不会。该缺陷要求验证密钥解析为空值。强非空密钥使攻击者无法计算伪造签名,即使在受影响的 python-jose 版本上也是如此。
传入 algorithms=["HS256"] 能修复空密钥缺陷吗?
不能。允许列表限制验证器接受的算法,但不检查密钥长度。当应用程序传入空密钥并允许 HS256 时,使用 HS256 和空密钥签名的令牌仍会通过验证。
有修复的 python-jose 版本吗?
没有。最新版本仍是 2025-05-28 的 3.5.0,CVE 记录没有指明修复版本。使用应用程序侧检查或迁移到维护中的库。
PyJWT 有同样的问题吗?
没有。PyJWT 2.15.1 会以 InvalidKeyError("HMAC key must not be empty.") 拒绝空 HMAC 密钥。2026-11-09 已通过 HS256 编码和解码在本地验证。
更换密钥有帮助吗?
有,前提是每个部署都收到新值。更换为强随机密钥会消除伪造所依赖的空密钥条件,也会使使用旧密钥签署的令牌失效。在配置仍解析为空的任何地方,空密钥仍然可被利用,因此请将更换与检查结合使用。
攻击者不知道密钥为空也能利用吗?
攻击者需要空密钥条件成立。有些部署使其可以被推测,例如未设置且默认值为空的环境变量,或 k: "" 的 JWKS 条目。一旦条件成立,攻击者在本地计算签名,无需其他访问。
使用 Ostorlab 查找使用 python-jose 的服务
Ostorlab 的软件成分分析和 SBOM 清单会列出服务解析的 Python 软件包和版本。这样可以找出仍依赖 python-jose 的应用程序,并让它们经过空密钥检查或迁移。
参考文献
- VulnCheck 公告,"python-jose through 3.5.0 Authentication Bypass via Empty HMAC Key"(2026-11-02):https://www.vulncheck.com/advisories/python-jose-through-3.5.0-authentication-bypass-via-empty-hmac-key(2026 年 11 月 9 日检查)
- CVE 记录 CVE-2026-120459(2026-11-02):https://www.cve.org/CVERecord?id=CVE-2026-120459(2026 年 11 月 9 日检查)
- python-jose issue #398,"Security: DER key algorithm confusion, empty HMAC key, and timing side-channels"(2026-03-08):https://github.com/mpdavis/python-jose/issues/398(2026 年 11 月 9 日检查)
- python-jose 3.5.0,
jose/backends/native.py(2025-05-28):https://github.com/mpdavis/python-jose/blob/3.5.0/jose/backends/native.py(2026 年 11 月 9 日检查) - python-jose 3.5.0,
jose/backends/cryptography_backend.py(2025-05-28):https://github.com/mpdavis/python-jose/blob/3.5.0/jose/backends/cryptography_backend.py(2026 年 11 月 9 日检查) - PyJWT 2.15.1,
HMACAlgorithm.prepare_key(2026 年 11 月 9 日检查):https://github.com/jpadilla/pyjwt/blob/2.15.1/jwt/algorithms.py - python-jose 的 PyPI 页面,发布历史(2026 年 11 月 9 日检查):https://pypi.org/project/python-jose/
- pypistats,python-jose 每周下载量(2026 年 11 月 9 日检查):https://pypistats.org/packages/python-jose