Skip to content

Commit 08c0caa

Browse files
authored
Add depth limit to GraphQL parser (#9531)
1 parent e1b4824 commit 08c0caa

12 files changed

Lines changed: 139 additions & 23 deletions

src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services
9191

9292
return new ParserOptions(
9393
noLocations: !options.IncludeLocations,
94+
allowFragmentVariables: false,
9495
maxAllowedNodes: options.MaxAllowedNodes,
9596
maxAllowedTokens: options.MaxAllowedTokens,
96-
maxAllowedFields: options.MaxAllowedFields);
97+
maxAllowedFields: options.MaxAllowedFields,
98+
maxAllowedDirectives: options.MaxAllowedDirectives,
99+
maxAllowedRecursionDepth: options.MaxAllowedRecursionDepth);
97100
});
98101

99102
return services;

src/HotChocolate/Core/src/Execution/Options/RequestParserOptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,17 @@ public sealed class RequestParserOptions
5050
/// as fields is an easier way to estimate query size for GraphQL requests.
5151
/// </summary>
5252
public int MaxAllowedFields { get; set; } = 2048;
53+
54+
/// <summary>
55+
/// The maximum number of directives allowed per location (e.g. per field,
56+
/// per operation, per fragment definition). Repeatable directives can be used
57+
/// to exhaust CPU and memory resources if not limited.
58+
/// </summary>
59+
public int MaxAllowedDirectives { get; set; } = 4;
60+
61+
/// <summary>
62+
/// The maximum allowed recursion depth when parsing a document.
63+
/// This prevents stack overflow from deeply nested queries.
64+
/// </summary>
65+
public int MaxAllowedRecursionDepth { get; set; } = 200;
5366
}

src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,29 @@ public ParserOptions(
4242
MaxAllowedTokens = maxAllowedTokens;
4343
MaxAllowedNodes = maxAllowedNodes;
4444
MaxAllowedFields = maxAllowedFields;
45+
MaxAllowedDirectives = 4;
46+
MaxAllowedRecursionDepth = 200;
47+
}
48+
49+
/// <summary>
50+
/// Initializes a new instance of <see cref="ParserOptions"/> with security limits.
51+
/// </summary>
52+
public ParserOptions(
53+
bool noLocations,
54+
bool allowFragmentVariables,
55+
int maxAllowedNodes,
56+
int maxAllowedTokens,
57+
int maxAllowedFields,
58+
int maxAllowedDirectives,
59+
int maxAllowedRecursionDepth)
60+
{
61+
NoLocations = noLocations;
62+
Experimental = new(allowFragmentVariables);
63+
MaxAllowedTokens = maxAllowedTokens;
64+
MaxAllowedNodes = maxAllowedNodes;
65+
MaxAllowedFields = maxAllowedFields;
66+
MaxAllowedDirectives = maxAllowedDirectives;
67+
MaxAllowedRecursionDepth = maxAllowedRecursionDepth;
4568
}
4669

4770
/// <summary>
@@ -83,6 +106,18 @@ public ParserOptions(
83106
/// </summary>
84107
public int MaxAllowedFields { get; }
85108

109+
/// <summary>
110+
/// The maximum number of directives allowed per location (e.g. per field,
111+
/// per operation, per fragment definition). Repeatable directives can be used
112+
/// to exhaust CPU and memory resources if not limited.
113+
/// </summary>
114+
public int MaxAllowedDirectives { get; }
115+
116+
/// <summary>
117+
/// Gets the maximum allowed recursion depth of a parsed document.
118+
/// </summary>
119+
public int MaxAllowedRecursionDepth { get; }
120+
86121
/// <summary>
87122
/// Gets the experimental parser options
88123
/// which are by default switched of.

src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,10 @@
204204
<data name="Utf8GraphQLParser_Start_MaxAllowedFieldsReached" xml:space="preserve">
205205
<value>The GraphQL request document contains more than {0} fields. Parsing aborted.</value>
206206
</data>
207+
<data name="Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached" xml:space="preserve">
208+
<value>A location in the GraphQL document contains more than {0} directives. Parsing aborted.</value>
209+
</data>
210+
<data name="Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached" xml:space="preserve">
211+
<value>Document exceeds the maximum allowed recursion depth of {0}. Parsing aborted.</value>
212+
</data>
207213
</root>

src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Directives.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.Runtime.CompilerServices;
3+
using static HotChocolate.Language.Properties.LangUtf8Resources;
34

45
namespace HotChocolate.Language;
56

@@ -65,7 +66,7 @@ private NameNode ParseDirectiveLocation()
6566
throw Unexpected(kind);
6667
}
6768

