Skip to content

Commit 1c74f23

Browse files
grandchildricefjl
andauthored
graphql: add query depth limit to prevent DoS attacks (#32344)
## Summary This PR addresses a DoS vulnerability in the GraphQL service by implementing a maximum query depth limit. While #26026 introduced timeout handling, it didn't fully mitigate the attack vector where deeply nested queries can still consume excessive CPU and memory resources before the timeout is reached. ## Changes - Added `maxQueryDepth` constant (set to 20) to limit the maximum nesting depth of GraphQL queries - Applied the depth limit using `graphql.MaxDepth()` option when parsing the schema - Added test case `TestGraphQLMaxDepth` to verify that queries exceeding the depth limit are properly rejected ## Security Impact Without query depth limits, malicious actors could craft deeply nested queries that: - Consume excessive CPU cycles during query parsing and execution - Allocate large amounts of memory for nested result structures - Potentially cause service degradation or outages even with timeout protection This fix complements the existing timeout mechanism by preventing resource-intensive queries from being executed in the first place. ## Testing Added `TestGraphQLMaxDepth` which verifies that queries with nesting depth > 20 are rejected with a `MaxDepthExceeded` error. ## References - Original issue: #26026 - Related security best practices: https://www.howtographql.com/advanced/4-security/ --------- Co-authored-by: Felix Lange <[email protected]>
1 parent dffa1f5 commit 1c74f23

File tree

2 files changed

+38
-1
lines changed

2 files changed

+38
-1
lines changed

graphql/graphql_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,40 @@ func TestWithdrawals(t *testing.T) {
430430
}
431431
}
432432

433+
// TestGraphQLMaxDepth ensures that queries exceeding the configured maximum depth
434+
// are rejected to prevent resource exhaustion from deeply nested operations.
435+
func TestGraphQLMaxDepth(t *testing.T) {
436+
stack := createNode(t)
437+
defer stack.Close()
438+
439+
h, err := newHandler(stack, nil, nil, []string{}, []string{})
440+
if err != nil {
441+
t.Fatalf("could not create graphql service: %v", err)
442+
}
443+
444+
var b strings.Builder
445+
for i := 0; i < maxQueryDepth+1; i++ {
446+
b.WriteString("ommers{")
447+
}
448+
b.WriteString("number")
449+
for i := 0; i < maxQueryDepth+1; i++ {
450+
b.WriteString("}")
451+
}
452+
query := fmt.Sprintf("{block{%s}}", b.String())
453+
454+
res := h.Schema.Exec(context.Background(), query, "", nil)
455+
var found bool
456+
for _, err := range res.Errors {
457+
if err.Rule == "MaxDepthExceeded" {
458+
found = true
459+
break
460+
}
461+
}
462+
if !found {
463+
t.Fatalf("expected max depth exceeded error, got %v", res.Errors)
464+
}
465+
}
466+
433467
func createNode(t *testing.T) *node.Node {
434468
stack, err := node.New(&node.Config{
435469
HTTPHost: "127.0.0.1",

graphql/service.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ import (
3232
gqlErrors "github.com/graph-gophers/graphql-go/errors"
3333
)
3434

35+
// maxQueryDepth limits the maximum field nesting depth allowed in GraphQL queries.
36+
const maxQueryDepth = 20
37+
3538
type handler struct {
3639
Schema *graphql.Schema
3740
}
@@ -116,7 +119,7 @@ func New(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterS
116119
func newHandler(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) (*handler, error) {
117120
q := Resolver{backend, filterSystem}
118121

119-
s, err := graphql.ParseSchema(schema, &q)
122+
s, err := graphql.ParseSchema(schema, &q, graphql.MaxDepth(maxQueryDepth))
120123
if err != nil {
121124
return nil, err
122125
}

0 commit comments

Comments
 (0)