@@ -26,14 +26,17 @@ type fieldInfo struct {
26
26
}
27
27
28
28
type context struct {
29
- schema * ast.Schema
30
- doc * ast.ExecutableDefinition
31
- errs []* errors.QueryError
32
- opErrs map [* ast.OperationDefinition ][]* errors.QueryError
33
- usedVars map [* ast.OperationDefinition ]varSet
34
- fieldMap map [* ast.Field ]fieldInfo
35
- overlapValidated map [selectionPair ]struct {}
36
- maxDepth int
29
+ schema * ast.Schema
30
+ doc * ast.ExecutableDefinition
31
+ errs []* errors.QueryError
32
+ opErrs map [* ast.OperationDefinition ][]* errors.QueryError
33
+ usedVars map [* ast.OperationDefinition ]varSet
34
+ fieldMap map [* ast.Field ]fieldInfo
35
+ overlapValidated map [selectionPair ]struct {}
36
+ maxDepth int
37
+ overlapPairLimit int
38
+ overlapPairsObserved int
39
+ overlapLimitHit bool
37
40
}
38
41
39
42
func (c * context ) addErr (loc errors.Location , rule string , format string , a ... interface {}) {
@@ -53,7 +56,7 @@ type opContext struct {
53
56
ops []* ast.OperationDefinition
54
57
}
55
58
56
- func newContext (s * ast.Schema , doc * ast.ExecutableDefinition , maxDepth int ) * context {
59
+ func newContext (s * ast.Schema , doc * ast.ExecutableDefinition , maxDepth int , overlapPairLimit int ) * context {
57
60
return & context {
58
61
schema : s ,
59
62
doc : doc ,
@@ -62,11 +65,12 @@ func newContext(s *ast.Schema, doc *ast.ExecutableDefinition, maxDepth int) *con
62
65
fieldMap : make (map [* ast.Field ]fieldInfo ),
63
66
overlapValidated : make (map [selectionPair ]struct {}),
64
67
maxDepth : maxDepth ,
68
+ overlapPairLimit : overlapPairLimit ,
65
69
}
66
70
}
67
71
68
- func Validate (s * ast.Schema , doc * ast.ExecutableDefinition , variables map [string ]interface {}, maxDepth int ) []* errors.QueryError {
69
- c := newContext (s , doc , maxDepth )
72
+ func Validate (s * ast.Schema , doc * ast.ExecutableDefinition , variables map [string ]interface {}, maxDepth int , overlapPairLimit int ) []* errors.QueryError {
73
+ c := newContext (s , doc , maxDepth , overlapPairLimit )
70
74
71
75
opNames := make (nameSet , len (doc .Operations ))
72
76
fragUsedBy := make (map [* ast.FragmentDefinition ][]* ast.OperationDefinition )
@@ -303,13 +307,76 @@ func validateMaxDepth(c *opContext, sels []ast.Selection, visited map[*ast.Fragm
303
307
}
304
308
305
309
func validateSelectionSet (c * opContext , sels []ast.Selection , t ast.NamedType ) {
310
+ if len (sels ) == 0 {
311
+ return
312
+ }
313
+
314
+ // First pass: validate each selection and bucket fields by response name (alias or name).
315
+ fieldGroups := make (map [string ][]ast.Selection )
316
+ var fragments []ast.Selection // fragment spreads & inline fragments
306
317
for _ , sel := range sels {
318
+ if c .overlapLimitHit {
319
+ return
320
+ }
307
321
validateSelection (c , sel , t )
322
+ switch s := sel .(type ) {
323
+ case * ast.Field :
324
+ name := s .Alias .Name
325
+ if name == "" {
326
+ name = s .Name .Name
327
+ }
328
+ fieldGroups [name ] = append (fieldGroups [name ], sel )
329
+ default :
330
+ fragments = append (fragments , sel )
331
+ }
308
332
}
309
333
310
- for i , a := range sels {
311
- for _ , b := range sels [i + 1 :] {
312
- c .validateOverlap (a , b , nil , nil )
334
+ // Compare fields only within same response name group (was O(n^2) across all fields previously).
335
+ for _ , group := range fieldGroups {
336
+ if c .overlapLimitHit {
337
+ break
338
+ }
339
+ if len (group ) < 2 {
340
+ continue
341
+ }
342
+ for i , a := range group {
343
+ if c .overlapLimitHit {
344
+ break
345
+ }
346
+ for _ , b := range group [i + 1 :] {
347
+ if c .overlapLimitHit {
348
+ break
349
+ }
350
+ c .validateOverlap (a , b , nil , nil )
351
+ }
352
+ }
353
+ }
354
+
355
+ // Fragments can introduce any field names, so we must compare them with all fields and each other.
356
+ if len (fragments ) > 0 && ! c .overlapLimitHit {
357
+ // Flatten fields for fragment comparison.
358
+ var allFields []ast.Selection
359
+ for _ , group := range fieldGroups {
360
+ allFields = append (allFields , group ... )
361
+ }
362
+ for i , fa := range fragments {
363
+ if c .overlapLimitHit {
364
+ break
365
+ }
366
+ // Compare fragment with all fields
367
+ for _ , fld := range allFields {
368
+ if c .overlapLimitHit {
369
+ break
370
+ }
371
+ c .validateOverlap (fa , fld , nil , nil )
372
+ }
373
+ // Compare fragment with following fragments
374
+ for _ , fb := range fragments [i + 1 :] {
375
+ if c .overlapLimitHit {
376
+ break
377
+ }
378
+ c .validateOverlap (fa , fb , nil , nil )
379
+ }
313
380
}
314
381
}
315
382
}
@@ -523,11 +590,38 @@ func (c *context) validateOverlap(a, b ast.Selection, reasons *[]string, locs *[
523
590
return
524
591
}
525
592
526
- if _ , ok := c .overlapValidated [selectionPair {a , b }]; ok {
593
+ // Optimisation 1: store only one direction of the pair to halve memory and lookups.
594
+ pa := reflect .ValueOf (a ).Pointer ()
595
+ pb := reflect .ValueOf (b ).Pointer ()
596
+ if pb < pa { // canonical ordering
597
+ a , b = b , a
598
+ }
599
+ key := selectionPair {a : a , b : b }
600
+ if _ , ok := c .overlapValidated [key ]; ok {
527
601
return
528
602
}
529
- c .overlapValidated [selectionPair {a , b }] = struct {}{}
530
- c .overlapValidated [selectionPair {b , a }] = struct {}{}
603
+ c .overlapValidated [key ] = struct {}{}
604
+
605
+ if c .overlapPairLimit > 0 && ! c .overlapLimitHit {
606
+ c .overlapPairsObserved ++
607
+ if c .overlapPairsObserved > c .overlapPairLimit {
608
+ c .overlapLimitHit = true
609
+ // determine a representative location for error reporting
610
+ var loc errors.Location
611
+ switch sel := a .(type ) {
612
+ case * ast.Field :
613
+ loc = sel .Alias .Loc
614
+ case * ast.InlineFragment :
615
+ loc = sel .Loc
616
+ case * ast.FragmentSpread :
617
+ loc = sel .Loc
618
+ default :
619
+ // leave zero value
620
+ }
621
+ c .addErr (loc , "OverlapValidationLimitExceeded" , "Overlapping field validation aborted after examining %d pairs (limit %d). Consider restructuring the query or increasing the limit." , c .overlapPairsObserved - 1 , c .overlapPairLimit )
622
+ return
623
+ }
624
+ }
531
625
532
626
switch a := a .(type ) {
533
627
case * ast.Field :
@@ -608,11 +702,54 @@ func (c *context) validateFieldOverlap(a, b *ast.Field) ([]string, []errors.Loca
608
702
609
703
var reasons []string
610
704
var locs []errors.Location
705
+
706
+ // Fast-path: if either side has no subselections we are done.
707
+ if len (a .SelectionSet ) == 0 || len (b .SelectionSet ) == 0 {
708
+ return nil , nil
709
+ }
710
+
711
+ // Optimisation 2: avoid O(m*n) cartesian product for large sibling lists with mostly
712
+ // distinct response names (common & exploitable for DoS). Instead, index B's field
713
+ // selections by response name (alias/name). For each field in A we only compare
714
+ // against fields in B with the same response name plus all fragment spreads / inline
715
+ // fragments (which can expand to any field names and must be compared exhaustively).
716
+ bFieldIndex := make (map [string ][]ast.Selection , len (b .SelectionSet ))
717
+ var bNonField []ast.Selection
718
+ for _ , bs := range b .SelectionSet {
719
+ if f , ok := bs .(* ast.Field ); ok {
720
+ name := f .Alias .Name
721
+ if name == "" { // alias may be empty, fall back to field name
722
+ name = f .Name .Name
723
+ }
724
+ bFieldIndex [name ] = append (bFieldIndex [name ], bs )
725
+ continue
726
+ }
727
+ bNonField = append (bNonField , bs )
728
+ }
729
+
611
730
for _ , a2 := range a .SelectionSet {
731
+ if af , ok := a2 .(* ast.Field ); ok {
732
+ name := af .Alias .Name
733
+ if name == "" {
734
+ name = af .Name .Name
735
+ }
736
+ // Compare only against same-name fields + all non-field selections.
737
+ if matches := bFieldIndex [name ]; len (matches ) != 0 {
738
+ for _ , bMatch := range matches {
739
+ c .validateOverlap (a2 , bMatch , & reasons , & locs )
740
+ }
741
+ }
742
+ for _ , bnf := range bNonField {
743
+ c .validateOverlap (a2 , bnf , & reasons , & locs )
744
+ }
745
+ continue
746
+ }
747
+ // For fragments / inline fragments we still need to compare against every selection in B.
612
748
for _ , b2 := range b .SelectionSet {
613
749
c .validateOverlap (a2 , b2 , & reasons , & locs )
614
750
}
615
751
}
752
+
616
753
return reasons , locs
617
754
}
618
755
0 commit comments