diff --git a/router-tests/go.mod b/router-tests/go.mod index 56e8f747a2..ed3ddeaf8b 100644 --- a/router-tests/go.mod +++ b/router-tests/go.mod @@ -23,10 +23,10 @@ require ( github.com/twmb/franz-go v1.16.1 github.com/twmb/franz-go/pkg/kadm v1.11.0 github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 - github.com/wundergraph/cosmo/demo v0.0.0-20250715133706-4c418b758ddd + github.com/wundergraph/cosmo/demo v0.0.0-20250721114211-ea47c3893316 github.com/wundergraph/cosmo/demo/pkg/subgraphs/projects v0.0.0-20250715110703-10f2e5f9c79e - github.com/wundergraph/cosmo/router v0.0.0-20250715133706-4c418b758ddd - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.207 + github.com/wundergraph/cosmo/router v0.0.0-20250721114211-ea47c3893316 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.208 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/sdk/metric v1.28.0 diff --git a/router-tests/go.sum b/router-tests/go.sum index 995b25670e..62c342ed27 100644 --- a/router-tests/go.sum +++ b/router-tests/go.sum @@ -325,8 +325,8 @@ github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTB github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= github.com/wundergraph/consul/sdk v0.0.0-20250204115147-ed842a8fd301 h1:EzfKHQoTjFDDcgaECCCR2aTePqMu9QBmPbyhqIYOhV0= github.com/wundergraph/consul/sdk v0.0.0-20250204115147-ed842a8fd301/go.mod h1:wxI0Nak5dI5RvJuzGyiEK4nZj0O9X+Aw6U0tC1wPKq0= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.207 h1:g2MpMjU/Jk30oBzfBjGRgH3EzTvwI0IV57HhlUjeyZc= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.207/go.mod h1:DaBrBCMgKGd3t7zg7z11jKm+0mVJiesr/IQCRG9qgP0= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.208 h1:dnWJ/nv+M2SF9aZnHXPU+gfSiANC7eAOSM4HU6ymc74= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.208/go.mod h1:DaBrBCMgKGd3t7zg7z11jKm+0mVJiesr/IQCRG9qgP0= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= diff --git a/router-tests/security_test.go b/router-tests/security_test.go new file mode 100644 index 0000000000..742301c21b --- /dev/null +++ b/router-tests/security_test.go @@ -0,0 +1,111 @@ +package integration + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/wundergraph/cosmo/router-tests/testenv" + "github.com/wundergraph/cosmo/router/pkg/config" +) + +func TestParserHardLimits(t *testing.T) { + t.Parallel() + + t.Run("parser approximate depth limit", func(t *testing.T) { + t.Parallel() + t.Run("blocks queries over the limit", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + ModifySecurityConfiguration: func(securityConfiguration *config.SecurityConfiguration) { + securityConfiguration.ParserLimits = config.ParserLimitsConfiguration{ + ApproximateDepthLimit: 2, + } + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res, _ := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `{ employee(id:1) { id details { forename surname } } }`, + }) + require.Equal(t, 400, res.Response.StatusCode) + require.Equal(t, `{"errors":[{"message":"allowed parsing depth per GraphQL document of '2' exceeded"}]}`, res.Body) + }) + }) + + t.Run("blocks persisted queries over the limit", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + ModifySecurityConfiguration: func(securityConfiguration *config.SecurityConfiguration) { + securityConfiguration.ParserLimits = config.ParserLimitsConfiguration{ + ApproximateDepthLimit: 2, + } + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + header := make(http.Header) + header.Add("graphql-client-name", "my-client") + res, _ := xEnv.MakeGraphQLRequestOverGET(testenv.GraphQLRequest{ + OperationName: []byte(`Find`), + Variables: []byte(`{"criteria": {"nationality": "GERMAN" }}`), + Extensions: []byte(`{"persistedQuery": {"version": 1, "sha256Hash": "e33580cf6276de9a75fb3b1c4b7580fec2a1c8facd13f3487bf6c7c3f854f7e3"}}`), + Header: header, + }) + require.Equal(t, 400, res.Response.StatusCode) + require.Equal(t, `{"errors":[{"message":"allowed parsing depth per GraphQL document of '2' exceeded"}]}`, res.Body) + }) + }) + + t.Run("default limit allows persisted queries", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{}, func(t *testing.T, xEnv *testenv.Environment) { + header := make(http.Header) + header.Add("graphql-client-name", "my-client") + res, _ := xEnv.MakeGraphQLRequestOverGET(testenv.GraphQLRequest{ + OperationName: []byte(`Find`), + Variables: []byte(`{"criteria": {"nationality": "GERMAN" }}`), + Extensions: []byte(`{"persistedQuery": {"version": 1, "sha256Hash": "e33580cf6276de9a75fb3b1c4b7580fec2a1c8facd13f3487bf6c7c3f854f7e3"}}`), + Header: header, + }) + require.Equal(t, 200, res.Response.StatusCode) + require.Equal(t, `{"data":{"findEmployees":[{"id":1,"details":{"forename":"Jens","surname":"Neuse"}},{"id":2,"details":{"forename":"Dustin","surname":"Deus"}},{"id":4,"details":{"forename":"Björn","surname":"Schwenzer"}},{"id":11,"details":{"forename":"Alexandra","surname":"Neuse"}}]}}`, res.Body) + }) + }) + }) + + t.Run("parser total fields limit", func(t *testing.T) { + t.Parallel() + + t.Run("blocks queries over the limit", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + ModifySecurityConfiguration: func(securityConfiguration *config.SecurityConfiguration) { + securityConfiguration.ParserLimits = config.ParserLimitsConfiguration{ + TotalFieldsLimit: 2, + } + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res, _ := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `{ employee(id:1) { id details { forename surname } } }`, + }) + require.Equal(t, 400, res.Response.StatusCode) + require.Equal(t, `{"errors":[{"message":"allowed number of fields per GraphQL document of '2' exceeded"}]}`, res.Body) + }) + }) + + t.Run("allows queries under the limit", func(t *testing.T) { + t.Parallel() + testenv.Run(t, &testenv.Config{ + ModifySecurityConfiguration: func(securityConfiguration *config.SecurityConfiguration) { + securityConfiguration.ParserLimits = config.ParserLimitsConfiguration{ + TotalFieldsLimit: 6, // fail if count > limit + } + }, + }, func(t *testing.T, xEnv *testenv.Environment) { + res, _ := xEnv.MakeGraphQLRequest(testenv.GraphQLRequest{ + Query: `{ employee(id:1) { id details { forename surname } } }`, // has 5 fields + }) + require.Equal(t, 200, res.Response.StatusCode) + require.Equal(t, `{"data":{"employee":{"id":1,"details":{"forename":"Jens","surname":"Neuse"}}}}`, res.Body) + }) + }) + }) +} diff --git a/router/core/cache_warmup.go b/router/core/cache_warmup.go index 2417fdd796..2689d7c5d4 100644 --- a/router/core/cache_warmup.go +++ b/router/core/cache_warmup.go @@ -305,15 +305,13 @@ func (c *CacheWarmupPlanningProcessor) ProcessOperation(ctx context.Context, ope return nil, err } + // NOTE: we do not validate query complexity here, because queries come from analytics, so they should be valid + _, err = k.Validate(true, k.parsedOperation.RemapVariables, nil) if err != nil { return nil, err } - if c.complexityLimits != nil { - _, _, _ = k.ValidateQueryComplexity(c.complexityLimits, k.kit.doc, c.routerSchema, k.parsedOperation.IsPersistedOperation) - } - planOptions := PlanOptions{ ClientInfo: item.Client, TraceOptions: resolve.TraceOptions{ diff --git a/router/core/graph_server.go b/router/core/graph_server.go index d112c75552..a490a5fdb8 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -14,8 +14,6 @@ import ( "sync" "time" - "github.com/wundergraph/cosmo/router/internal/circuit" - "github.com/cespare/xxhash/v2" "github.com/cloudflare/backoff" "github.com/dgraph-io/ristretto/v2" @@ -24,6 +22,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/klauspost/compress/gzhttp" "github.com/klauspost/compress/gzip" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" "go.opentelemetry.io/otel/attribute" otelmetric "go.opentelemetry.io/otel/metric" oteltrace "go.opentelemetry.io/otel/trace" @@ -35,6 +34,7 @@ import ( "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/common" nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" + "github.com/wundergraph/cosmo/router/internal/circuit" "github.com/wundergraph/cosmo/router/internal/expr" rjwt "github.com/wundergraph/cosmo/router/internal/jwt" rmiddleware "github.com/wundergraph/cosmo/router/internal/middleware" @@ -1190,21 +1190,26 @@ func (s *graphServer) buildGraphMux( } operationProcessor := NewOperationProcessor(OperationProcessorOptions{ - Executor: executor, - MaxOperationSizeInBytes: int64(s.routerTrafficConfig.MaxRequestBodyBytes), - PersistedOperationClient: s.persistedOperationClient, - AutomaticPersistedOperationCacheTtl: s.automaticPersistedQueriesConfig.Cache.TTL, - EnablePersistedOperationsCache: s.engineExecutionConfiguration.EnablePersistedOperationsCache, - PersistedOpsNormalizationCache: gm.persistedOperationCache, - NormalizationCache: gm.normalizationCache, - ValidationCache: gm.validationCache, - QueryDepthCache: gm.complexityCalculationCache, - OperationHashCache: gm.operationHashCache, - ParseKitPoolSize: s.engineExecutionConfiguration.ParseKitPoolSize, - IntrospectionEnabled: s.Config.introspection, + Executor: executor, + MaxOperationSizeInBytes: int64(s.routerTrafficConfig.MaxRequestBodyBytes), + PersistedOperationClient: s.persistedOperationClient, + AutomaticPersistedOperationCacheTtl: s.automaticPersistedQueriesConfig.Cache.TTL, + EnablePersistedOperationsCache: s.engineExecutionConfiguration.EnablePersistedOperationsCache, + PersistedOpsNormalizationCache: gm.persistedOperationCache, + NormalizationCache: gm.normalizationCache, + ValidationCache: gm.validationCache, + QueryDepthCache: gm.complexityCalculationCache, + OperationHashCache: gm.operationHashCache, + ParseKitPoolSize: s.engineExecutionConfiguration.ParseKitPoolSize, + IntrospectionEnabled: s.Config.introspection, + ParserTokenizerLimits: astparser.TokenizerLimits{ + MaxDepth: s.Config.securityConfiguration.ParserLimits.ApproximateDepthLimit, + MaxFields: s.Config.securityConfiguration.ParserLimits.TotalFieldsLimit, + }, ApolloCompatibilityFlags: s.apolloCompatibilityFlags, ApolloRouterCompatibilityFlags: s.apolloRouterCompatibilityFlags, DisableExposingVariablesContentOnValidationError: s.engineExecutionConfiguration.DisableExposingVariablesContentOnValidationError, + ComplexityLimits: s.securityConfiguration.ComplexityLimits, }) operationPlanner := NewOperationPlanner(executor, gm.planCache) diff --git a/router/core/graphql_prehandler.go b/router/core/graphql_prehandler.go index 06bb42c3c3..944b325d7b 100644 --- a/router/core/graphql_prehandler.go +++ b/router/core/graphql_prehandler.go @@ -897,6 +897,28 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson trace.WithSpanKind(trace.SpanKindInternal), trace.WithAttributes(requestContext.telemetry.traceAttrs...), ) + + // Validate that the planned query doesn't exceed the maximum query depth configured + // This check runs if they've configured a max query depth, and it can optionally be turned off for persisted operations + if h.complexityLimits != nil { + cacheHit, complexityCalcs, queryDepthErr := operationKit.ValidateQueryComplexity() + engineValidateSpan.SetAttributes(otel.WgQueryDepth.Int(complexityCalcs.Depth)) + engineValidateSpan.SetAttributes(otel.WgQueryTotalFields.Int(complexityCalcs.TotalFields)) + engineValidateSpan.SetAttributes(otel.WgQueryRootFields.Int(complexityCalcs.RootFields)) + engineValidateSpan.SetAttributes(otel.WgQueryRootFieldAliases.Int(complexityCalcs.RootFieldAliases)) + engineValidateSpan.SetAttributes(otel.WgQueryDepthCacheHit.Bool(cacheHit)) + if queryDepthErr != nil { + rtrace.AttachErrToSpan(engineValidateSpan, err) + + requestContext.operation.validationTime = time.Since(startValidation) + httpOperation.traceTimings.EndValidate() + + engineValidateSpan.End() + + return queryDepthErr + } + } + validationCached, err := operationKit.Validate(requestContext.operation.executionOptions.SkipLoader, requestContext.operation.remapVariables, h.apolloCompatibilityFlags) if err != nil { rtrace.AttachErrToSpan(engineValidateSpan, err) @@ -921,27 +943,6 @@ func (h *PreHandler) handleOperation(req *http.Request, variablesParser *astjson engineValidateSpan.SetAttributes(otel.WgVariablesValidationSkipped.Bool(true)) } - // Validate that the planned query doesn't exceed the maximum query depth configured - // This check runs if they've configured a max query depth, and it can optionally be turned off for persisted operations - if h.complexityLimits != nil { - cacheHit, complexityCalcs, queryDepthErr := operationKit.ValidateQueryComplexity(h.complexityLimits, operationKit.kit.doc, h.executor.RouterSchema, operationKit.parsedOperation.IsPersistedOperation) - engineValidateSpan.SetAttributes(otel.WgQueryDepth.Int(complexityCalcs.Depth)) - engineValidateSpan.SetAttributes(otel.WgQueryTotalFields.Int(complexityCalcs.TotalFields)) - engineValidateSpan.SetAttributes(otel.WgQueryRootFields.Int(complexityCalcs.RootFields)) - engineValidateSpan.SetAttributes(otel.WgQueryRootFieldAliases.Int(complexityCalcs.RootFieldAliases)) - engineValidateSpan.SetAttributes(otel.WgQueryDepthCacheHit.Bool(cacheHit)) - if queryDepthErr != nil { - rtrace.AttachErrToSpan(engineValidateSpan, err) - - requestContext.operation.validationTime = time.Since(startValidation) - httpOperation.traceTimings.EndValidate() - - engineValidateSpan.End() - - return queryDepthErr - } - } - requestContext.operation.validationTime = time.Since(startValidation) httpOperation.traceTimings.EndValidate() diff --git a/router/core/operation_processor.go b/router/core/operation_processor.go index 259b016e99..9cb939fa33 100644 --- a/router/core/operation_processor.go +++ b/router/core/operation_processor.go @@ -114,6 +114,8 @@ type OperationProcessorOptions struct { ApolloCompatibilityFlags config.ApolloCompatibilityFlags ApolloRouterCompatibilityFlags config.ApolloRouterCompatibilityFlags DisableExposingVariablesContentOnValidationError bool + ComplexityLimits *config.ComplexityLimits + ParserTokenizerLimits astparser.TokenizerLimits } // OperationProcessor provides shared resources to the parseKit and OperationKit. @@ -127,6 +129,8 @@ type OperationProcessor struct { parseKitSemaphore chan int introspectionEnabled bool parseKitOptions *parseKitOptions + complexityLimits *config.ComplexityLimits + parserTokenizerLimits astparser.TokenizerLimits } // parseKit is a helper struct to parse, normalize and validate operations @@ -522,7 +526,12 @@ func (o *OperationKit) Parse() error { report := &operationreport.Report{} o.kit.doc.Input.ResetInputString(o.parsedOperation.Request.Query) - o.kit.parser.Parse(o.kit.doc, report) + if _, err := o.kit.parser.ParseWithLimits(o.operationProcessor.parserTokenizerLimits, o.kit.doc, report); err != nil { + return &httpGraphqlError{ + message: err.Error(), + statusCode: http.StatusBadRequest, + } + } if report.HasErrors() { return &reportError{ report: report, @@ -750,7 +759,12 @@ func (o *OperationKit) setAndParseOperationDoc() error { o.kit.doc.Input.ResetInputString(o.parsedOperation.NormalizedRepresentation) o.kit.doc.Input.Variables = o.parsedOperation.Request.Variables report := &operationreport.Report{} - o.kit.parser.Parse(o.kit.doc, report) + if _, err := o.kit.parser.ParseWithLimits(o.operationProcessor.parserTokenizerLimits, o.kit.doc, report); err != nil { + return &httpGraphqlError{ + message: err.Error(), + statusCode: http.StatusBadRequest, + } + } if report.HasErrors() { return &reportError{ report: report, @@ -1076,15 +1090,19 @@ func (o *OperationKit) Validate(skipLoader bool, remapVariables map[string]strin } // ValidateQueryComplexity validates that the query complexity is within the limits set in the configuration -func (o *OperationKit) ValidateQueryComplexity(complexityLimitConfig *config.ComplexityLimits, operation, definition *ast.Document, isPersisted bool) (bool, ComplexityCacheEntry, error) { +func (o *OperationKit) ValidateQueryComplexity() (ok bool, cacheEntry ComplexityCacheEntry, err error) { + if o.operationProcessor.complexityLimits == nil { + return true, ComplexityCacheEntry{}, nil + } + if o.cache != nil && o.cache.complexityCache != nil { - if cachedComplexity, ok := o.cache.complexityCache.Get(o.parsedOperation.InternalID); ok { - return ok, cachedComplexity, o.runComplexityComparisons(complexityLimitConfig, cachedComplexity, isPersisted) + if cachedComplexity, found := o.cache.complexityCache.Get(o.parsedOperation.InternalID); found { + return true, cachedComplexity, o.runComplexityComparisons(o.operationProcessor.complexityLimits, cachedComplexity, o.parsedOperation.IsPersistedOperation) } } report := operationreport.Report{} - globalComplexityResult, rootFieldStats := operation_complexity.CalculateOperationComplexity(operation, definition, &report) + globalComplexityResult, rootFieldStats := operation_complexity.CalculateOperationComplexity(o.kit.doc, o.operationProcessor.executor.ClientSchema, &report) cacheResult := ComplexityCacheEntry{ Depth: globalComplexityResult.Depth, TotalFields: globalComplexityResult.NodeCount, @@ -1101,7 +1119,7 @@ func (o *OperationKit) ValidateQueryComplexity(complexityLimitConfig *config.Com o.cache.complexityCache.Set(o.parsedOperation.InternalID, cacheResult, 1) } - return false, cacheResult, o.runComplexityComparisons(complexityLimitConfig, cacheResult, isPersisted) + return false, cacheResult, o.runComplexityComparisons(o.operationProcessor.complexityLimits, cacheResult, o.parsedOperation.IsPersistedOperation) } func (o *OperationKit) runComplexityComparisons(complexityLimitConfig *config.ComplexityLimits, cachedComplexity ComplexityCacheEntry, isPersisted bool) error { @@ -1219,6 +1237,8 @@ func NewOperationProcessor(opts OperationProcessorOptions) *OperationProcessor { parseKits: make(map[int]*parseKit, opts.ParseKitPoolSize), parseKitSemaphore: make(chan int, opts.ParseKitPoolSize), introspectionEnabled: opts.IntrospectionEnabled, + parserTokenizerLimits: opts.ParserTokenizerLimits, + complexityLimits: opts.ComplexityLimits, parseKitOptions: &parseKitOptions{ apolloCompatibilityFlags: opts.ApolloCompatibilityFlags, apolloRouterCompatibilityFlags: opts.ApolloRouterCompatibilityFlags, diff --git a/router/core/websocket.go b/router/core/websocket.go index fd618f50c6..d1942e97c9 100644 --- a/router/core/websocket.go +++ b/router/core/websocket.go @@ -894,6 +894,12 @@ func (h *WebSocketConnectionHandler) parseAndPlan(registration *SubscriptionRegi startValidation := time.Now() + _, _, err = operationKit.ValidateQueryComplexity() + if err != nil { + opContext.validationTime = time.Since(startValidation) + return nil, nil, err + } + if _, err := operationKit.Validate(h.plannerOptions.ExecutionOptions.SkipLoader, opContext.remapVariables, &h.apolloCompatibilityFlags); err != nil { opContext.validationTime = time.Since(startValidation) return nil, nil, err diff --git a/router/go.mod b/router/go.mod index 79ef6d2a60..bd467e7e07 100644 --- a/router/go.mod +++ b/router/go.mod @@ -31,7 +31,7 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 github.com/twmb/franz-go v1.16.1 - github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.207 + github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.208 // Do not upgrade, it renames attributes we rely on go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/contrib/propagators/b3 v1.23.0 diff --git a/router/go.sum b/router/go.sum index 74707730d5..23fb84d208 100644 --- a/router/go.sum +++ b/router/go.sum @@ -290,8 +290,8 @@ github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0 github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083 h1:8/D7f8gKxTBjW+SZK4mhxTTBVpxcqeBgWF1Rfmltbfk= github.com/wundergraph/astjson v0.0.0-20250106123708-be463c97e083/go.mod h1:eOTL6acwctsN4F3b7YE+eE2t8zcJ/doLm9sZzsxxxrE= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.207 h1:g2MpMjU/Jk30oBzfBjGRgH3EzTvwI0IV57HhlUjeyZc= -github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.207/go.mod h1:DaBrBCMgKGd3t7zg7z11jKm+0mVJiesr/IQCRG9qgP0= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.208 h1:dnWJ/nv+M2SF9aZnHXPU+gfSiANC7eAOSM4HU6ymc74= +github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.208/go.mod h1:DaBrBCMgKGd3t7zg7z11jKm+0mVJiesr/IQCRG9qgP0= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index 923fcac36f..43ee17b598 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -405,6 +405,12 @@ type SecurityConfiguration struct { ComplexityCalculationCache *ComplexityCalculationCache `yaml:"complexity_calculation_cache"` ComplexityLimits *ComplexityLimits `yaml:"complexity_limits"` DepthLimit *QueryDepthConfiguration `yaml:"depth_limit"` + ParserLimits ParserLimitsConfiguration `yaml:"parser_limits"` +} + +type ParserLimitsConfiguration struct { + ApproximateDepthLimit int `yaml:"approximate_depth_limit,omitempty" envDefault:"100"` // 0 means disabled + TotalFieldsLimit int `yaml:"total_fields_limit,omitempty" envDefault:"500"` // 0 means disabled } type QueryDepthConfiguration struct { diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index 477be33e9a..debee1ed9e 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -2437,6 +2437,25 @@ "default": false } } + }, + "parser_limits": { + "type": "object", + "description": "The configuration to enforce parser limits for the query depth and fields count. This configuration leads to early fail for the queries above the limits during parsing. Do not change without need", + "additionalProperties": false, + "properties": { + "approximate_depth_limit": { + "type": "integer", + "description": "The approximate cumulative depth limit of a query, including fragments. Set to 0 to disable.", + "default": 100, + "minimum": 0 + }, + "parser_total_fields_limit": { + "type": "integer", + "description": "The total number of fields the parser will allow. Set to 0 to disable.", + "default": 500, + "minimum": 0 + } + } } } }, diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index 0e11cad37c..9c9aa94d21 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -309,7 +309,11 @@ }, "ComplexityCalculationCache": null, "ComplexityLimits": null, - "DepthLimit": null + "DepthLimit": null, + "ParserLimits": { + "ApproximateDepthLimit": 100, + "TotalFieldsLimit": 500 + } }, "EngineExecutionConfiguration": { "Debug": { diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index 5be309fc5d..177f558b5d 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -656,7 +656,11 @@ "IgnorePersistedOperations": true } }, - "DepthLimit": null + "DepthLimit": null, + "ParserLimits": { + "ApproximateDepthLimit": 100, + "TotalFieldsLimit": 500 + } }, "EngineExecutionConfiguration": { "Debug": {