Skip to content

Commit 4cbaf67

Browse files
authored
Add depth limit to GraphQL parser (#9528)
1 parent 7f8b04d commit 4cbaf67

13 files changed

Lines changed: 364 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
@@ -94,9 +94,12 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services
9494

9595
return new ParserOptions(
9696
noLocations: !options.IncludeLocations,
97+
allowFragmentVariables: false,
9798
maxAllowedNodes: options.MaxAllowedNodes,
9899
maxAllowedTokens: options.MaxAllowedTokens,
99-
maxAllowedFields: options.MaxAllowedFields);
100+
maxAllowedFields: options.MaxAllowedFields,
101+
maxAllowedDirectives: options.MaxAllowedDirectives,
102+
maxAllowedRecursionDepth: options.MaxAllowedRecursionDepth);
100103
});
101104

102105
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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,51 @@ public ParserOptions(
4545
MaxAllowedTokens = maxAllowedTokens;
4646
MaxAllowedNodes = maxAllowedNodes;
4747
MaxAllowedFields = maxAllowedFields;
48+
MaxAllowedDirectives = 4;
49+
MaxAllowedRecursionDepth = 200;
50+
}
51+
52+
/// <summary>
53+
/// Initializes a new instance of <see cref="ParserOptions"/> with security limits.
54+
/// </summary>
55+
/// <param name="noLocations">
56+
/// Defines that the parse shall not preserve syntax node locations.
57+
/// </param>
58+
/// <param name="allowFragmentVariables">
59+
/// Defines that the parser shall parse fragment variables.
60+
/// </param>
61+
/// <param name="maxAllowedNodes">
62+
/// The maximum number of nodes allowed within a document.
63+
/// </param>
64+
/// <param name="maxAllowedTokens">
65+
/// The maximum number of tokens allowed within a document.
66+
/// </param>
67+
/// <param name="maxAllowedFields">
68+
/// The maximum number of fields allowed within a query document.
69+
/// </param>
70+
/// <param name="maxAllowedDirectives">
71+
/// The maximum number of directives allowed per location (e.g. per field,
72+
/// per operation, per fragment definition).
73+
/// </param>
74+
/// <param name="maxAllowedRecursionDepth">
75+
/// The maximum allowed recursion depth of a parsed document.
76+
/// </param>
77+
public ParserOptions(
78+
bool noLocations,
79+
bool allowFragmentVariables,
80+
int maxAllowedNodes,
81+
int maxAllowedTokens,
82+
int maxAllowedFields,
83+
int maxAllowedDirectives,
84+
int maxAllowedRecursionDepth)
85+
{
86+
NoLocations = noLocations;
87+
Experimental = new(allowFragmentVariables);
88+
MaxAllowedTokens = maxAllowedTokens;
89+
MaxAllowedNodes = maxAllowedNodes;
90+
MaxAllowedFields = maxAllowedFields;
91+
MaxAllowedDirectives = maxAllowedDirectives;
92+
MaxAllowedRecursionDepth = maxAllowedRecursionDepth;
4893
}
4994