68-
private List<DirectiveNode> ParseDirectives(bool isConstant)
69+
private List<DirectiveNode> ParseDirectives(bool isConstant, bool isQueryLocation = false)
6970
{
7071
if (_reader.Kind == TokenKind.At)
7172
{
@@ -74,6 +75,15 @@ private List<DirectiveNode> ParseDirectives(bool isConstant)
7475
while (_reader.Kind == TokenKind.At)
7576
{
7677
list.Add(ParseDirective(isConstant));
78+
79+
if (isQueryLocation && list.Count > _maxAllowedDirectives)
80+
{
81+
throw new SyntaxException(
82+
_reader,
83+
string.Format(
84+
Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached,
85+
_maxAllowedDirectives));
86+
}
7787
}
7888

7989
return list;

src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Fragments.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ private FragmentDefinitionNode ParseFragmentDefinition()
5555
ParseVariableDefinitions();
5656
ExpectOnKeyword();
5757
var typeCondition = ParseNamedType();
58-
var directives = ParseDirectives(false);
58+
var directives = ParseDirectives(false, isQueryLocation: true);
5959
var selectionSet = ParseSelectionSet();
6060
var location = CreateLocation(in start);
6161

@@ -74,7 +74,7 @@ private FragmentDefinitionNode ParseFragmentDefinition()
7474
var name = ParseFragmentName();
7575
ExpectOnKeyword();
7676
var typeCondition = ParseNamedType();
77-
var directives = ParseDirectives(false);
77+
var directives = ParseDirectives(false, isQueryLocation: true);
7878
var selectionSet = ParseSelectionSet();
7979
var location = CreateLocation(in start);
8080

@@ -101,7 +101,7 @@ private FragmentDefinitionNode ParseFragmentDefinition()
101101
private FragmentSpreadNode ParseFragmentSpread(in TokenInfo start)
102102
{
103103
var name = ParseFragmentName();
104-
var directives = ParseDirectives(false);
104+
var directives = ParseDirectives(false, isQueryLocation: true);
105105
var location = CreateLocation(in start);
106106

107107
return new FragmentSpreadNode
@@ -127,7 +127,7 @@ private InlineFragmentNode ParseInlineFragment(
127127
in TokenInfo start,
128128
NamedTypeNode? typeCondition)
129129
{
130-
var directives = ParseDirectives(false);
130+
var directives = ParseDirectives(false, isQueryLocation: true);
131131
var selectionSet = ParseSelectionSet();
132132
var location = CreateLocation(in start);
133133

src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ private OperationDefinitionNode ParseOperationDefinition()
2323
var operation = ParseOperationType();
2424
var name = _reader.Kind == TokenKind.Name ? ParseName() : null;
2525
var variableDefinitions = ParseVariableDefinitions();
26-
var directives = ParseDirectives(false);
26+
var directives = ParseDirectives(false, isQueryLocation: true);
2727
var selectionSet = ParseSelectionSet();
2828
var location = CreateLocation(in start);
2929

@@ -133,7 +133,7 @@ private VariableDefinitionNode ParseVariableDefinition()
133133
? ParseValueLiteral(true)
134134
: null;
135135
var directives =
136-
ParseDirectives(true);
136+
ParseDirectives(isConstant: true, isQueryLocation: true);
137137

138138
var location = CreateLocation(in start);
139139

@@ -173,6 +173,7 @@ private VariableNode ParseVariable()
173173
/// </summary>
174174
private SelectionSetNode ParseSelectionSet()
175175
{
176+
IncreaseDepth();
176177
var start = Start();
177178

178179
if (_reader.Kind != TokenKind.LeftBrace)
@@ -200,6 +201,7 @@ private SelectionSetNode ParseSelectionSet()
200201

201202
var location = CreateLocation(in start);
202203

204+
DecreaseDepth();
203205
return new SelectionSetNode
204206
(
205207
location,
@@ -252,7 +254,7 @@ private FieldNode ParseField()
252254

253255
var arguments = ParseArguments(false);
254256
var required = ParseRequiredStatus();
255-
var directives = ParseDirectives(false);
257+
var directives = ParseDirectives(false, isQueryLocation: true);
256258
var selectionSet = _reader.Kind == TokenKind.LeftBrace
257259
? ParseSelectionSet()
258260
: null;

src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public ref partial struct Utf8GraphQLParser
1212
/// </summary>
1313
private ITypeNode ParseTypeReference()
1414
{
15+
IncreaseDepth();
1516
ITypeNode type;
1617
Location? location;
1718

@@ -40,6 +41,7 @@ private ITypeNode ParseTypeReference()
4041
MoveNext();
4142
location = CreateLocation(in start);
4243

44+
DecreaseDepth();
4345
return new NonNullTypeNode
4446
(
4547
location,
@@ -50,6 +52,7 @@ private ITypeNode ParseTypeReference()
5052
Unexpected(TokenKind.Bang);
5153
}
5254

55+
DecreaseDepth();
5356
return type;
5457
}
5558

src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,25 @@ internal NameNode ParseName()
2727
[MethodImpl(MethodImplOptions.AggressiveInlining)]
2828
internal bool MoveNext() => _reader.MoveNext();
2929

30+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
31+
private void IncreaseDepth()
32+
{
33+
if (++_recursionDepth > _maxAllowedRecursionDepth)
34+
{
35+
throw new SyntaxException(
36+
_reader,
37+
string.Format(
38+
Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached,
39+
_maxAllowedRecursionDepth));
40+
}
41+
}
42+
43+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
44+
private void DecreaseDepth()
45+
{
46+
--_recursionDepth;
47+
}
48+
3049
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3150
private TokenInfo Start()
3251
{

0 commit comments

Comments
 (0)