# GraphQL-core has denial of service via memory leak in aborted validations

GraphQL-core re-raised a shared module-level ValidationAbortedError, so each aborted validation appended frames to its traceback, permanently leaking ~124 KiB per request until OOM.

- Payout: Research
- Severity: high
- Program: GraphQL-core
- Found by: riodw
- Disclosed: 2026-10-30
- CVE: CVE-2026-119286
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-mpqx-q82w-9x46
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-mpqx-q82w-9x46
- Tags: area:web domain:api domain:oss impact:dos kind:research platform:ghsa sev:high tech:graphql tech:python technique:source-review vuln:dos

**Target:** Python GraphQL validation (validate())
**Root cause:** validate() raised a single module-level ValidationAbortedError instance; re-raising appends frames to its existing __traceback__, retaining the document, validation context and errors of every aborted validation for process lifetime.
**Steps:**
1. Build a schema and a document with more than max_errors (100) validation errors, e.g. `{ f0 f1 ... f100 }` with 101 unknown fields.
2. Call `validate(schema, document)` repeatedly; each call aborts and re-raises the shared error.
3. Observe retained memory via tracemalloc: ~147 KiB after one call, ~25 MB after 200 calls on CPython 3.12.
4. Repeat requests until the worker exceeds its container memory limit and is OOM-killed.
**How found:** Reporter noticed the aborted-validation path raised a shared module-level exception instance and reasoned that Python appends frames to an existing __traceback__ on re-raise, then confirmed retention with tracemalloc.
**Exploitation:** Unauthenticated client sends small documents with >100 validation errors; validation runs before execution so depth/complexity limits don't help; each request permanently retains ~124 KiB, so a few thousand requests exhaust a 256-512 MB worker.
**Insight:** Module-level singleton exception instances are a leak vector in Python: any re-raise grows their __traceback__ forever. Grep for shared exception objects raised repeatedly, especially on attacker-reachable abort paths that bypass execution-stage limits.

## Proof of concept (copied exactly from the original report)

**Python PoC measuring retained memory after aborted validations**

```
import gc
import tracemalloc
from graphql import build_schema, parse, validate

schema = build_schema("type Query { a: Int }")
document = parse("{ " + " ".join(f"f{i}" for i in range(101)) + " }")

tracemalloc.start()
validate(schema, document)
gc.collect()
base = tracemalloc.get_traced_memory()[0]
for _ in range(200):
    validate(schema, document)
gc.collect()
print((tracemalloc.get_traced_memory()[0] - base) // 1024, "KiB")
```

