Skip to content

Commit b9271e6

Browse files
committed
Add depth limit to GraphQL parser
1 parent 386abb3 commit b9271e6

10 files changed

Lines changed: 116 additions & 22 deletions

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,37 @@ public ParserOptions(
2121
NoLocations = noLocations;
2222
Experimental = new ParserOptionsExperimental(
2323
allowFragmentVariables);
24+
MaxAllowedDirectives = 4;
25+
MaxAllowedRecursionDepth = 200;
2426
}
2527

28+
/// <summary>
29+
/// Initializes a new instance of <see cref="ParserOptions"/> with security limits.
30+
/// </summary>
31+
public ParserOptions(
32+
bool noLocations,
33+
bool allowFragmentVariables,
34+
int maxAllowedDirectives,
35+
int maxAllowedRecursionDepth)
36+
{
37+
NoLocations = noLocations;
38+
Experimental = new ParserOptionsExperimental(allowFragmentVariables);
39+
MaxAllowedDirectives = maxAllowedDirectives;
40+
MaxAllowedRecursionDepth = maxAllowedRecursionDepth;
41+
}
42+
43+
/// <summary>
44+
/// The maximum number of directives allowed per location (e.g. per field,
45+
/// per operation, per fragment definition). Repeatable directives can be used
46+
/// to exhaust CPU and memory resources if not limited.
47+
/// </summary>
48+
public int MaxAllowedDirectives { get; }
49+
50+
/// <summary>
51+
/// Gets the maximum allowed recursion depth of a parsed document.
52+
/// </summary>
53+
public int MaxAllowedRecursionDepth { get; }
54+
2655
/// <summary>
2756
/// By default, the parser creates <see cref="ISyntaxNode" />s
2857
/// that know the location in the source that they correspond to.

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
@@ -75,4 +75,10 @@
7575
<data name="UnexpectedToken" xml:space="preserve">
7676
<value>Unexpected token: {0}.</value>
7777
</data>
78+
<data name="Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached" xml:space="preserve">
79+
<value>A location in the GraphQL document contains more than {0} directives. Parsing aborted.</value>
80+
</data>
81+
<data name="Utf8GraphQLParser_Start_MaxAllowedRecursionDepthReached" xml:space="preserve">
82+
<value>Document exceeds the maximum allowed recursion depth of {0}. Parsing aborted.</value>
83+
</data>
7884
</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

@@ -67,7 +68,7 @@ private NameNode ParseDirectiveLocation()
6768
throw Unexpected(kind);
6869
}
6970

70-
private List<DirectiveNode> ParseDirectives(bool isConstant)
71+
private List<DirectiveNode> ParseDirectives(bool isConstant, bool isQueryLocation = false)
7172
{
7273
if (_reader.Kind == TokenKind.At)
7374
{
@@ -76,6 +77,15 @@ private List<DirectiveNode> ParseDirectives(bool isConstant)
7677
while (_reader.Kind == TokenKind.At)
7778
{
7879
list.Add(ParseDirective(isConstant));
80+
81+
if (isQueryLocation && list.Count > _maxAllowedDirectives)
82+
{
83+
throw new SyntaxException(
84+
_reader,
85+
string.Format(
86+
Utf8GraphQLParser_ParseDirective_MaxAllowedDirectivesReached,
87+
_maxAllowedDirectives));
88+
}
7989
}
8090

8191
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
@@ -58,7 +58,7 @@ private FragmentDefinitionNode ParseFragmentDefinition()
5858
ParseVariableDefinitions();
5959
ExpectOnKeyword();
6060
NamedTypeNode typeCondition = ParseNamedType();
61-
List<DirectiveNode> directives = ParseDirectives(false);
61+
List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true);
6262
SelectionSetNode selectionSet = ParseSelectionSet();
6363
Location? location = CreateLocation(in start);
6464

@@ -77,7 +77,7 @@ private FragmentDefinitionNode ParseFragmentDefinition()
7777
NameNode name = ParseFragmentName();
7878
ExpectOnKeyword();
7979
NamedTypeNode typeCondition = ParseNamedType();
80-
List<DirectiveNode> directives = ParseDirectives(false);
80+
List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true);
8181
SelectionSetNode selectionSet = ParseSelectionSet();
8282
Location? location = CreateLocation(in start);
8383

@@ -104,7 +104,7 @@ private FragmentDefinitionNode ParseFragmentDefinition()
104104
private FragmentSpreadNode ParseFragmentSpread(in TokenInfo start)
105105
{
106106
NameNode name = ParseFragmentName();
107-
List<DirectiveNode> directives = ParseDirectives(false);
107+
List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true);
108108
Location? location = CreateLocation(in start);
109109

110110
return new FragmentSpreadNode
@@ -130,7 +130,7 @@ private InlineFragmentNode ParseInlineFragment(
130130
in TokenInfo start,
131131
NamedTypeNode? typeCondition)
132132
{
133-
List<DirectiveNode> directives = ParseDirectives(false);
133+
List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true);
134134
SelectionSetNode selectionSet = ParseSelectionSet();
135135
Location? location = CreateLocation(in start);
136136

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ private OperationDefinitionNode ParseOperationDefinition()
2525
OperationType operation = ParseOperationType();
2626
NameNode? name = _reader.Kind == TokenKind.Name ? ParseName() : null;
2727
List<VariableDefinitionNode> variableDefinitions = ParseVariableDefinitions();
28-
List<DirectiveNode> directives = ParseDirectives(false);
28+
List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true);
2929
SelectionSetNode selectionSet = ParseSelectionSet();
3030
Location? location = CreateLocation(in start);
3131