5095
/// <summary>
@@ -86,6 +131,18 @@ public ParserOptions(
86131
/// </summary>
87132
public int MaxAllowedFields { get; }
88133

134+
/// <summary>
135+
/// The maximum number of directives allowed per location (e.g. per field,
136+
/// per operation, per fragment definition). Repeatable directives can be used
137+
/// to exhaust CPU and memory resources if not limited.
138+
/// </summary>
139+
public int MaxAllowedDirectives { get; }
140+
141+
/// <summary>
142+
/// Gets the maximum allowed recursion depth of a parsed document.
143+
/// </summary>
144+
public int MaxAllowedRecursionDepth { get; }
145+
89146
/// <summary>
90147
/// Gets the experimental parser options
91148
/// 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
@@ -207,4 +207,10 @@
207207
<data name="Utf8GraphQLParser_Start_MaxAllowedFieldsReached" xml:space="preserve">
208208
<value>The GraphQL request document contains more than {0} fields. Parsing aborted.</value>
209209
</data>
210+
<data name="Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached" xml:space="preserve">
211+
<value>A location in the GraphQL document contains more than {0} directives. Parsing aborted.</value>
212+
</data>
213+
<data name="Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached" xml:space="preserve">
214+
<value>Document exceeds the maximum allowed recursion depth of {0}. Parsing aborted.</value>
215+
</data>
210216
</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,4 +1,5 @@
11
using System.Runtime.CompilerServices;
2+
using static HotChocolate.Language.Properties.LangUtf8Resources;
23

34
namespace HotChocolate.Language;
45

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

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

7888
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
@@ -53,7 +53,7 @@ private FragmentDefinitionNode ParseFragmentDefinition()
5353
ParseVariableDefinitions();
5454
ExpectOnKeyword();
5555
var typeCondition = ParseNamedType();
56-
var directives = ParseDirectives(false);
56+
var directives = ParseDirectives(false, isQueryLocation: true);
5757
var selectionSet = ParseSelectionSet();
5858
var location = CreateLocation(in start);
5959

@@ -72,7 +72,7 @@ private FragmentDefinitionNode ParseFragmentDefinition()
7272
var name = ParseFragmentName();
7373
ExpectOnKeyword();
7474
var typeCondition = ParseNamedType();
75-
var directives = ParseDirectives(false);
75+
var directives = ParseDirectives(false, isQueryLocation: true);
7676
var selectionSet = ParseSelectionSet();
7777
var location = CreateLocation(in start);
7878

@@ -99,7 +99,7 @@ private FragmentDefinitionNode ParseFragmentDefinition()
9999
private FragmentSpreadNode ParseFragmentSpread(in TokenInfo start)
100100
{
101101
var name = ParseFragmentName();
102-
var directives = ParseDirectives(false);
102+
var directives = ParseDirectives(false, isQueryLocation: true);
103103
var location = CreateLocation(in start);
104104

105105
return new FragmentSpreadNode
@@ -125,7 +125,7 @@ private InlineFragmentNode ParseInlineFragment(
125125
in TokenInfo start,
126126
NamedTypeNode? typeCondition)
127127
{
128-
var directives = ParseDirectives(false);
128+
var directives = ParseDirectives(false, isQueryLocation: true);
129129
var selectionSet = ParseSelectionSet();
130130
var location = CreateLocation(in start);
131131

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

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

@@ -127,7 +127,7 @@ private VariableDefinitionNode ParseVariableDefinition()
127127
? ParseValueLiteral(true)
128128
: null;
129129
var directives =
130-
ParseDirectives(isConstant: true);
130+
ParseDirectives(isConstant: true, isQueryLocation: true);
131131

132132
var location = CreateLocation(in start);
133133

@@ -163,6 +163,7 @@ private VariableNode ParseVariable()
163163
/// </summary>
164164
private SelectionSetNode ParseSelectionSet()
165165
{
166+
IncreaseDepth();
166167
var start = Start();
167168

168169
if (_reader.Kind != TokenKind.LeftBrace)
@@ -191,6 +192,7 @@ private SelectionSetNode ParseSelectionSet()
191192

192193
var location = CreateLocation(in start);
193194

195+
DecreaseDepth();
194196
return new SelectionSetNode(
195197
location,
196198
selections);
@@ -240,7 +242,7 @@ private FieldNode ParseField()
240242
}
241243

242244
var arguments = ParseArguments(false);
243-
var directives = ParseDirectives(false);
245+
var directives = ParseDirectives(false, isQueryLocation: true);
244246
var selectionSet = _reader.Kind == TokenKind.LeftBrace
245247
? ParseSelectionSet()
246248
: 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
@@ -24,6 +24,25 @@ private NameNode ParseName()
2424
[MethodImpl(MethodImplOptions.AggressiveInlining)]
2525
private bool MoveNext() => _reader.MoveNext();
2626

27+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
28+
private void IncreaseDepth()
29+
{
30+
if (++_recursionDepth > _maxAllowedRecursionDepth)
31+
{
32+
throw new SyntaxException(
33+
_reader,
34+
string.Format(
35+
Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached,
36+
_maxAllowedRecursionDepth));
37+
}
38+
}
39+
40+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
41+
private void DecreaseDepth()
42+
{
43+
--_recursionDepth;
44+
}
45+
2746
[MethodImpl(MethodImplOptions.AggressiveInlining)]
2847
private TokenInfo Start()
2948
{

0 commit comments

Comments
 (0)