CVE-2026-40324
Description
Hot Chocolate is an open-source GraphQL server. Prior to versions 12.22.7, 13.9.16, 14.3.1, and 15.1.14, Hot Chocolate's recursive descent parser Utf8GraphQLParser has no recursion depth limit. A crafted GraphQL document with deeply nested selection sets, object values, list values, or list types can trigger a StackOverflowException on payloads as small as 40 KB. Because StackOverflowException is uncatchable in .NET (since .NET 2.0), the entire worker process is terminated immediately. All in-flight HTTP requests, background IHostedService tasks, and open WebSocket subscriptions on that worker are dropped. The orchestrator (Kubernetes, IIS, etc.) must restart the process. This occurs before any validation rules run — MaxExecutionDepth, complexity analyzers, persisted query allow-lists, and custom IDocumentValidatorRule implementations cannot intercept the crash because Utf8GraphQLParser.Parse is invoked before validation. The MaxAllowedFields=2048 limit does not help because the crashing payloads contain very few fields. The fix in versions 12.22.7, 13.9.16, 14.3.1, and 15.1.14 adds a MaxAllowedRecursionDepth option to ParserOptions with a safe default, and enforces it across all recursive parser methods (ParseSelectionSet, ParseValueLiteral, ParseObject, ParseList, ParseTypeReference, etc.). When the limit is exceeded, a catchable SyntaxException is thrown instead of overflowing the stack. There is no application-level workaround. StackOverflowException cannot be caught in .NET. The only mitigation is to upgrade to a patched version. Operators can reduce (but not eliminate) risk by limiting HTTP request body size at the reverse proxy or load balancer layer, though the smallest crashing payload (40 KB) is well below most default body size limits and is highly compressible (~few hundred bytes via gzip).
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
HotChocolate.LanguageNuGet | < 12.22.7 | 12.22.7 |
HotChocolate.LanguageNuGet | >= 13.0.0, < 13.9.16 | 13.9.16 |
HotChocolate.LanguageNuGet | >= 14.0.0, < 14.3.1 | 14.3.1 |
HotChocolate.LanguageNuGet | >= 15.0.0, < 15.1.14 | 15.1.14 |
Affected products
1Patches
4b9271e6a5004Add depth limit to GraphQL parser
10 files changed · +116 −22
src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs+29 −0 modified@@ -21,8 +21,37 @@ public ParserOptions( NoLocations = noLocations; Experimental = new ParserOptionsExperimental( allowFragmentVariables); + MaxAllowedDirectives = 4; + MaxAllowedRecursionDepth = 200; } + /// <summary> + /// Initializes a new instance of <see cref="ParserOptions"/> with security limits. + /// </summary> + public ParserOptions( + bool noLocations, + bool allowFragmentVariables, + int maxAllowedDirectives, + int maxAllowedRecursionDepth) + { + NoLocations = noLocations; + Experimental = new ParserOptionsExperimental(allowFragmentVariables); + MaxAllowedDirectives = maxAllowedDirectives; + MaxAllowedRecursionDepth = maxAllowedRecursionDepth; + } + + /// <summary> + /// The maximum number of directives allowed per location (e.g. per field, + /// per operation, per fragment definition). Repeatable directives can be used + /// to exhaust CPU and memory resources if not limited. + /// </summary> + public int MaxAllowedDirectives { get; } + + /// <summary> + /// Gets the maximum allowed recursion depth of a parsed document. + /// </summary> + public int MaxAllowedRecursionDepth { get; } + /// <summary> /// By default, the parser creates <see cref="ISyntaxNode" />s /// that know the location in the source that they correspond to.
src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs+12 −0 modified@@ -158,5 +158,17 @@ internal static string UnexpectedToken { return ResourceManager.GetString("UnexpectedToken", resourceCulture); } } + + internal static string Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached { + get { + return ResourceManager.GetString("Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached", resourceCulture); + } + } + + internal static string Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached { + get { + return ResourceManager.GetString("Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached", resourceCulture); + } + } } }
src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx+6 −0 modified@@ -75,4 +75,10 @@ <data name="UnexpectedToken" xml:space="preserve"> <value>Unexpected token: {0}.</value> </data> + <data name="Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached" xml:space="preserve"> + <value>A location in the GraphQL document contains more than {0} directives. Parsing aborted.</value> + </data> + <data name="Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached" xml:space="preserve"> + <value>Document exceeds the maximum allowed recursion depth of {0}. Parsing aborted.</value> + </data> </root>
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs+8 −0 modified@@ -9,8 +9,11 @@ public ref partial struct Utf8GraphQLParser { private readonly bool _createLocation; private readonly bool _allowFragmentVars; + private readonly int _maxAllowedDirectives; + private readonly int _maxAllowedRecursionDepth; private Utf8GraphQLReader _reader; private StringValueNode? _description; + private int _recursionDepth; public Utf8GraphQLParser( ReadOnlySpan<byte> graphQLData, @@ -24,6 +27,8 @@ public Utf8GraphQLParser( options ??= ParserOptions.Default; _createLocation = !options.NoLocations; _allowFragmentVars = options.Experimental.AllowFragmentVariables; + _maxAllowedDirectives = options.MaxAllowedDirectives; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = new Utf8GraphQLReader(graphQLData); _description = null; } @@ -40,13 +45,16 @@ internal Utf8GraphQLParser( options ??= ParserOptions.Default; _createLocation = !options.NoLocations; _allowFragmentVars = options.Experimental.AllowFragmentVariables; + _maxAllowedDirectives = options.MaxAllowedDirectives; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = reader; _description = null; } public DocumentNode Parse() { var definitions = new List<IDefinitionNode>(); + _recursionDepth = 0; TokenInfo start = Start();
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Directives.cs+11 −1 modified@@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; +using static HotChocolate.Language.Properties.LangUtf8Resources; namespace HotChocolate.Language; @@ -67,7 +68,7 @@ private NameNode ParseDirectiveLocation() throw Unexpected(kind); } - private List<DirectiveNode> ParseDirectives(bool isConstant) + private List<DirectiveNode> ParseDirectives(bool isConstant, bool isQueryLocation = false) { if (_reader.Kind == TokenKind.At) { @@ -76,6 +77,15 @@ private List<DirectiveNode> ParseDirectives(bool isConstant) while (_reader.Kind == TokenKind.At) { list.Add(ParseDirective(isConstant)); + + if (isQueryLocation && list.Count > _maxAllowedDirectives) + { + throw new SyntaxException( + _reader, + string.Format( + Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached, + _maxAllowedDirectives)); + } } return list;
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Fragments.cs+4 −4 modified@@ -58,7 +58,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() ParseVariableDefinitions(); ExpectOnKeyword(); NamedTypeNode typeCondition = ParseNamedType(); - List<DirectiveNode> directives = ParseDirectives(false); + List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true); SelectionSetNode selectionSet = ParseSelectionSet(); Location? location = CreateLocation(in start); @@ -77,7 +77,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() NameNode name = ParseFragmentName(); ExpectOnKeyword(); NamedTypeNode typeCondition = ParseNamedType(); - List<DirectiveNode> directives = ParseDirectives(false); + List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true); SelectionSetNode selectionSet = ParseSelectionSet(); Location? location = CreateLocation(in start); @@ -104,7 +104,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() private FragmentSpreadNode ParseFragmentSpread(in TokenInfo start) { NameNode name = ParseFragmentName(); - List<DirectiveNode> directives = ParseDirectives(false); + List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true); Location? location = CreateLocation(in start); return new FragmentSpreadNode @@ -130,7 +130,7 @@ private InlineFragmentNode ParseInlineFragment( in TokenInfo start, NamedTypeNode? typeCondition) { - List<DirectiveNode> directives = ParseDirectives(false); + List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true); SelectionSetNode selectionSet = ParseSelectionSet(); Location? location = CreateLocation(in start);
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs+5 −3 modified@@ -25,7 +25,7 @@ private OperationDefinitionNode ParseOperationDefinition() OperationType operation = ParseOperationType(); NameNode? name = _reader.Kind == TokenKind.Name ? ParseName() : null; List<VariableDefinitionNode> variableDefinitions = ParseVariableDefinitions(); - List<DirectiveNode> directives = ParseDirectives(false); + List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true); SelectionSetNode selectionSet = ParseSelectionSet(); Location? location = CreateLocation(in start); @@ -139,7 +139,7 @@ private VariableDefinitionNode ParseVariableDefinition() ? ParseValueLiteral(true) : null; List<DirectiveNode> directives = - ParseDirectives(true); + ParseDirectives(true, isQueryLocation: true); Location? location = CreateLocation(in start); @@ -181,6 +181,7 @@ private VariableNode ParseVariable() [MethodImpl(MethodImplOptions.AggressiveInlining)] private SelectionSetNode ParseSelectionSet() { + IncreaseDepth(); TokenInfo start = Start(); if (_reader.Kind != TokenKind.LeftBrace) @@ -208,6 +209,7 @@ private SelectionSetNode ParseSelectionSet() Location? location = CreateLocation(in start); + DecreaseDepth(); return new SelectionSetNode ( location, @@ -253,7 +255,7 @@ private FieldNode ParseField() List<ArgumentNode> arguments = ParseArguments(false); INullabilityNode? required = ParseRequiredStatus(); - List<DirectiveNode> directives = ParseDirectives(false); + List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true); SelectionSetNode? selectionSet = _reader.Kind == TokenKind.LeftBrace ? ParseSelectionSet() : null;
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs+3 −0 modified@@ -12,6 +12,7 @@ public ref partial struct Utf8GraphQLParser /// </summary> private ITypeNode ParseTypeReference() { + IncreaseDepth(); ITypeNode type; Location? location; @@ -40,6 +41,7 @@ private ITypeNode ParseTypeReference() MoveNext(); location = CreateLocation(in start); + DecreaseDepth(); return new NonNullTypeNode ( location, @@ -50,6 +52,7 @@ private ITypeNode ParseTypeReference() Unexpected(TokenKind.Bang); } + DecreaseDepth(); return type; }
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs+19 −0 modified@@ -27,6 +27,25 @@ internal NameNode ParseName() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal 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() => _createLocation
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs+19 −14 modified@@ -31,32 +31,37 @@ public ref partial struct Utf8GraphQLParser /// </param> internal 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(in _reader)) + else if (TokenHelper.IsScalarValue(in _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)]
08c0caa42ca3Add depth limit to GraphQL parser (#9531)
12 files changed · +139 −23
src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs+4 −1 modified@@ -91,9 +91,12 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services return new ParserOptions( noLocations: !options.IncludeLocations, + allowFragmentVariables: false, maxAllowedNodes: options.MaxAllowedNodes, maxAllowedTokens: options.MaxAllowedTokens, - maxAllowedFields: options.MaxAllowedFields); + maxAllowedFields: options.MaxAllowedFields, + maxAllowedDirectives: options.MaxAllowedDirectives, + maxAllowedRecursionDepth: options.MaxAllowedRecursionDepth); }); return services;
src/HotChocolate/Core/src/Execution/Options/RequestParserOptions.cs+13 −0 modified@@ -50,4 +50,17 @@ public sealed class RequestParserOptions /// as fields is an easier way to estimate query size for GraphQL requests. /// </summary> public int MaxAllowedFields { get; set; } = 2048; + + /// <summary> + /// The maximum number of directives allowed per location (e.g. per field, + /// per operation, per fragment definition). Repeatable directives can be used + /// to exhaust CPU and memory resources if not limited. + /// </summary> + public int MaxAllowedDirectives { get; set; } = 4; + + /// <summary> + /// The maximum allowed recursion depth when parsing a document. + /// This prevents stack overflow from deeply nested queries. + /// </summary> + public int MaxAllowedRecursionDepth { get; set; } = 200; }
src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs+35 −0 modified@@ -42,6 +42,29 @@ public ParserOptions( MaxAllowedTokens = maxAllowedTokens; MaxAllowedNodes = maxAllowedNodes; MaxAllowedFields = maxAllowedFields; + MaxAllowedDirectives = 4; + MaxAllowedRecursionDepth = 200; + } + + /// <summary> + /// Initializes a new instance of <see cref="ParserOptions"/> with security limits. + /// </summary> + public ParserOptions( + bool noLocations, + bool allowFragmentVariables, + int maxAllowedNodes, + int maxAllowedTokens, + int maxAllowedFields, + int maxAllowedDirectives, + int maxAllowedRecursionDepth) + { + NoLocations = noLocations; + Experimental = new(allowFragmentVariables); + MaxAllowedTokens = maxAllowedTokens; + MaxAllowedNodes = maxAllowedNodes; + MaxAllowedFields = maxAllowedFields; + MaxAllowedDirectives = maxAllowedDirectives; + MaxAllowedRecursionDepth = maxAllowedRecursionDepth; } /// <summary> @@ -83,6 +106,18 @@ public ParserOptions( /// </summary> public int MaxAllowedFields { get; } + /// <summary> + /// The maximum number of directives allowed per location (e.g. per field, + /// per operation, per fragment definition). Repeatable directives can be used + /// to exhaust CPU and memory resources if not limited. + /// </summary> + public int MaxAllowedDirectives { get; } + + /// <summary> + /// Gets the maximum allowed recursion depth of a parsed document. + /// </summary> + public int MaxAllowedRecursionDepth { get; } + /// <summary> /// Gets the experimental parser options /// which are by default switched of.
src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs+12 −0 modified@@ -218,5 +218,17 @@ internal static string Utf8GraphQLParser_Start_MaxAllowedFieldsReached { return ResourceManager.GetString("Utf8GraphQLParser_Start_MaxAllowedFieldsReached", resourceCulture); } } + + internal static string Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached { + get { + return ResourceManager.GetString("Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached", resourceCulture); + } + } + + internal static string Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached { + get { + return ResourceManager.GetString("Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached", resourceCulture); + } + } } }
src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx+6 −0 modified@@ -204,4 +204,10 @@ <data name="Utf8GraphQLParser_Start_MaxAllowedFieldsReached" xml:space="preserve"> <value>The GraphQL request document contains more than {0} fields. Parsing aborted.</value> </data> + <data name="Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached" xml:space="preserve"> + <value>A location in the GraphQL document contains more than {0} directives. Parsing aborted.</value> + </data> + <data name="Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached" xml:space="preserve"> + <value>Document exceeds the maximum allowed recursion depth of {0}. Parsing aborted.</value> + </data> </root>
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs+8 −0 modified@@ -12,10 +12,13 @@ public ref partial struct Utf8GraphQLParser private readonly bool _allowFragmentVars; private readonly int _maxAllowedNodes; private readonly int _maxAllowedFields; + private readonly int _maxAllowedDirectives; + private readonly int _maxAllowedRecursionDepth; private Utf8GraphQLReader _reader; private StringValueNode? _description; private int _parsedNodes; private int _parsedFields; + private int _recursionDepth; public Utf8GraphQLParser( ReadOnlySpan<byte> graphQLData, @@ -31,6 +34,8 @@ public Utf8GraphQLParser( _allowFragmentVars = options.Experimental.AllowFragmentVariables; _maxAllowedNodes = options.MaxAllowedNodes; _maxAllowedFields = options.MaxAllowedFields; + _maxAllowedDirectives = options.MaxAllowedDirectives; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = new Utf8GraphQLReader(graphQLData, options.MaxAllowedTokens); _description = null; } @@ -49,6 +54,8 @@ internal Utf8GraphQLParser( _allowFragmentVars = options.Experimental.AllowFragmentVariables; _maxAllowedNodes = options.MaxAllowedNodes; _maxAllowedFields = options.MaxAllowedFields; + _maxAllowedDirectives = options.MaxAllowedDirectives; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = reader; _description = null; } @@ -72,6 +79,7 @@ internal Utf8GraphQLParser( public DocumentNode Parse() { _parsedNodes = 0; + _recursionDepth = 0; var definitions = new List<IDefinitionNode>(); var start = Start();
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Directives.cs+11 −1 modified@@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; +using static HotChocolate.Language.Properties.LangUtf8Resources; namespace HotChocolate.Language; @@ -65,7 +66,7 @@ private NameNode ParseDirectiveLocation() throw Unexpected(kind); } - private List<DirectiveNode> ParseDirectives(bool isConstant) + private List<DirectiveNode> ParseDirectives(bool isConstant, bool isQueryLocation = false) { if (_reader.Kind == TokenKind.At) { @@ -74,6 +75,15 @@ private List<DirectiveNode> ParseDirectives(bool isConstant) while (_reader.Kind == TokenKind.At) { list.Add(ParseDirective(isConstant)); + + if (isQueryLocation && list.Count > _maxAllowedDirectives) + { + throw new SyntaxException( + _reader, + string.Format( + Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached, + _maxAllowedDirectives)); + } } return list;
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Fragments.cs+4 −4 modified@@ -55,7 +55,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() ParseVariableDefinitions(); ExpectOnKeyword(); var typeCondition = ParseNamedType(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start); @@ -74,7 +74,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() var name = ParseFragmentName(); ExpectOnKeyword(); var typeCondition = ParseNamedType(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start); @@ -101,7 +101,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() private FragmentSpreadNode ParseFragmentSpread(in TokenInfo start) { var name = ParseFragmentName(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var location = CreateLocation(in start); return new FragmentSpreadNode @@ -127,7 +127,7 @@ private InlineFragmentNode ParseInlineFragment( in TokenInfo start, NamedTypeNode? typeCondition) { - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start);
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs+5 −3 modified@@ -23,7 +23,7 @@ private OperationDefinitionNode ParseOperationDefinition() var operation = ParseOperationType(); var name = _reader.Kind == TokenKind.Name ? ParseName() : null; var variableDefinitions = ParseVariableDefinitions(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start); @@ -133,7 +133,7 @@ private VariableDefinitionNode ParseVariableDefinition() ? ParseValueLiteral(true) : null; var directives = - ParseDirectives(true); + ParseDirectives(isConstant: true, isQueryLocation: true); var location = CreateLocation(in start); @@ -173,6 +173,7 @@ private VariableNode ParseVariable() /// </summary> private SelectionSetNode ParseSelectionSet() { + IncreaseDepth(); var start = Start(); if (_reader.Kind != TokenKind.LeftBrace) @@ -200,6 +201,7 @@ private SelectionSetNode ParseSelectionSet() var location = CreateLocation(in start); + DecreaseDepth(); return new SelectionSetNode ( location, @@ -252,7 +254,7 @@ private FieldNode ParseField() var arguments = ParseArguments(false); var required = ParseRequiredStatus(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = _reader.Kind == TokenKind.LeftBrace ? ParseSelectionSet() : null;
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs+3 −0 modified@@ -12,6 +12,7 @@ public ref partial struct Utf8GraphQLParser /// </summary> private ITypeNode ParseTypeReference() { + IncreaseDepth(); ITypeNode type; Location? location; @@ -40,6 +41,7 @@ private ITypeNode ParseTypeReference() MoveNext(); location = CreateLocation(in start); + DecreaseDepth(); return new NonNullTypeNode ( location, @@ -50,6 +52,7 @@ private ITypeNode ParseTypeReference() Unexpected(TokenKind.Bang); } + DecreaseDepth(); return type; }
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs+19 −0 modified@@ -27,6 +27,25 @@ internal NameNode ParseName() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal 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() {
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs+19 −14 modified@@ -32,32 +32,37 @@ public ref partial struct Utf8GraphQLParser /// </param> internal 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(in _reader)) + else if (TokenHelper.IsScalarValue(in _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)]
b185eb276c9eAdd depth limit to GraphQL parser (#9530)
14 files changed · +162 −24
.build/Build.csproj+1 −1 modified@@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>net7.0</TargetFramework> + <TargetFramework>net8.0</TargetFramework> <RootNamespace></RootNamespace> <NoWarn>CS0649;CS0169</NoWarn> <NukeRootDirectory>..</NukeRootDirectory>
src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs+4 −1 modified@@ -96,9 +96,12 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services return new ParserOptions( noLocations: !options.IncludeLocations, + allowFragmentVariables: false, maxAllowedNodes: options.MaxAllowedNodes, maxAllowedTokens: options.MaxAllowedTokens, - maxAllowedFields: options.MaxAllowedFields); + maxAllowedFields: options.MaxAllowedFields, + maxAllowedDirectives: options.MaxAllowedDirectives, + maxAllowedRecursionDepth: options.MaxAllowedRecursionDepth); }); return services;
src/HotChocolate/Core/src/Execution/Options/RequestParserOptions.cs+13 −0 modified@@ -50,4 +50,17 @@ public sealed class RequestParserOptions /// as fields is an easier way to estimate query size for GraphQL requests. /// </summary> public int MaxAllowedFields { get; set; } = 2048; + + /// <summary> + /// The maximum number of directives allowed per location (e.g. per field, + /// per operation, per fragment definition). Repeatable directives can be used + /// to exhaust CPU and memory resources if not limited. + /// </summary> + public int MaxAllowedDirectives { get; set; } = 4; + + /// <summary> + /// The maximum allowed recursion depth when parsing a document. + /// This prevents stack overflow from deeply nested queries. + /// </summary> + public int MaxAllowedRecursionDepth { get; set; } = 200; }
src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs+56 −0 modified@@ -45,6 +45,50 @@ public ParserOptions( MaxAllowedTokens = maxAllowedTokens; MaxAllowedNodes = maxAllowedNodes; MaxAllowedFields = maxAllowedFields; + MaxAllowedDirectives = 4; + MaxAllowedRecursionDepth = 200; + } + + /// <summary> + /// Initializes a new instance of <see cref="ParserOptions"/>. + /// </summary> + /// <param name="noLocations"> + /// Defines that the parse shall not preserve syntax node locations. + /// </param> + /// <param name="allowFragmentVariables"> + /// Defines that the parser shall parse fragment variables. + /// </param> + /// <param name="maxAllowedNodes"> + /// The maximum number of nodes allowed within a document. + /// </param> + /// <param name="maxAllowedTokens"> + /// The maximum number of tokens allowed within a document. + /// </param> + /// <param name="maxAllowedFields"> + /// The maximum number of fields allowed within a query document. + /// </param> + /// <param name="maxAllowedDirectives"> + /// The maximum number of directives allowed per location. + /// </param> + /// <param name="maxAllowedRecursionDepth"> + /// The maximum allowed recursion depth of a parsed document. + /// </param> + public ParserOptions( + bool noLocations, + bool allowFragmentVariables, + int maxAllowedNodes, + int maxAllowedTokens, + int maxAllowedFields, + int maxAllowedDirectives, + int maxAllowedRecursionDepth) + { + NoLocations = noLocations; + Experimental = new(allowFragmentVariables); + MaxAllowedTokens = maxAllowedTokens; + MaxAllowedNodes = maxAllowedNodes; + MaxAllowedFields = maxAllowedFields; + MaxAllowedDirectives = maxAllowedDirectives; + MaxAllowedRecursionDepth = maxAllowedRecursionDepth; } /// <summary> @@ -86,6 +130,18 @@ public ParserOptions( /// </summary> public int MaxAllowedFields { get; } + /// <summary> + /// The maximum number of directives allowed per location (e.g. per field, + /// per operation, per fragment definition). Repeatable directives can be used + /// to exhaust CPU and memory resources if not limited. + /// </summary> + public int MaxAllowedDirectives { get; } + + /// <summary> + /// Gets the maximum allowed recursion depth of a parsed document. + /// </summary> + public int MaxAllowedRecursionDepth { get; } + /// <summary> /// Gets the experimental parser options /// which are by default switched of.
src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs+12 −0 modified@@ -224,5 +224,17 @@ internal static string Utf8GraphQLParser_Start_MaxAllowedFieldsReached { return ResourceManager.GetString("Utf8GraphQLParser_Start_MaxAllowedFieldsReached", resourceCulture); } } + + internal static string Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached { + get { + return ResourceManager.GetString("Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached", resourceCulture); + } + } + + internal static string Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached { + get { + return ResourceManager.GetString("Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached", resourceCulture); + } + } } }
src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx+6 −0 modified@@ -207,4 +207,10 @@ <data name="Utf8GraphQLParser_Start_MaxAllowedFieldsReached" xml:space="preserve"> <value>The GraphQL request document contains more than {0} fields. Parsing aborted.</value> </data> + <data name="Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached" xml:space="preserve"> + <value>A location in the GraphQL document contains more than {0} directives. Parsing aborted.</value> + </data> + <data name="Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached" xml:space="preserve"> + <value>Document exceeds the maximum allowed recursion depth of {0}. Parsing aborted.</value> + </data> </root>
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs+8 −0 modified@@ -10,10 +10,13 @@ public ref partial struct Utf8GraphQLParser private readonly bool _allowFragmentVars; private readonly int _maxAllowedNodes; private readonly int _maxAllowedFields; + private readonly int _maxAllowedDirectives; + private readonly int _maxAllowedRecursionDepth; private Utf8GraphQLReader _reader; private StringValueNode? _description; private int _parsedNodes; private int _parsedFields; + private int _recursionDepth; public Utf8GraphQLParser( ReadOnlySpan<byte> graphQLData, @@ -29,6 +32,8 @@ public Utf8GraphQLParser( _allowFragmentVars = options.Experimental.AllowFragmentVariables; _maxAllowedNodes = options.MaxAllowedNodes; _maxAllowedFields = options.MaxAllowedFields; + _maxAllowedDirectives = options.MaxAllowedDirectives; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = new Utf8GraphQLReader(graphQLData, options.MaxAllowedTokens); _description = null; } @@ -47,6 +52,8 @@ internal Utf8GraphQLParser( _allowFragmentVars = options.Experimental.AllowFragmentVariables; _maxAllowedNodes = options.MaxAllowedNodes; _maxAllowedFields = options.MaxAllowedFields; + _maxAllowedDirectives = options.MaxAllowedDirectives; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = reader; _description = null; } @@ -64,6 +71,7 @@ internal Utf8GraphQLParser( public DocumentNode Parse() { _parsedNodes = 0; + _recursionDepth = 0; var definitions = new List<IDefinitionNode>(); var start = Start();
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Directives.cs+11 −1 modified@@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using static HotChocolate.Language.Properties.LangUtf8Resources; namespace HotChocolate.Language; @@ -64,7 +65,7 @@ private NameNode ParseDirectiveLocation() throw Unexpected(kind); } - private List<DirectiveNode> ParseDirectives(bool isConstant) + private List<DirectiveNode> ParseDirectives(bool isConstant, bool isQueryLocation = false) { if (_reader.Kind == TokenKind.At) { @@ -73,6 +74,15 @@ private List<DirectiveNode> ParseDirectives(bool isConstant) while (_reader.Kind == TokenKind.At) { list.Add(ParseDirective(isConstant)); + + if (isQueryLocation && list.Count > _maxAllowedDirectives) + { + throw new SyntaxException( + _reader, + string.Format( + Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached, + _maxAllowedDirectives)); + } } return list;
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Fragments.cs+4 −4 modified@@ -53,7 +53,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() ParseVariableDefinitions(); ExpectOnKeyword(); var typeCondition = ParseNamedType(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start); @@ -72,7 +72,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() var name = ParseFragmentName(); ExpectOnKeyword(); var typeCondition = ParseNamedType(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start); @@ -99,7 +99,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() private FragmentSpreadNode ParseFragmentSpread(in TokenInfo start) { var name = ParseFragmentName(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var location = CreateLocation(in start); return new FragmentSpreadNode @@ -125,7 +125,7 @@ private InlineFragmentNode ParseInlineFragment( in TokenInfo start, NamedTypeNode? typeCondition) { - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start);
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs+5 −3 modified@@ -21,7 +21,7 @@ private OperationDefinitionNode ParseOperationDefinition() var operation = ParseOperationType(); var name = _reader.Kind == TokenKind.Name ? ParseName() : null; var variableDefinitions = ParseVariableDefinitions(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start); @@ -127,7 +127,7 @@ private VariableDefinitionNode ParseVariableDefinition() ? ParseValueLiteral(true) : null; var directives = - ParseDirectives(isConstant: true); + ParseDirectives(isConstant: true, isQueryLocation: true); var location = CreateLocation(in start); @@ -163,6 +163,7 @@ private VariableNode ParseVariable() /// </summary> private SelectionSetNode ParseSelectionSet() { + IncreaseDepth(); var start = Start(); if (_reader.Kind != TokenKind.LeftBrace) @@ -191,6 +192,7 @@ private SelectionSetNode ParseSelectionSet() var location = CreateLocation(in start); + DecreaseDepth(); return new SelectionSetNode( location, selections); @@ -240,7 +242,7 @@ private FieldNode ParseField() } var arguments = ParseArguments(false); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = _reader.Kind == TokenKind.LeftBrace ? ParseSelectionSet() : null;
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs+3 −0 modified@@ -12,6 +12,7 @@ public ref partial struct Utf8GraphQLParser /// </summary> private ITypeNode ParseTypeReference() { + IncreaseDepth(); ITypeNode type; Location? location; @@ -40,6 +41,7 @@ private ITypeNode ParseTypeReference() MoveNext(); location = CreateLocation(in start); + DecreaseDepth(); return new NonNullTypeNode ( location, @@ -50,6 +52,7 @@ private ITypeNode ParseTypeReference() Unexpected(TokenKind.Bang); } + DecreaseDepth(); return type; }
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs+19 −0 modified@@ -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() {
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs+19 −14 modified@@ -29,32 +29,37 @@ public ref partial struct Utf8GraphQLParser /// </param> 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)]
src/HotChocolate/Primitives/test/Directory.Build.props+1 −0 modified@@ -2,6 +2,7 @@ <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)..\'))" /> <PropertyGroup> + <TargetFrameworks>$(TestTargetFrameworks)</TargetFrameworks> <IsPackable>false</IsPackable> <GenerateDocumentationFile>false</GenerateDocumentationFile> </PropertyGroup>
4cbaf67d366fAdd depth limit to GraphQL parser (#9528)
13 files changed · +364 −23
src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs+4 −1 modified@@ -94,9 +94,12 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services return new ParserOptions( noLocations: !options.IncludeLocations, + allowFragmentVariables: false, maxAllowedNodes: options.MaxAllowedNodes, maxAllowedTokens: options.MaxAllowedTokens, - maxAllowedFields: options.MaxAllowedFields); + maxAllowedFields: options.MaxAllowedFields, + maxAllowedDirectives: options.MaxAllowedDirectives, + maxAllowedRecursionDepth: options.MaxAllowedRecursionDepth); }); return services;
src/HotChocolate/Core/src/Execution/Options/RequestParserOptions.cs+13 −0 modified@@ -50,4 +50,17 @@ public sealed class RequestParserOptions /// as fields is an easier way to estimate query size for GraphQL requests. /// </summary> public int MaxAllowedFields { get; set; } = 2048; + + /// <summary> + /// The maximum number of directives allowed per location (e.g. per field, + /// per operation, per fragment definition). Repeatable directives can be used + /// to exhaust CPU and memory resources if not limited. + /// </summary> + public int MaxAllowedDirectives { get; set; } = 4; + + /// <summary> + /// The maximum allowed recursion depth when parsing a document. + /// This prevents stack overflow from deeply nested queries. + /// </summary> + public int MaxAllowedRecursionDepth { get; set; } = 200; }
src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs+57 −0 modified@@ -45,6 +45,51 @@ public ParserOptions( MaxAllowedTokens = maxAllowedTokens; MaxAllowedNodes = maxAllowedNodes; MaxAllowedFields = maxAllowedFields; + MaxAllowedDirectives = 4; + MaxAllowedRecursionDepth = 200; + } + + /// <summary> + /// Initializes a new instance of <see cref="ParserOptions"/> with security limits. + /// </summary> + /// <param name="noLocations"> + /// Defines that the parse shall not preserve syntax node locations. + /// </param> + /// <param name="allowFragmentVariables"> + /// Defines that the parser shall parse fragment variables. + /// </param> + /// <param name="maxAllowedNodes"> + /// The maximum number of nodes allowed within a document. + /// </param> + /// <param name="maxAllowedTokens"> + /// The maximum number of tokens allowed within a document. + /// </param> + /// <param name="maxAllowedFields"> + /// The maximum number of fields allowed within a query document. + /// </param> + /// <param name="maxAllowedDirectives"> + /// The maximum number of directives allowed per location (e.g. per field, + /// per operation, per fragment definition). + /// </param> + /// <param name="maxAllowedRecursionDepth"> + /// The maximum allowed recursion depth of a parsed document. + /// </param> + public ParserOptions( + bool noLocations, + bool allowFragmentVariables, + int maxAllowedNodes, + int maxAllowedTokens, + int maxAllowedFields, + int maxAllowedDirectives, + int maxAllowedRecursionDepth) + { + NoLocations = noLocations; + Experimental = new(allowFragmentVariables); + MaxAllowedTokens = maxAllowedTokens; + MaxAllowedNodes = maxAllowedNodes; + MaxAllowedFields = maxAllowedFields; + MaxAllowedDirectives = maxAllowedDirectives; + MaxAllowedRecursionDepth = maxAllowedRecursionDepth; } /// <summary> @@ -86,6 +131,18 @@ public ParserOptions( /// </summary> public int MaxAllowedFields { get; } + /// <summary> + /// The maximum number of directives allowed per location (e.g. per field, + /// per operation, per fragment definition). Repeatable directives can be used + /// to exhaust CPU and memory resources if not limited. + /// </summary> + public int MaxAllowedDirectives { get; } + + /// <summary> + /// Gets the maximum allowed recursion depth of a parsed document. + /// </summary> + public int MaxAllowedRecursionDepth { get; } + /// <summary> /// Gets the experimental parser options /// which are by default switched of.
src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs+12 −0 modified@@ -224,5 +224,17 @@ internal static string Utf8GraphQLParser_Start_MaxAllowedFieldsReached { return ResourceManager.GetString("Utf8GraphQLParser_Start_MaxAllowedFieldsReached", resourceCulture); } } + + internal static string Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached { + get { + return ResourceManager.GetString("Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached", resourceCulture); + } + } + + internal static string Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached { + get { + return ResourceManager.GetString("Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached", resourceCulture); + } + } } }
src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx+6 −0 modified@@ -207,4 +207,10 @@ <data name="Utf8GraphQLParser_Start_MaxAllowedFieldsReached" xml:space="preserve"> <value>The GraphQL request document contains more than {0} fields. Parsing aborted.</value> </data> + <data name="Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached" xml:space="preserve"> + <value>A location in the GraphQL document contains more than {0} directives. Parsing aborted.</value> + </data> + <data name="Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached" xml:space="preserve"> + <value>Document exceeds the maximum allowed recursion depth of {0}. Parsing aborted.</value> + </data> </root>
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs+8 −0 modified@@ -10,10 +10,13 @@ public ref partial struct Utf8GraphQLParser private readonly bool _allowFragmentVars; private readonly int _maxAllowedNodes; private readonly int _maxAllowedFields; + private readonly int _maxAllowedDirectives; + private readonly int _maxAllowedRecursionDepth; private Utf8GraphQLReader _reader; private StringValueNode? _description; private int _parsedNodes; private int _parsedFields; + private int _recursionDepth; public Utf8GraphQLParser( ReadOnlySpan<byte> graphQLData, @@ -29,6 +32,8 @@ public Utf8GraphQLParser( _allowFragmentVars = options.Experimental.AllowFragmentVariables; _maxAllowedNodes = options.MaxAllowedNodes; _maxAllowedFields = options.MaxAllowedFields; + _maxAllowedDirectives = options.MaxAllowedDirectives; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = new Utf8GraphQLReader(graphQLData, options.MaxAllowedTokens); _description = null; } @@ -47,6 +52,8 @@ internal Utf8GraphQLParser( _allowFragmentVars = options.Experimental.AllowFragmentVariables; _maxAllowedNodes = options.MaxAllowedNodes; _maxAllowedFields = options.MaxAllowedFields; + _maxAllowedDirectives = options.MaxAllowedDirectives; + _maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth; _reader = reader; _description = null; } @@ -64,6 +71,7 @@ internal Utf8GraphQLParser( public DocumentNode Parse() { _parsedNodes = 0; + _recursionDepth = 0; var definitions = new List<IDefinitionNode>(); var start = Start();
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Directives.cs+11 −1 modified@@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using static HotChocolate.Language.Properties.LangUtf8Resources; namespace HotChocolate.Language; @@ -64,7 +65,7 @@ private NameNode ParseDirectiveLocation() throw Unexpected(kind); } - private List<DirectiveNode> ParseDirectives(bool isConstant) + private List<DirectiveNode> ParseDirectives(bool isConstant, bool isQueryLocation = false) { if (_reader.Kind == TokenKind.At) { @@ -73,6 +74,15 @@ private List<DirectiveNode> ParseDirectives(bool isConstant) while (_reader.Kind == TokenKind.At) { list.Add(ParseDirective(isConstant)); + + if (isQueryLocation && list.Count > _maxAllowedDirectives) + { + throw new SyntaxException( + _reader, + string.Format( + Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached, + _maxAllowedDirectives)); + } } return list;
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Fragments.cs+4 −4 modified@@ -53,7 +53,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() ParseVariableDefinitions(); ExpectOnKeyword(); var typeCondition = ParseNamedType(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start); @@ -72,7 +72,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() var name = ParseFragmentName(); ExpectOnKeyword(); var typeCondition = ParseNamedType(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start); @@ -99,7 +99,7 @@ private FragmentDefinitionNode ParseFragmentDefinition() private FragmentSpreadNode ParseFragmentSpread(in TokenInfo start) { var name = ParseFragmentName(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var location = CreateLocation(in start); return new FragmentSpreadNode @@ -125,7 +125,7 @@ private InlineFragmentNode ParseInlineFragment( in TokenInfo start, NamedTypeNode? typeCondition) { - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start);
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs+5 −3 modified@@ -21,7 +21,7 @@ private OperationDefinitionNode ParseOperationDefinition() var operation = ParseOperationType(); var name = _reader.Kind == TokenKind.Name ? ParseName() : null; var variableDefinitions = ParseVariableDefinitions(); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = ParseSelectionSet(); var location = CreateLocation(in start); @@ -127,7 +127,7 @@ private VariableDefinitionNode ParseVariableDefinition() ? ParseValueLiteral(true) : null; var directives = - ParseDirectives(isConstant: true); + ParseDirectives(isConstant: true, isQueryLocation: true); var location = CreateLocation(in start); @@ -163,6 +163,7 @@ private VariableNode ParseVariable() /// </summary> private SelectionSetNode ParseSelectionSet() { + IncreaseDepth(); var start = Start(); if (_reader.Kind != TokenKind.LeftBrace) @@ -191,6 +192,7 @@ private SelectionSetNode ParseSelectionSet() var location = CreateLocation(in start); + DecreaseDepth(); return new SelectionSetNode( location, selections); @@ -240,7 +242,7 @@ private FieldNode ParseField() } var arguments = ParseArguments(false); - var directives = ParseDirectives(false); + var directives = ParseDirectives(false, isQueryLocation: true); var selectionSet = _reader.Kind == TokenKind.LeftBrace ? ParseSelectionSet() : null;
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs+3 −0 modified@@ -12,6 +12,7 @@ public ref partial struct Utf8GraphQLParser /// </summary> private ITypeNode ParseTypeReference() { + IncreaseDepth(); ITypeNode type; Location? location; @@ -40,6 +41,7 @@ private ITypeNode ParseTypeReference() MoveNext(); location = CreateLocation(in start); + DecreaseDepth(); return new NonNullTypeNode ( location, @@ -50,6 +52,7 @@ private ITypeNode ParseTypeReference() Unexpected(TokenKind.Bang); } + DecreaseDepth(); return type; }
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs+19 −0 modified@@ -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() {
src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs+19 −14 modified@@ -29,32 +29,37 @@ public ref partial struct Utf8GraphQLParser /// </param> 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)]
src/HotChocolate/Language/test/Language.Tests/Parser/QueryParserTests.cs+203 −0 modified@@ -6,6 +6,209 @@ 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() + { + const int depth = 201; + var query = string.Concat(Enumerable.Repeat("{ a", depth)) + + string.Concat(Enumerable.Repeat(" }", depth)); + + Assert + .Throws<SyntaxException>(() => 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() + { + const int depth = 201; + var query = "{ a(x: " + + string.Concat(Enumerable.Repeat("{a: ", depth)) + + "1" + + string.Concat(Enumerable.Repeat("}", depth)) + + ") }"; + + Assert + .Throws<SyntaxException>(() => 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() + { + const int depth = 201; + var query = "{ a(x: " + + string.Concat(Enumerable.Repeat("[", depth)) + + "1" + + string.Concat(Enumerable.Repeat("]", depth)) + + ") }"; + + Assert + .Throws<SyntaxException>(() => 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() + { + const int depth = 201; + var query = $"query($v: {string.Concat(Enumerable.Repeat("[", depth))}Int{string.Concat(Enumerable.Repeat("]", depth))}) {{ a }}"; + + Assert + .Throws<SyntaxException>(() => 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() + { + 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( + noLocations: false, + allowFragmentVariables: false, + maxAllowedNodes: int.MaxValue, + maxAllowedTokens: int.MaxValue, + maxAllowedFields: 2048, + maxAllowedDirectives: 4, + maxAllowedRecursionDepth: 10); + const int depth = 11; + var query = string.Concat(Enumerable.Repeat("{ a", depth)) + + string.Concat(Enumerable.Repeat(" }", depth)); + + Assert + .Throws<SyntaxException>(() => 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( + noLocations: false, + allowFragmentVariables: false, + maxAllowedNodes: int.MaxValue, + maxAllowedTokens: int.MaxValue, + maxAllowedFields: 2048, + maxAllowedDirectives: 4, + 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) + { + var query = string.Concat(Enumerable.Repeat("{ a", depth)) + + string.Concat(Enumerable.Repeat(" }", depth)); + + Assert.Throws<SyntaxException>(() => Utf8GraphQLParser.Parse(query)); + } + + [Theory] + [InlineData(20_000)] + [InlineData(50_000)] + public void Reject_Attack_Payload_Nested_List_Values(int depth) + { + var query = "{ a(x: " + + string.Concat(Enumerable.Repeat("[", depth)) + + "1" + + string.Concat(Enumerable.Repeat("]", depth)) + + ") }"; + + Assert.Throws<SyntaxException>(() => Utf8GraphQLParser.Parse(query)); + } + + [Fact] + public void Default_MaxAllowedDirectives_Is_4() + { + Assert.Equal(4, ParserOptions.Default.MaxAllowedDirectives); + } + + [Fact] + public void Reject_Fields_Exceeding_Max_Allowed_Directives_Per_Location() + { + Assert + .Throws<SyntaxException>(() => Utf8GraphQLParser.Parse("{ a @d @d @d @d @d }")) + .Message + .MatchInlineSnapshot( + "A location in the GraphQL document contains more than 4 directives. Parsing aborted."); + } + + [Fact] + public void Allow_Fields_Within_Max_Allowed_Directives_Per_Location() + { + Utf8GraphQLParser.Parse("{ a @d @d @d @d }"); + } + + [Fact] + public void Reject_Fields_Exceeding_Custom_Directive_Limit() + { + var options = new ParserOptions( + noLocations: false, + allowFragmentVariables: false, + maxAllowedNodes: int.MaxValue, + maxAllowedTokens: int.MaxValue, + maxAllowedFields: 2048, + maxAllowedDirectives: 2, + maxAllowedRecursionDepth: 200); + + Assert + .Throws<SyntaxException>(() => Utf8GraphQLParser.Parse("{ a @d @d @d }", options)) + .Message + .MatchInlineSnapshot( + "A location in the GraphQL document contains more than 2 directives. Parsing aborted."); + } + + [Fact] + public void Allow_Fields_Within_Custom_Directive_Limit() + { + var options = new ParserOptions( + noLocations: false, + allowFragmentVariables: false, + maxAllowedNodes: int.MaxValue, + maxAllowedTokens: int.MaxValue, + maxAllowedFields: 2048, + maxAllowedDirectives: 2, + maxAllowedRecursionDepth: 200); + Utf8GraphQLParser.Parse("{ a @d @d }", options); + } + [Fact] public void Reject_Queries_With_More_Than_2048_Fields() {;
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
14- github.com/advisories/GHSA-qr3m-xw4c-jqw3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-40324ghsaADVISORY
- github.com/ChilliCream/graphql-platform/commit/08c0caa42ca33c121bbed49d2db892e5bf6fb541nvdWEB
- github.com/ChilliCream/graphql-platform/commit/4cbaf67d366f800fc1e484bc5c06dfcf27b45023nvdWEB
- github.com/ChilliCream/graphql-platform/commit/b185eb276c9ee227bd44616ff113be7f01a66c69nvdWEB
- github.com/ChilliCream/graphql-platform/commit/b9271e6a500484c002fd528dcd34d1a9b445480fnvdWEB
- github.com/ChilliCream/graphql-platform/pull/9528nvdWEB
- github.com/ChilliCream/graphql-platform/pull/9530nvdWEB
- github.com/ChilliCream/graphql-platform/pull/9531nvdWEB
- github.com/ChilliCream/graphql-platform/releases/tag/12.22.7nvdWEB
- github.com/ChilliCream/graphql-platform/releases/tag/13.9.16nvdWEB
- github.com/ChilliCream/graphql-platform/releases/tag/14.3.1nvdWEB
- github.com/ChilliCream/graphql-platform/releases/tag/15.1.14nvdWEB
- github.com/ChilliCream/graphql-platform/security/advisories/GHSA-qr3m-xw4c-jqw3nvdWEB
News mentions
0No linked articles in our index yet.