@@ -139,7 +139,7 @@ private VariableDefinitionNode ParseVariableDefinition()
139139
? ParseValueLiteral(true)
140140
: null;
141141
List<DirectiveNode> directives =
142-
ParseDirectives(true);
142+
ParseDirectives(true, isQueryLocation: true);
143143

144144
Location? location = CreateLocation(in start);
145145

@@ -181,6 +181,7 @@ private VariableNode ParseVariable()
181181
[MethodImpl(MethodImplOptions.AggressiveInlining)]
182182
private SelectionSetNode ParseSelectionSet()
183183
{
184+
IncreaseDepth();
184185
TokenInfo start = Start();
185186

186187
if (_reader.Kind != TokenKind.LeftBrace)
@@ -208,6 +209,7 @@ private SelectionSetNode ParseSelectionSet()
208209

209210
Location? location = CreateLocation(in start);
210211

212+
DecreaseDepth();
211213
return new SelectionSetNode
212214
(
213215
location,
@@ -253,7 +255,7 @@ private FieldNode ParseField()
253255

254256
List<ArgumentNode> arguments = ParseArguments(false);
255257
INullabilityNode? required = ParseRequiredStatus();
256-
List<DirectiveNode> directives = ParseDirectives(false);
258+
List<DirectiveNode> directives = ParseDirectives(false, isQueryLocation: true);
257259
SelectionSetNode? selectionSet = _reader.Kind == TokenKind.LeftBrace
258260
? ParseSelectionSet()
259261
: 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
_createLocation

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

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,32 +31,37 @@ public ref partial struct Utf8GraphQLParser
3131
/// </param>
3232
internal IValueNode ParseValueLiteral(bool isConstant)
3333
{
34+
IncreaseDepth();
35+
36+
IValueNode node;
37+
3438
if (_reader.Kind == TokenKind.LeftBracket)
3539
{
36-
return ParseList(isConstant);
40+
node = ParseList(isConstant);
3741
}
38-
39-
if (_reader.Kind == TokenKind.LeftBrace)
42+
else if (_reader.Kind == TokenKind.LeftBrace)
4043
{
41-
return ParseObject(isConstant);
44+
node = ParseObject(isConstant);
4245
}
43-
44-
if (TokenHelper.IsScalarValue(in _reader))
46+
else if (TokenHelper.IsScalarValue(in _reader))
4547
{
46-
return ParseScalarValue();
48+
node = ParseScalarValue();
4749
}
48-
49-
if (_reader.Kind == TokenKind.Name)
50+
else if (_reader.Kind == TokenKind.Name)
5051
{
51-
return ParseEnumValue();
52+
node = ParseEnumValue();
5253
}
53-
54-
if (_reader.Kind == TokenKind.Dollar && !isConstant)
54+
else if (_reader.Kind == TokenKind.Dollar && !isConstant)
55+
{
56+
node = ParseVariable();
57+
}
58+
else
5559
{
56-
return ParseVariable();
60+
throw Unexpected(_reader.Kind);
5761
}
5862

59-
throw Unexpected(_reader.Kind);
63+
DecreaseDepth();
64+
return node;
6065
}
6166

6267
[MethodImpl(MethodImplOptions.AggressiveInlining)]

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ public ref partial struct Utf8GraphQLParser
99
{
1010
private readonly bool _createLocation;
1111
private readonly bool _allowFragmentVars;
12+
private readonly int _maxAllowedDirectives;
13+
private readonly int _maxAllowedRecursionDepth;
1214
private Utf8GraphQLReader _reader;
1315
private StringValueNode? _description;
16+
private int _recursionDepth;
1417

1518
public Utf8GraphQLParser(
1619
ReadOnlySpan<byte> graphQLData,
@@ -24,6 +27,8 @@ public Utf8GraphQLParser(
2427
options ??= ParserOptions.Default;
2528
_createLocation = !options.NoLocations;
2629
_allowFragmentVars = options.Experimental.AllowFragmentVariables;
30+
_maxAllowedDirectives = options.MaxAllowedDirectives;
31+
_maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth;
2732
_reader = new Utf8GraphQLReader(graphQLData);
2833
_description = null;
2934
}
@@ -40,13 +45,16 @@ internal Utf8GraphQLParser(
4045
options ??= ParserOptions.Default;
4146
_createLocation = !options.NoLocations;
4247
_allowFragmentVars = options.Experimental.AllowFragmentVariables;
48+
_maxAllowedDirectives = options.MaxAllowedDirectives;
49+
_maxAllowedRecursionDepth = options.MaxAllowedRecursionDepth;
4350
_reader = reader;
4451
_description = null;
4552
}
4653

4754
public DocumentNode Parse()
4855
{
4956
var definitions = new List<IDefinitionNode>();
57+
_recursionDepth = 0;
5058

5159
TokenInfo start = Start();
5260

0 commit comments

Comments
 (0)