CVE-2026-119286: GraphQL-core has denial of service via memory leak in aborted validations

Published Oct 29, 2026
·
Updated

Summary

When a validation was aborted because the document contained more errors than allowed by maxerrors (100 by default), validate() raised a single module-level ValidationAbortedError instance. Python appends the frames of every raise to the existing traceback of an exception that is raised again, so every aborted validation extended the traceback of this shared error. That traceback kept the frames, and with them the document, the validation context and all errors of every aborted validation, alive for the lifetime of the process.

Details

Any client can trigger an aborted validation by sending a small document with more than 100 validation errors, e.g. 101 unknown fields. No authentication is required, and since validation runs before execution, depth limiters and complexity analyzers do not prevent it. Each such request permanently retained about 124 KiB on CPython 3.12, without an upper limit, so memory grew with every request until the process was restarted or killed.

GraphQL.js uses a shared error object at the same place, but JavaScript errors capture their stack trace only once when they are created, so it is not affected. GraphQL-core 3.2 creates a new error for each aborted validation and is not affected either.

Proof of concept

python import gc import tracemalloc from graphql import buildschema, parse, validate

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

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

With GraphQL-core 3.3.0 on CPython 3.12, the retained memory grows from 147 KiB after one call to about 25 MB after 200 calls. With the fix, it stays constant at about 180 KiB.

Impact

Denial of service through memory exhaustion. A few thousand small requests are enough to push a worker past a typical 256 MB or 512 MB container limit and trigger an OOM kill. No data is disclosed or modified.

Patches

Fixed in 3.3.1.

validate() now creates a new ValidationAbortedError for every aborted validation and removes its traceback and exception context before returning it, so that the returned errors do not keep the frames of the validation alive. The module-level validationabortederror instance, which was not exported, has been removed.

Workarounds

If you cannot upgrade, clear the traceback of the shared error after each validation:

python from graphql.validation import validate as validatedocument from graphql.validation.validate import validationabortederror

def validate(args, kwargs): try: return validatedocument(args, kwargs) finally: validationabortederror.traceback = None

Pass this function instead of the original validate where your framework allows it.

Credits

Reported by @riodw. @Fire162 submitted a first fix in #288.

Other sources

GraphQL-core is a Python port of the GraphQL.js reference implementation. From 3.3.0a3 until 3.3.1, validate() reuses a module-level ValidationAbortedError when a document exceeds maxerrors. Python appends frames to the reused exception's traceback on every raise, retaining each document, validation context, and error set for the process lifetime. An unauthenticated client can repeatedly submit a small document with more than 100 validation errors to cause unbounded memory growth before query execution, eventually exhausting worker memory without disclosing or modifying data. This issue is fixed in version 3.3.1.

— OSV

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/graphql-core to a version that resolves this vulnerability.

    Fixed in 3.3.1
  2. Upgrade

    Upgrade graphql-core to a version that resolves this vulnerability.

    Fixed in 3.3.1
  3. Compensating control

    If you cannot upgrade, pass a wrapper function instead of the original validate function where the framework allows it, and clear validation_aborted_error.__traceback__ = None after each validation.

Frequently Asked Questions

1

Who can trigger the issue?

Any client able to submit GraphQL documents can trigger it. Authentication is not required.

2

Does the default validation configuration allow exploitation?

Yes. The default max_errors limit is 100, and a document containing more than 100 validation errors, such as 101 unknown fields, causes validation to abort.

3

Do depth limits or query-complexity controls mitigate this issue?

No. Validation occurs before execution, so depth limiters and complexity analyzers do not prevent the aborted-validation path from being reached.

4

How might an operator recognize active exploitation?

Process memory will grow after repeated aborted validations and will not be released for the lifetime of the process. On CPython 3.12, each triggering request retained approximately 124 KiB, until the process was restarted or killed.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203