Summary
The GraphQL endpoint enforces no depth, complexity, or cost limit on queries. graphql.parse(query) (server/graphqlQuerying.ts:453) is called without the library's available maxTokens cap.
- Sparse graphs: self-referential
@relationship chains execute to depth 2048 at flat 2–4ms; the only limit is accidental — at depth ~20000 the recursive parse / buildSelectQuery (graphqlQuerying.ts:286) overflows the stack → HTTP 500 Maximum call stack size exceeded (caught; worker survives; no OOM). The error is opaque (no depth/field context).
- Dense graphs (confirmed): on a fully-connected graph, nested-query cost grows ~
fanout^depth with the entire matched subtree serialized into one response and no server pushback — e.g. fanout=10 / depth 5 = 111,111 nodes in a single ~591ms response; walking past the data depth still costs seconds.
An attacker who can write a dense subgraph (or a naturally dense relationship domain) can force six-figure-node, multi-second single-request work with no rejection. No crash/OOM, but a real DoS lever.
Suggestion
Add a configurable max selection depth + complexity/cost bound, and pass maxTokens to graphql.parse; return a clear 400 (with depth/field context) rather than a 500 stack overflow or unbounded serialization.
Repro
integrationTests/qa-scratch/graphql-depth.test.ts (sparse + stack-overflow edge); integrationTests/qa-scratch/graphql-dense.test.ts (dense fan-out).
Found via the exploratory QA campaign (qa-explorer), scenarios QA-036 + QA-039. Harper 001bf7b9c (v5.1.0, main).
Summary
The GraphQL endpoint enforces no depth, complexity, or cost limit on queries.
graphql.parse(query)(server/graphqlQuerying.ts:453) is called without the library's availablemaxTokenscap.@relationshipchains execute to depth 2048 at flat 2–4ms; the only limit is accidental — at depth ~20000 the recursive parse /buildSelectQuery(graphqlQuerying.ts:286) overflows the stack → HTTP 500Maximum call stack size exceeded(caught; worker survives; no OOM). The error is opaque (no depth/field context).fanout^depthwith the entire matched subtree serialized into one response and no server pushback — e.g. fanout=10 / depth 5 = 111,111 nodes in a single ~591ms response; walking past the data depth still costs seconds.An attacker who can write a dense subgraph (or a naturally dense relationship domain) can force six-figure-node, multi-second single-request work with no rejection. No crash/OOM, but a real DoS lever.
Suggestion
Add a configurable max selection depth + complexity/cost bound, and pass
maxTokenstographql.parse; return a clear400(with depth/field context) rather than a500stack overflow or unbounded serialization.Repro
integrationTests/qa-scratch/graphql-depth.test.ts(sparse + stack-overflow edge);integrationTests/qa-scratch/graphql-dense.test.ts(dense fan-out).Found via the exploratory QA campaign (qa-explorer), scenarios QA-036 + QA-039. Harper
001bf7b9c(v5.1.0, main).