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()
{