Skip to content

Commit eae31ca

Browse files
authored
validation: fix bug in maxDepth fragment spread logic (#492)
1 parent 9d31459 commit eae31ca

File tree

3 files changed

+113
-7
lines changed

3 files changed

+113
-7
lines changed

graphql_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4297,3 +4297,32 @@ func TestInterfaceImplementingInterface(t *testing.T) {
42974297
`,
42984298
}})
42994299
}
4300+
4301+
func TestCircularFragmentMaxDepth(t *testing.T) {
4302+
withMaxDepth := graphql.MustParseSchema(starwars.Schema, &starwars.Resolver{}, graphql.MaxDepth(2))
4303+
gqltesting.RunTests(t, []*gqltesting.Test{
4304+
{
4305+
Schema: withMaxDepth,
4306+
Query: `
4307+
query {
4308+
...X
4309+
}
4310+
4311+
fragment X on Query {
4312+
...Y
4313+
}
4314+
fragment Y on Query {
4315+
...X
4316+
}
4317+
`,
4318+
ExpectedErrors: []*gqlerrors.QueryError{{
4319+
Message: `Cannot spread fragment "X" within itself via Y.`,
4320+
Rule: "NoFragmentCycles",
4321+
Locations: []gqlerrors.Location{
4322+
{Line: 7, Column: 20},
4323+
{Line: 10, Column: 20},
4324+
},
4325+
}},
4326+
},
4327+
})
4328+
}

internal/validation/validate_max_depth_test.go

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const (
3434
id: ID!
3535
name: String!
3636
friends: [Character]
37+
enemies: [Character]
3738
appearsIn: [Episode]!
3839
}
3940
@@ -43,12 +44,15 @@ const (
4344
JEDI
4445
}
4546
46-
type Starship {}
47+
type Starship {
48+
id: ID!
49+
}
4750
4851
type Human implements Character {
4952
id: ID!
5053
name: String!
5154
friends: [Character]
55+
enemies: [Character]
5256
appearsIn: [Episode]!
5357
starships: [Starship]
5458
totalCredits: Int
@@ -58,6 +62,7 @@ const (
5862
id: ID!
5963
name: String!
6064
friends: [Character]
65+
enemies: [Character]
6166
appearsIn: [Episode]!
6267
primaryFunction: String
6368
}`
@@ -304,6 +309,64 @@ func TestMaxDepthFragmentSpreads(t *testing.T) {
304309
depth: 6,
305310
failure: true,
306311
},
312+
{
313+
name: "spreadAtDifferentDepths",
314+
query: `
315+
fragment character on Character {
316+
name # depth + 0
317+
friends { # depth + 0
318+
name # depth + 1
319+
}
320+
}
321+
322+
query laterDepthValidated {
323+
...character # depth 1 (+1)
324+
enemies { # depth 1
325+
friends { # depth 2
326+
...character # depth 2 (+1), should error!
327+
}
328+
}
329+
}
330+
`,
331+
depth: 2,
332+
failure: true,
333+
},
334+
{
335+
name: "spreadAtSameDepth",
336+
query: `
337+
fragment character on Character {
338+
name # depth + 0
339+
friends { # depth + 0
340+
name # depth + 1
341+
}
342+
}
343+
query {
344+
characters { # depth 1
345+
friends { # depth 2
346+
...character # depth 3 (+1)
347+
}
348+
enemies { # depth 2
349+
...character # depth 3 (+1)
350+
}
351+
}
352+
}
353+
`,
354+
depth: 4,
355+
},
356+
{
357+
name: "fragmentCycle",
358+
query: `
359+
fragment X on Query { ...Y }
360+
fragment Y on Query { ...Z }
361+
fragment Z on Query { ...X }
362+
363+
query {
364+
...X
365+
}
366+
`,
367+
depth: 10,
368+
failure: true,
369+
},
307370
} {
308371
tc.Run(t, s)
309372
}
@@ -431,7 +494,7 @@ func TestMaxDepthValidation(t *testing.T) {
431494

432495
opc := &opContext{context: context, ops: doc.Operations}
433496

434-
actual := validateMaxDepth(opc, op.Selections, 1)
497+
actual := validateMaxDepth(opc, op.Selections, nil, 1)
435498
if actual != tc.expected {
436499
t.Errorf("expected %t, actual %t", tc.expected, actual)
437500
}

internal/validation/validation.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func Validate(s *types.Schema, doc *types.ExecutableDefinition, variables map[st
7676

7777
// Check if max depth is exceeded, if it's set. If max depth is exceeded,
7878
// don't continue to validate the document and exit early.
79-
if validateMaxDepth(opc, op.Selections, 1) {
79+
if validateMaxDepth(opc, op.Selections, nil, 1) {
8080
return c.errs
8181
}
8282

@@ -235,13 +235,19 @@ func validateValue(c *opContext, v *types.InputValueDefinition, val interface{},
235235

236236
// validates the query doesn't go deeper than maxDepth (if set). Returns whether
237237
// or not query validated max depth to avoid excessive recursion.
238-
func validateMaxDepth(c *opContext, sels []types.Selection, depth int) bool {
238+
//
239+
// The visited map is necessary to ensure that max depth validation does not get stuck in cyclical
240+
// fragment spreads.
241+
func validateMaxDepth(c *opContext, sels []types.Selection, visited map[*types.FragmentDefinition]struct{}, depth int) bool {
239242
// maxDepth checking is turned off when maxDepth is 0
240243
if c.maxDepth == 0 {
241244
return false
242245
}
243246

244247
exceededMaxDepth := false
248+
if visited == nil {
249+
visited = map[*types.FragmentDefinition]struct{}{}
250+
}
245251

246252
for _, sel := range sels {
247253
switch sel := sel.(type) {
@@ -251,11 +257,12 @@ func validateMaxDepth(c *opContext, sels []types.Selection, depth int) bool {
251257
c.addErr(sel.Alias.Loc, "MaxDepthExceeded", "Field %q has depth %d that exceeds max depth %d", sel.Name.Name, depth, c.maxDepth)
252258
continue
253259
}
254-
exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, sel.SelectionSet, depth+1)
260+
exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, sel.SelectionSet, visited, depth+1)
261+
255262
case *types.InlineFragment:
256263
// Depth is not checked because inline fragments resolve to other fields which are checked.
257264
// Depth is not incremented because inline fragments have the same depth as neighboring fields
258-
exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, sel.Selections, depth)
265+
exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, sel.Selections, visited, depth)
259266
case *types.FragmentSpread:
260267
// Depth is not checked because fragments resolve to other fields which are checked.
261268
frag := c.doc.Fragments.Get(sel.Name.Name)
@@ -264,8 +271,15 @@ func validateMaxDepth(c *opContext, sels []types.Selection, depth int) bool {
264271
c.addErr(sel.Loc, "MaxDepthEvaluationError", "Unknown fragment %q. Unable to evaluate depth.", sel.Name.Name)
265272
continue
266273
}
274+
275+
if _, ok := visited[frag]; ok {
276+
// we've already seen this fragment, don't check depth again.
277+
continue
278+
}
279+
visited[frag] = struct{}{}
280+
267281
// Depth is not incremented because fragments have the same depth as surrounding fields
268-
exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, frag.Selections, depth)
282+
exceededMaxDepth = exceededMaxDepth || validateMaxDepth(c, frag.Selections, visited, depth)
269283
}
270284
}
271285

0 commit comments

Comments
 (0)