diff --git a/language/parser/parser.go b/language/parser/parser.go index 4ae3dc33..6d5e076b 100644 --- a/language/parser/parser.go +++ b/language/parser/parser.go @@ -51,6 +51,30 @@ type Parser struct { Options ParseOptions PrevEnd int Token lexer.Token + depth int +} + +// maxRecursionDepth bounds how deeply the parser will descend into nested +// constructs (selection sets, input values, and list types). Documents that +// nest beyond this limit are rejected with a syntax error rather than being +// allowed to exhaust the goroutine stack. The limit is far larger than any +// realistic GraphQL document requires. +const maxRecursionDepth = 500 + +// enter records descent into a nested construct and reports an error when the +// nesting exceeds maxRecursionDepth. Every call must be paired with leave. +func (parser *Parser) enter() error { + parser.depth++ + if parser.depth > maxRecursionDepth { + return gqlerrors.NewSyntaxError(parser.Source, parser.Token.Start, + fmt.Sprintf("Syntax Error: Document nests deeper than the maximum of %d levels", maxRecursionDepth)) + } + return nil +} + +// leave records the end of a nested construct entered with enter. +func (parser *Parser) leave() { + parser.depth-- } func Parse(p ParseParams) (*ast.Document, error) { @@ -315,6 +339,10 @@ func parseVariable(parser *Parser) (*ast.Variable, error) { * SelectionSet : { Selection+ } */ func parseSelectionSet(parser *Parser) (*ast.SelectionSet, error) { + if err := parser.enter(); err != nil { + return nil, err + } + defer parser.leave() start := parser.Token.Start selections := []ast.Selection{} if iSelections, err := reverse(parser, @@ -569,6 +597,10 @@ func parseFragmentName(parser *Parser) (*ast.Name, error) { * EnumValue : Name but not `true`, `false` or `null` */ func parseValueLiteral(parser *Parser, isConst bool) (ast.Value, error) { + if err := parser.enter(); err != nil { + return nil, err + } + defer parser.leave() token := parser.Token switch token.Kind { case lexer.BRACKET_L: @@ -771,6 +803,10 @@ func parseDirective(parser *Parser) (*ast.Directive, error) { * - NonNullType */ func parseType(parser *Parser) (ttype ast.Type, err error) { + if err = parser.enter(); err != nil { + return nil, err + } + defer parser.leave() token := parser.Token // [ String! ]! switch token.Kind { diff --git a/language/parser/parser_depth_test.go b/language/parser/parser_depth_test.go new file mode 100644 index 00000000..cda3a44a --- /dev/null +++ b/language/parser/parser_depth_test.go @@ -0,0 +1,37 @@ +package parser + +import ( + "strings" + "testing" +) + +// A document that nests far deeper than any real query must be rejected with a +// syntax error rather than being allowed to exhaust the goroutine stack. +func TestParseDeeplyNestedInputDoesNotOverflowStack(t *testing.T) { + cases := map[string]string{ + "list value": "{f(a:" + strings.Repeat("[", 200000) + ")}", + "object value": "{f(a:" + strings.Repeat("{b:", 200000) + "}", + "selection set": strings.Repeat("{f", 200000), + "list type": "query($v:" + strings.Repeat("[", 200000) + "Int){f}", + } + for name, src := range cases { + t.Run(name, func(t *testing.T) { + _, err := Parse(ParseParams{Source: src}) + if err == nil { + t.Fatalf("expected a syntax error for deeply nested input, got nil") + } + if !strings.Contains(err.Error(), "nests deeper") { + t.Fatalf("expected depth-limit error, got: %v", err) + } + }) + } +} + +// Reasonably nested documents must still parse successfully. +func TestParseModeratelyNestedInputStillParses(t *testing.T) { + depth := 100 + src := "{f(a:" + strings.Repeat("[", depth) + strings.Repeat("]", depth) + ")}" + if _, err := Parse(ParseParams{Source: src}); err != nil { + t.Fatalf("moderately nested document should parse, got: %v", err) + } +}