Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
57 changes: 57 additions & 0 deletions src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using static HotChocolate.Language.Properties.LangUtf8Resources;

namespace HotChocolate.Language;

Expand Down Expand Up @@ -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)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -163,6 +163,7 @@ private VariableNode ParseVariable()
/// </summary>
private SelectionSetNode ParseSelectionSet()
{
IncreaseDepth();
var start = Start();

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

var location = CreateLocation(in start);

DecreaseDepth();
return new SelectionSetNode(
location,
selections);
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public ref partial struct Utf8GraphQLParser
/// </summary>
private ITypeNode ParseTypeReference()
{
IncreaseDepth();
ITypeNode type;
Location? location;

Expand Down Expand Up @@ -40,6 +41,7 @@ private ITypeNode ParseTypeReference()
MoveNext();
location = CreateLocation(in start);

DecreaseDepth();
return new NonNullTypeNode
(
location,
Expand All @@ -50,6 +52,7 @@ private ITypeNode ParseTypeReference()
Unexpected(TokenKind.Bang);
}

DecreaseDepth();
return type;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -64,6 +71,7 @@ internal Utf8GraphQLParser(
public DocumentNode Parse()
{
_parsedNodes = 0;
_recursionDepth = 0;
var definitions = new List<IDefinitionNode>();

var start = Start();
Expand Down
Loading
Loading