From 335c8086dc5e5ca5d0444780c98fa6ef42ce23bd Mon Sep 17 00:00:00 2001 From: Minh Vu <38443830+fallintoplace@users.noreply.github.com> Date: Fri, 22 May 2026 23:46:31 +0200 Subject: [PATCH] graphql: limit request body size --- graphql/graphql_test.go | 32 +++++++++++++++++++++++++++++++- graphql/service.go | 28 ++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/graphql/graphql_test.go b/graphql/graphql_test.go index ca864d5fb2..d0f80ee282 100644 --- a/graphql/graphql_test.go +++ b/graphql/graphql_test.go @@ -23,6 +23,7 @@ import ( "io" "math/big" "net/http" + "net/http/httptest" "strings" "testing" "time" @@ -132,7 +133,7 @@ func TestGraphQLBlockSerialization(t *testing.T) { code: 400, }, { - body: `{"query": "{bleh{number}}","variables": null}"`, + body: `{"query": "{bleh{number}}","variables": null}`, want: `{"errors":[{"message":"Cannot query field \"bleh\" on type \"Query\".","locations":[{"line":1,"column":2}]}]}`, code: 400, }, @@ -175,6 +176,35 @@ func TestGraphQLBlockSerialization(t *testing.T) { } } +func TestGraphQLHTTPBodyLimit(t *testing.T) { + tests := []struct { + name string + body string + }{ + { + name: "should reject oversized request body if query field exceeds limit", + body: `{"query":"` + strings.Repeat("a", maxRequestBodySize) + `"}`, + }, + { + name: "should reject oversized request body if trailing data exceeds limit", + body: `{"query":"{block{number}}"}` + strings.Repeat(" ", maxRequestBodySize), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/graphql", strings.NewReader(test.body)) + w := httptest.NewRecorder() + + handler{}.ServeHTTP(w, req) + + if w.Code != http.StatusRequestEntityTooLarge { + t.Fatalf("got status %d, want %d", w.Code, http.StatusRequestEntityTooLarge) + } + }) + } +} + func TestGraphQLBlockSerializationEIP2718(t *testing.T) { // Account for signing txes var ( diff --git a/graphql/service.go b/graphql/service.go index 9381a51da6..4d530586a3 100644 --- a/graphql/service.go +++ b/graphql/service.go @@ -19,6 +19,8 @@ package graphql import ( "context" "encoding/json" + "errors" + "io" "net/http" "strconv" "sync" @@ -35,18 +37,31 @@ import ( // maxQueryDepth limits the maximum field nesting depth allowed in GraphQL queries. const maxQueryDepth = 20 +// maxRequestBodySize limits the size of incoming GraphQL request bodies. +const maxRequestBodySize = 5 * 1024 * 1024 + type handler struct { Schema *graphql.Schema } func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize) + var params struct { Query string `json:"query"` OperationName string `json:"operationName"` Variables map[string]interface{} `json:"variables"` } - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + dec := json.NewDecoder(r.Body) + if err := dec.Decode(¶ms); err != nil { + writeRequestError(w, err) + return + } + if err := dec.Decode(&struct{}{}); err != io.EOF { + if err == nil { + err = errors.New("unexpected content after JSON value") + } + writeRequestError(w, err) return } @@ -108,6 +123,15 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { }) } +func writeRequestError(w http.ResponseWriter, err error) { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + http.Error(w, err.Error(), http.StatusRequestEntityTooLarge) + return + } + http.Error(w, err.Error(), http.StatusBadRequest) +} + // New constructs a new GraphQL service instance. func New(stack *node.Node, backend ethapi.Backend, filterSystem *filters.FilterSystem, cors, vhosts []string) error { _, err := newHandler(stack, backend, filterSystem, cors, vhosts)