Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions router-tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions router-tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
111 changes: 111 additions & 0 deletions router-tests/security_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
}
6 changes: 2 additions & 4 deletions router/core/cache_warmup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
33 changes: 19 additions & 14 deletions router/core/graph_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)

Expand Down
43 changes: 22 additions & 21 deletions router/core/graphql_prehandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()

Expand Down
34 changes: 27 additions & 7 deletions router/core/operation_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions router/core/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion router/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading