diff --git a/.build/Build.csproj b/.build/Build.csproj index 8e7cc9e2f82..fc0931ecdcc 100644 --- a/.build/Build.csproj +++ b/.build/Build.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 CS0649;CS0169 .. diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs index 6ef1a1e6f2b..7c391caa525 100644 --- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs +++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs @@ -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; diff --git a/src/HotChocolate/Core/src/Execution/Options/RequestParserOptions.cs b/src/HotChocolate/Core/src/Execution/Options/RequestParserOptions.cs index bdaf9e0aaa0..a243b6dcdb1 100644 --- a/src/HotChocolate/Core/src/Execution/Options/RequestParserOptions.cs +++ b/src/HotChocolate/Core/src/Execution/Options/RequestParserOptions.cs @@ -50,4 +50,17 @@ public sealed class RequestParserOptions /// as fields is an easier way to estimate query size for GraphQL requests. /// public int MaxAllowedFields { get; set; } = 2048; + + /// + /// 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. + /// + public int MaxAllowedDirectives { get; set; } = 4; + + /// + /// The maximum allowed recursion depth when parsing a document. + /// This prevents stack overflow from deeply nested queries. + /// + public int MaxAllowedRecursionDepth { get; set; } = 200; } diff --git a/src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs b/src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs index 1c5ebac76d5..32e639deecb 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/ParserOptions.cs @@ -45,6 +45,50 @@ public ParserOptions( MaxAllowedTokens = maxAllowedTokens; MaxAllowedNodes = maxAllowedNodes; MaxAllowedFields = maxAllowedFields; + MaxAllowedDirectives = 4; + MaxAllowedRecursionDepth = 200; + } + + /// + /// Initializes a new instance of . + /// + /// + /// Defines that the parse shall not preserve syntax node locations. + /// + /// + /// Defines that the parser shall parse fragment variables. + /// + /// + /// The maximum number of nodes allowed within a document. + /// + /// + /// The maximum number of tokens allowed within a document. + /// + /// + /// The maximum number of fields allowed within a query document. + /// + /// + /// The maximum number of directives allowed per location. + /// + /// + /// The maximum allowed recursion depth of a parsed document. + /// + 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; } /// @@ -86,6 +130,18 @@ public ParserOptions( /// public int MaxAllowedFields { get; } + /// + /// 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. + /// + public int MaxAllowedDirectives { get; } + + /// + /// Gets the maximum allowed recursion depth of a parsed document. + /// + public int MaxAllowedRecursionDepth { get; } + /// /// Gets the experimental parser options /// which are by default switched of. diff --git a/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs b/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs index 52181d1bd86..a59b05d96e3 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.Designer.cs @@ -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); + } + } } } diff --git a/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx b/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx index feaa997ae7e..464706cef98 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx +++ b/src/HotChocolate/Language/src/Language.Utf8/Properties/LangUtf8Resources.resx @@ -207,4 +207,10 @@ The GraphQL request document contains more than {0} fields. Parsing aborted. + + A location in the GraphQL document contains more than {0} directives. Parsing aborted. + + + Document exceeds the maximum allowed recursion depth of {0}. Parsing aborted. + diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Directives.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Directives.cs index 196b973ae3e..a3391286d6e 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Directives.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Directives.cs @@ -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 ParseDirectives(bool isConstant) + private List ParseDirectives(bool isConstant, bool isQueryLocation = false) { if (_reader.Kind == TokenKind.At) { @@ -73,6 +74,15 @@ private List 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; diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Fragments.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Fragments.cs index bde32c0890a..6dfa9900c13 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Fragments.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Fragments.cs @@ -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); diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs index a82e452346c..882a106ebb9 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Operations.cs @@ -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() /// 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; diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs index 854e8da30b4..2d0b3da063a 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Types.cs @@ -12,6 +12,7 @@ public ref partial struct Utf8GraphQLParser /// 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; } diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs index a7be817cc30..134ffe74639 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Utilities.cs @@ -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() { diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs index 7ba6b89645a..c1400e282cf 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.Values.cs @@ -29,32 +29,37 @@ public ref partial struct Utf8GraphQLParser /// 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)] diff --git a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs index 504a03647aa..99b792e059a 100644 --- a/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs +++ b/src/HotChocolate/Language/src/Language.Utf8/Utf8GraphQLParser.cs @@ -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 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(); var start = Start(); diff --git a/src/HotChocolate/Primitives/test/Directory.Build.props b/src/HotChocolate/Primitives/test/Directory.Build.props index 59f7f79cf82..f881c8cf0f5 100644 --- a/src/HotChocolate/Primitives/test/Directory.Build.props +++ b/src/HotChocolate/Primitives/test/Directory.Build.props @@ -2,6 +2,7 @@ + $(TestTargetFrameworks) false false