diff --git a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs index 7f8db92b2a1..82f5e1eabe7 100644 --- a/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Types/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs @@ -84,7 +84,8 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services noLocations: !options.IncludeLocations, maxAllowedNodes: options.MaxAllowedNodes, maxAllowedTokens: options.MaxAllowedTokens, - maxAllowedFields: options.MaxAllowedFields); + maxAllowedFields: options.MaxAllowedFields, + maxAllowedRecursionDepth: options.MaxAllowedRecursionDepth); }); return services; diff --git a/src/HotChocolate/Core/src/Types/Execution/Options/RequestParserOptions.cs b/src/HotChocolate/Core/src/Types/Execution/Options/RequestParserOptions.cs index bdaf9e0aaa0..32c19fc628c 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Options/RequestParserOptions.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Options/RequestParserOptions.cs @@ -8,46 +8,59 @@ namespace HotChocolate.Execution.Options; public sealed class RequestParserOptions { /// + /// /// Specifies if locations shall be preserved in syntax nodes so that errors can /// later refer to locations of the original source text. /// These location objects will take up extra memory. - /// - /// Default: true + /// + /// Default: true /// public bool IncludeLocations { get; set; } = true; /// + /// /// Parser CPU and memory usage is linear to the number of nodes in a document /// however in extreme cases it becomes quadratic due to memory exhaustion. /// Parsing happens before validation so even invalid queries can burn lots of /// CPU time and memory. - /// - /// To prevent this you can set a maximum number of nodes allowed within a document. - /// - /// This limitation effects the . + /// + /// To prevent this you can set a maximum number of nodes allowed within a document. + /// This limitation affects the . /// public int MaxAllowedNodes { get; set; } = int.MaxValue; /// + /// /// Parser CPU and memory usage is linear to the number of tokens in a document /// however in extreme cases it becomes quadratic due to memory exhaustion. /// Parsing happens before validation so even invalid queries can burn lots of /// CPU time and memory. - /// - /// To prevent this you can set a maximum number of tokens allowed within a document. - /// - /// This limitation effects the . + /// + /// To prevent this you can set a maximum number of tokens allowed within a document. + /// This limitation affects the . /// public int MaxAllowedTokens { get; set; } = int.MaxValue; /// + /// /// Parser CPU and memory usage is linear to the number of nodes in a document /// however in extreme cases it becomes quadratic due to memory exhaustion. /// Parsing happens before validation so even invalid queries can burn lots of /// CPU time and memory. - /// + /// + /// /// To prevent this you can set a maximum number of fields allowed within a document /// as fields is an easier way to estimate query size for GraphQL requests. + /// /// public int MaxAllowedFields { get; set; } = 2048; + + /// + /// + /// The maximum allowed recursion depth when parsing a document. + /// This prevents stack overflow from deeply nested queries. + /// + /// Default: 200 + /// + public int MaxAllowedRecursionDepth { get; set; } = 200; } diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityTestHelper.cs b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityTestHelper.cs index 97bd9804ec2..b6fe1001af5 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityTestHelper.cs +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/ActivityTestHelper.cs @@ -119,8 +119,7 @@ private static void SerializeActivity(Activity activity) var scrubbedStackTrace = StackTracePathRegex().Replace(stackTrace, match => { var fileName = System.IO.Path.GetFileName(match.Groups["path"].Value); - var lineNumber = match.Groups["line"].Value; - return $" in {fileName}:line {lineNumber}"; + return $" in {fileName}"; }); yield return new KeyValuePair( diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityExecutionDiagnosticListenerTests.ParsingError_InvalidGraphQLDocument_ReportsErrorStatus.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityExecutionDiagnosticListenerTests.ParsingError_InvalidGraphQLDocument_ReportsErrorStatus.snap index e8efaf306c2..ad1d5945b6f 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityExecutionDiagnosticListenerTests.ParsingError_InvalidGraphQLDocument_ReportsErrorStatus.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityExecutionDiagnosticListenerTests.ParsingError_InvalidGraphQLDocument_ReportsErrorStatus.snap @@ -24,7 +24,7 @@ }, { "Key": "exception.stacktrace", - "Value": "HotChocolate.Language.SyntaxException: Expected a `RightBrace`-token, but found a `EndOfFile`-token.\n at HotChocolate.Language.Utf8GraphQLParser.ParseSelectionSet() in Utf8GraphQLParser.Operations.cs:line 221\n at HotChocolate.Language.Utf8GraphQLParser.ParseShortOperationDefinition() in Utf8GraphQLParser.Operations.cs:line 73\n at HotChocolate.Language.Utf8GraphQLParser.ParseDefinition() in Utf8GraphQLParser.cs:line 215\n at HotChocolate.Language.Utf8GraphQLParser.Parse() in Utf8GraphQLParser.cs:line 98\n at HotChocolate.Language.Utf8GraphQLParser.Parse(String sourceText, ParserOptions options) in Utf8GraphQLParser.cs:line 326\n at HotChocolate.Execution.Pipeline.DocumentParserMiddleware.InvokeAsync(RequestContext context) in DocumentParserMiddleware.cs:line 63" + "Value": "HotChocolate.Language.SyntaxException: Expected a `RightBrace`-token, but found a `EndOfFile`-token.\n at HotChocolate.Language.Utf8GraphQLParser.ParseSelectionSet() in Utf8GraphQLParser.Operations.cs\n at HotChocolate.Language.Utf8GraphQLParser.ParseShortOperationDefinition() in Utf8GraphQLParser.Operations.cs\n at HotChocolate.Language.Utf8GraphQLParser.ParseDefinition() in Utf8GraphQLParser.cs\n at HotChocolate.Language.Utf8GraphQLParser.Parse() in Utf8GraphQLParser.cs\n at HotChocolate.Language.Utf8GraphQLParser.Parse(String sourceText, ParserOptions options) in Utf8GraphQLParser.cs\n at HotChocolate.Execution.Pipeline.DocumentParserMiddleware.InvokeAsync(RequestContext context) in DocumentParserMiddleware.cs" }, { "Key": "exception.type", diff --git a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityExecutionDiagnosticListenerTests.SubscriptionEventError_Records_Subscription_Event_Error.snap b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityExecutionDiagnosticListenerTests.SubscriptionEventError_Records_Subscription_Event_Error.snap index cfd86e3b007..66ddcea5b5a 100644 --- a/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityExecutionDiagnosticListenerTests.SubscriptionEventError_Records_Subscription_Event_Error.snap +++ b/src/HotChocolate/Diagnostics/test/Diagnostics.Tests/__snapshots__/ActivityExecutionDiagnosticListenerTests.SubscriptionEventError_Records_Subscription_Event_Error.snap @@ -177,7 +177,7 @@ }, { "Key": "exception.stacktrace", - "Value": "System.InvalidOperationException: Subscription event failed.\n at HotChocolate.Diagnostics.ActivityExecutionDiagnosticListenerTests.SimpleSubscription.OnFailingMessage(String message) in ActivityExecutionDiagnosticListenerTests.cs:line 554\n at lambda_method(Closure, IResolverContext)\n at HotChocolate.Types.Helpers.FieldMiddlewareCompiler.<>c__DisplayClass9_0.<b__0>d.MoveNext() in FieldMiddlewareCompiler.cs:line 127\n--- End of stack trace from previous location ---\n at HotChocolate.Execution.Processing.Tasks.ResolverTask.ExecuteResolverPipelineAsync(CancellationToken cancellationToken) in ResolverTask.Execute.cs:line 135\n at HotChocolate.Execution.Processing.Tasks.ResolverTask.TryExecuteAsync(CancellationToken cancellationToken) in ResolverTask.Execute.cs:line 81" + "Value": "System.InvalidOperationException: Subscription event failed.\n at HotChocolate.Diagnostics.ActivityExecutionDiagnosticListenerTests.SimpleSubscription.OnFailingMessage(String message) in ActivityExecutionDiagnosticListenerTests.cs\n at lambda_method(Closure, IResolverContext)\n at HotChocolate.Types.Helpers.FieldMiddlewareCompiler.<>c__DisplayClass9_0.<b__0>d.MoveNext() in FieldMiddlewareCompiler.cs\n--- End of stack trace from previous location ---\n at HotChocolate.Execution.Processing.Tasks.ResolverTask.ExecuteResolverPipelineAsync(CancellationToken cancellationToken) in ResolverTask.Execute.cs\n at HotChocolate.Execution.Processing.Tasks.ResolverTask.TryExecuteAsync(CancellationToken cancellationToken) in ResolverTask.Execute.cs" }, { "Key": "exception.type", diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionParserOptions.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionParserOptions.cs index 430e4b85a0e..13a227e11dd 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionParserOptions.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionParserOptions.cs @@ -16,33 +16,46 @@ public sealed class FusionParserOptions public bool NoLocations { get; set; } /// + /// /// Parser CPU and memory usage is linear to the number of tokens in a document /// however in extreme cases it becomes quadratic due to memory exhaustion. /// Parsing happens before validation so even invalid queries can burn lots of /// CPU time and memory. - /// - /// To prevent this you can set a maximum number of tokens allowed within a document. + /// + /// To prevent this you can set a maximum number of tokens allowed within a document. /// public int MaxAllowedTokens { get; set; } = int.MaxValue; /// + /// /// Parser CPU and memory usage is linear to the number of nodes in a document /// however in extreme cases it becomes quadratic due to memory exhaustion. /// Parsing happens before validation so even invalid queries can burn lots of /// CPU time and memory. - /// - /// To prevent this you can set a maximum number of nodes allowed within a document. + /// + /// To prevent this you can set a maximum number of nodes allowed within a document. /// public int MaxAllowedNodes { get; set; } = int.MaxValue; /// + /// /// Parser CPU and memory usage is linear to the number of nodes in a document /// however in extreme cases it becomes quadratic due to memory exhaustion. /// Parsing happens before validation so even invalid queries can burn lots of /// CPU time and memory. - /// + /// + /// /// To prevent this you can set a maximum number of fields allowed within a document /// as fields is an easier way to estimate query size for GraphQL requests. + /// /// public int MaxAllowedFields { get; set; } = 2048; + + /// + /// + /// The maximum allowed recursion depth when parsing a document. + /// This prevents stack overflow from deeply nested queries. + /// + /// + public int MaxAllowedRecursionDepth { get; set; } = 200; } diff --git a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs index d3b2ea2d9a9..54a2234d5d1 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs @@ -292,7 +292,8 @@ private static ParserOptions CreateParserOptions(FusionGatewaySetup setup) allowFragmentVariables: false, maxAllowedNodes: options.MaxAllowedNodes, maxAllowedTokens: options.MaxAllowedTokens, - maxAllowedFields: options.MaxAllowedFields); + maxAllowedFields: options.MaxAllowedFields, + maxAllowedRecursionDepth: options.MaxAllowedRecursionDepth); } private SourceSchemaClientConfigurations CreateClientConfigurations( diff --git a/src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs b/src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs index 1c5ebac76d5..815abf3c211 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs @@ -33,18 +33,24 @@ public sealed class ParserOptions /// /// The maximum number of fields allowed within a query document. /// + /// + /// The maximum allowed recursion depth when parsing a document. + /// This prevents stack overflow from deeply nested queries. + /// public ParserOptions( bool noLocations = false, bool allowFragmentVariables = false, int maxAllowedNodes = int.MaxValue, int maxAllowedTokens = int.MaxValue, - int maxAllowedFields = 2048) + int maxAllowedFields = 2048, + int maxAllowedRecursionDepth = 200) { NoLocations = noLocations; Experimental = new(allowFragmentVariables); MaxAllowedTokens = maxAllowedTokens; MaxAllowedNodes = maxAllowedNodes; MaxAllowedFields = maxAllowedFields; + MaxAllowedRecursionDepth = maxAllowedRecursionDepth; } /// @@ -86,6 +92,11 @@ public ParserOptions( /// public int MaxAllowedFields { get; } + /// + /// Gets the maximum allowed recursion depth of a parsed document. + /// + public int MaxAllowedRecursionDepth { get; } + /// /// Gets the experimental parser options /// which are by default switched of. diff --git a/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs b/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs index 52181d1bd86..b67252760af 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs @@ -224,5 +224,11 @@ internal static string Utf8GraphQLParser_Start_MaxAllowedFieldsReached { return ResourceManager.GetString("Utf8GraphQLParser_Start_MaxAllowedFieldsReached", resourceCulture); } } + + internal static string Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached { + get { + return ResourceManager.GetString("Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx b/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx index feaa997ae7e..e1d2bb067b0 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx +++ b/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx @@ -207,4 +207,7 @@ The GraphQL request document contains more than {0} fields. Parsing aborted. + + Document exceeds the maximum allowed recursion depth of {0}. Parsing aborted. + diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs index 4c0e166b0f1..11cb8768313 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs @@ -194,6 +194,8 @@ private VariableNode ParseVariable() /// private SelectionSetNode ParseSelectionSet() { + IncreaseDepth(); + var start = Start(); if (_reader.Kind != TokenKind.LeftBrace) @@ -222,6 +224,7 @@ private SelectionSetNode ParseSelectionSet() var location = CreateLocation(in start); + DecreaseDepth(); return new SelectionSetNode( location, selections); diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs index 854e8da30b4..b2e6b9e01b7 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs @@ -12,6 +12,8 @@ public ref partial struct Utf8GraphQLParser /// private ITypeNode ParseTypeReference() { + IncreaseDepth(); + ITypeNode type; Location? location; @@ -40,6 +42,7 @@ private ITypeNode ParseTypeReference() MoveNext(); location = CreateLocation(in start); + DecreaseDepth(); return new NonNullTypeNode ( location, @@ -50,6 +53,7 @@ private ITypeNode ParseTypeReference() Unexpected(TokenKind.Bang); } + DecreaseDepth(); return type; } diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs index 983f61a8632..0621d19671c 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs @@ -24,6 +24,25 @@ private NameNode ParseName() [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool MoveNext() => _reader.MoveNext(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void IncreaseDepth() + { + if (++_recursionDepth > _maxAllowedRecursionDepth) + { + throw new SyntaxException( + _reader, + string.Format( + Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached, + _maxAllowedRecursionDepth)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void DecreaseDepth() + { + --_recursionDepth; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private TokenInfo Start() { diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs index c3d3b1a6963..2c3d4e162bf 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs @@ -29,32 +29,37 @@ public ref partial struct Utf8GraphQLParser /// private IValueNode ParseValueLiteral(bool isConstant) { + IncreaseDepth(); + + IValueNode node; + if (_reader.Kind == TokenKind.LeftBracket) { - return ParseList(isConstant); + node = ParseList(isConstant); } - - if (_reader.Kind == TokenKind.LeftBrace) + else if (_reader.Kind == TokenKind.LeftBrace) { - return ParseObject(isConstant); + node = ParseObject(isConstant); } - - if (TokenHelper.IsScalarValue(ref _reader)) + else if (TokenHelper.IsScalarValue(ref _reader)) { - return ParseScalarValue(); + node = ParseScalarValue(); } - - if (_reader.Kind == TokenKind.Name) + else if (_reader.Kind == TokenKind.Name) { - return ParseEnumValue(); + node = ParseEnumValue(); } - - if (_reader.Kind == TokenKind.Dollar && !isConstant) + else if (_reader.Kind == TokenKind.Dollar && !isConstant) + { + node = ParseVariable(); + } + else { - return ParseVariable(); + throw Unexpected(_reader.Kind); } - throw Unexpected(_reader.Kind); + DecreaseDepth(); + return node; } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs index 868ccff1e11..231b08a3cc5 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs @@ -12,10 +12,12 @@ public ref partial struct Utf8GraphQLParser private readonly bool _allowFragmentVars; private readonly int _maxAllowedNodes; private readonly int _maxAllowedFields; + private readonly int _maxAllowedRecursionDepth; private Utf8GraphQLReader _reader; private StringValueNode? _description; private int _parsedNodes; private int _parsedFields; + private int _recursionDepth; private Utf8MemoryBuilder? _memory; public Utf8GraphQLParser( @@ -32,6 +34,7 @@ public Utf8GraphQLParser( _allowFragmentVars = options.Experimental.AllowFragmentVariables; _maxAllowedNodes = options.MaxAllowedNodes; _maxAllowedFields = options.MaxAllowedFields; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = new Utf8GraphQLReader(sourceText, options.MaxAllowedTokens); _description = null; } @@ -50,6 +53,7 @@ public Utf8GraphQLParser( _allowFragmentVars = options.Experimental.AllowFragmentVariables; _maxAllowedNodes = options.MaxAllowedNodes; _maxAllowedFields = options.MaxAllowedFields; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = new Utf8GraphQLReader(sourceText, options.MaxAllowedTokens); _description = null; } @@ -68,6 +72,7 @@ internal Utf8GraphQLParser( _allowFragmentVars = options.Experimental.AllowFragmentVariables; _maxAllowedNodes = options.MaxAllowedNodes; _maxAllowedFields = options.MaxAllowedFields; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = reader; _description = null; } @@ -87,6 +92,7 @@ public DocumentNode Parse() try { _parsedNodes = 0; + _recursionDepth = 0; var definitions = new List(16); var start = Start(); diff --git a/src/HotChocolate/Language/test/Language.Tests/Parser/QueryParserTests.cs b/src/HotChocolate/Language/test/Language.Tests/Parser/QueryParserTests.cs index 05ceb25a64c..3b887a8c7bf 100644 --- a/src/HotChocolate/Language/test/Language.Tests/Parser/QueryParserTests.cs +++ b/src/HotChocolate/Language/test/Language.Tests/Parser/QueryParserTests.cs @@ -5,6 +5,149 @@ namespace HotChocolate.Language; public class QueryParserTests { + [Fact] + public void Default_MaxAllowedRecursionDepth_Is_200() + { + Assert.Equal(200, ParserOptions.Default.MaxAllowedRecursionDepth); + } + + [Fact] + public void Reject_Queries_Exceeding_Max_Recursion_Depth_Selection_Sets() + { + // Vector B: nested selection sets { a { a { ... } } } + const int depth = 201; + var query = string.Concat(Enumerable.Repeat("{ a", depth)) + + string.Concat(Enumerable.Repeat(" }", depth)); + + Assert + .Throws(() => Utf8GraphQLParser.Parse(query)) + .Message + .MatchInlineSnapshot( + "Document exceeds the maximum allowed recursion depth of 200. Parsing aborted."); + } + + [Fact] + public void Reject_Queries_Exceeding_Max_Recursion_Depth_Object_Values() + { + // Vector A: nested object values { a(x: {a: {a: ... 1 }}) } + const int depth = 201; + var query = "{ a(x: " + + string.Concat(Enumerable.Repeat("{a: ", depth)) + + "1" + + string.Concat(Enumerable.Repeat("}", depth)) + + ") }"; + + Assert + .Throws(() => Utf8GraphQLParser.Parse(query)) + .Message + .MatchInlineSnapshot( + "Document exceeds the maximum allowed recursion depth of 200. Parsing aborted."); + } + + [Fact] + public void Reject_Queries_Exceeding_Max_Recursion_Depth_List_Values() + { + // Vector C: nested list values [[[...1...]]] + const int depth = 201; + var query = "{ a(x: " + + string.Concat(Enumerable.Repeat("[", depth)) + + "1" + + string.Concat(Enumerable.Repeat("]", depth)) + + ") }"; + + Assert + .Throws(() => Utf8GraphQLParser.Parse(query)) + .Message + .MatchInlineSnapshot( + "Document exceeds the maximum allowed recursion depth of 200. Parsing aborted."); + } + + [Fact] + public void Reject_Queries_Exceeding_Max_Recursion_Depth_List_Types() + { + // Vector D: nested list types [[[...Int...]]] + const int depth = 201; + var query = $"query($v: {string.Concat(Enumerable.Repeat("[", depth))}Int{string.Concat(Enumerable.Repeat("]", depth))}) {{ a }}"; + + Assert + .Throws(() => Utf8GraphQLParser.Parse(query)) + .Message + .MatchInlineSnapshot( + "Document exceeds the maximum allowed recursion depth of 200. Parsing aborted."); + } + + [Fact] + public void Allow_Queries_Within_Max_Recursion_Depth() + { + // 50 levels of nesting is well within the default 200 limit + const int depth = 50; + var query = string.Concat(Enumerable.Repeat("{ a", depth)) + + string.Concat(Enumerable.Repeat(" }", depth)); + + var document = Utf8GraphQLParser.Parse(query); + + Assert.NotNull(document); + Assert.Single(document.Definitions); + } + + [Fact] + public void Reject_Queries_Exceeding_Custom_Recursion_Depth() + { + var options = new ParserOptions(maxAllowedRecursionDepth: 10); + const int depth = 11; + var query = string.Concat(Enumerable.Repeat("{ a", depth)) + + string.Concat(Enumerable.Repeat(" }", depth)); + + Assert + .Throws(() => Utf8GraphQLParser.Parse(query, options)) + .Message + .MatchInlineSnapshot( + "Document exceeds the maximum allowed recursion depth of 10. Parsing aborted."); + } + + [Fact] + public void Allow_Queries_Within_Custom_Recursion_Depth() + { + var options = new ParserOptions(maxAllowedRecursionDepth: 10); + const int depth = 10; + var query = string.Concat(Enumerable.Repeat("{ a", depth)) + + string.Concat(Enumerable.Repeat(" }", depth)); + + var document = Utf8GraphQLParser.Parse(query, options); + + Assert.NotNull(document); + Assert.Single(document.Definitions); + } + + [Theory] + [InlineData(20_000)] + [InlineData(50_000)] + public void Reject_Attack_Payload_Nested_Selection_Sets(int depth) + { + // Payloads at these depths would cause a StackOverflowException + // (process-fatal, uncatchable) without the recursion depth limit. + // With the limit, they throw a catchable SyntaxException at depth 201. + var query = string.Concat(Enumerable.Repeat("{ a", depth)) + + string.Concat(Enumerable.Repeat(" }", depth)); + + Assert.Throws(() => Utf8GraphQLParser.Parse(query)); + } + + [Theory] + [InlineData(20_000)] + [InlineData(50_000)] + public void Reject_Attack_Payload_Nested_List_Values(int depth) + { + // Vector C from the vulnerability report — smallest crashing payload (~40 KB). + var query = "{ a(x: " + + string.Concat(Enumerable.Repeat("[", depth)) + + "1" + + string.Concat(Enumerable.Repeat("]", depth)) + + ") }"; + + Assert.Throws(() => Utf8GraphQLParser.Parse(query)); + } + [Fact] public void Reject_Queries_With_More_Than_2048_Fields() {