high · 7.5 Patched

joserfc: Unbounded JWE General JSON `recipients` Array Leads to CPU Exhaustion (DoS) via Repeated RSA Private-Key Decryption

CVE-2026-119283 · GHSA-jpxq-7h8p-2q76 · EUVD-2026-113392 · published 29 Oct 2026

How serious is it

  • Severity: high, CVSS score 7.5 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-119283?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • joserfc (PyPI) < 1.7.5

How to check

  1. Run pip show joserfc in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade joserfc to 1.7.5 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

### 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 applicatio…

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from ENISA EUVD, GitHub Advisory Database, OSV. Last updated 31 Oct 2026. All vulnerabilities · RSS · JSON