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
- Run
pip show joserfcin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in 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.
- Upgrade
joserfcto 1.7.5 or later, commit the lockfile and redeploy. - 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
- github.com/authlib/joserfc/security/advisories/GHSA-jpxq-7h8p-2q76
- github.com/authlib/joserfc/commit/d6dc8df6c7609277e741069d3733e43996c1cd17
- github.com/authlib/joserfc/releases/tag/1.7.5
- nvd.nist.gov/vuln/detail/CVE-2026-119283
- github.com/advisories/GHSA-jpxq-7h8p-2q76
- github.com/authlib/joserfc
Merged from ENISA EUVD, GitHub Advisory Database, OSV. Last updated 31 Oct 2026. All vulnerabilities · RSS · JSON