Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added GraphQL Request Field Limit. #6381

Merged
merged 9 commits into from
Jul 24, 2023
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 @@ -91,7 +91,8 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services
return new ParserOptions(
noLocations: !options.IncludeLocations,
maxAllowedNodes: options.MaxAllowedNodes,
maxAllowedTokens: options.MaxAllowedTokens);
maxAllowedTokens: options.MaxAllowedTokens,
maxAllowedFields: options.MaxAllowedFields);
});

return services;
Expand Down
5 changes: 5 additions & 0 deletions src/HotChocolate/Core/src/Execution/ErrorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ public static IQueryResult MaxComplexityReached(
{
{ WellKnownContextData.ValidationErrors, true }
});

public static IError MaxComplexityReached() =>
new Error(
ErrorHelper_MaxComplexityReached,
ErrorCodes.Execution.ComplexityExceeded);

public static IQueryResult StateInvalidForComplexityAnalyzer() =>
QueryResultBuilder.CreateError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,15 @@ public sealed class RequestParserOptions
/// This limitation effects the <see cref="Utf8GraphQLReader"/>.
/// </summary>
public int MaxAllowedTokens { get; set; } = int.MaxValue;

/// <summary>
/// Parser CPU and memory usage is linear to the number of nodes in a document
/// however in extreme cases it becomes quadratic due to memory exhaustion.
/// Parsing happens before validation so even invalid queries can burn lots of
/// CPU time and memory.
///
/// To prevent this you can set a maximum number of fields allowed within a document
/// as fields is an easier way to estimate query size for GraphQL requests.
/// </summary>
public int MaxAllowedFields { get; set; } = 2048;
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ private Expression GetInputParser()

private Expression Combine(IReadOnlyList<Expression> expressions)
{
if (expressions.Count > 2048)
{
throw new GraphQLException(ErrorHelper.MaxComplexityReached());
}

var combinedComplexity = expressions[0];

for (var i = 1; i < expressions.Count; i++)
Expand Down
1 change: 1 addition & 0 deletions src/HotChocolate/Core/src/Types/HotChocolate.Types.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<InternalsVisibleTo Include="HotChocolate.Data" />
<InternalsVisibleTo Include="HotChocolate.Data.Raven" />
<InternalsVisibleTo Include="HotChocolate.Caching" />
<InternalsVisibleTo Include="HotChocolate.Fusion" />
<InternalsVisibleTo Include="StrawberryShake.CodeGeneration" />

<!--Legacy Support-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ internal enum FieldFlags
ParallelExecutable = 16,
Stream = 32,
Sealed = 64,
TypeNameField = 128
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public static class IntrospectionFields
/// Gets the field name of the __type introspection field.
/// </summary>
public static string Type => "__type";

private static readonly PureFieldDelegate _typeNameResolver =
ctx => ctx.ObjectType.Name;

internal static ObjectFieldDefinition CreateSchemaField(IDescriptorContext context)
{
Expand Down Expand Up @@ -69,10 +72,9 @@ internal static ObjectFieldDefinition CreateTypeNameField(IDescriptorContext con
.Description(TypeResources.TypeNameField_Description)
.Type<NonNullType<StringType>>();

descriptor.Extend().Definition.PureResolver = Resolve;

static string Resolve(IPureResolverContext ctx)
=> ctx.ObjectType.Name;
var definition = descriptor.Extend().Definition;
definition.PureResolver = _typeNameResolver;
definition.Flags |= FieldFlags.TypeNameField;

return CreateDefinition(descriptor);
}
Expand Down
3 changes: 3 additions & 0 deletions src/HotChocolate/Core/src/Types/Types/OutputFieldBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ internal OutputFieldBase(TDefinition definition, int index) : base(definition, i
/// </summary>
public bool IsIntrospectionField
=> (Flags & FieldFlags.Introspection) == FieldFlags.Introspection;

internal bool IsTypeNameField
=> (Flags & FieldFlags.TypeNameField) == FieldFlags.TypeNameField;

/// <inheritdoc />
public bool IsDeprecated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
}
}
],
"Count": 5
"Count": 5,
"FieldsCount": 1
}
},
"QueryId": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,80 @@ ... on Bar {
.UseDefaultPipeline());
}

[Fact]
public async Task Alias_Explosion_Does_Not_Kill_The_Analyzer_With_Defaults()
{
var executor =
await new ServiceCollection()
.AddGraphQL()
.AddDocumentFromString(FileResource.Open("CostSchema.graphql"))
.UseField(_ => _ => default)
.ConfigureSchema(s => s.AddCostDirectiveType())
.ModifyRequestOptions(
o =>
{
o.Complexity.Enable = true;
o.Complexity.MaximumAllowed = 1000;
})
.UseDefaultPipeline()
.BuildRequestExecutorAsync();

var result = await executor.ExecuteAsync(FileResource.Open("aliases.graphql"));

result.MatchSnapshot();
}

[Fact]
public async Task Alias_Explosion_Does_Not_Kill_The_Analyzer_With_Defaults_2()
{
var executor =
await new ServiceCollection()
.AddGraphQL()
.AddDocumentFromString(FileResource.Open("CostSchema.graphql"))
.UseField(_ => _ => default)
.ConfigureSchema(s => s.AddCostDirectiveType())
.ModifyRequestOptions(
o =>
{
o.Complexity.Enable = true;
o.Complexity.MaximumAllowed = 1000;
})
.UseDefaultPipeline()
.BuildRequestExecutorAsync();

var result = await executor.ExecuteAsync(FileResource.Open("aliases_2048.graphql"));

result.MatchSnapshot();
}

[Fact]
public async Task Alias_Explosion_Does_Not_Kill_The_Analyzer()
{
var executor =
await new ServiceCollection()
.AddGraphQL()
.AddDocumentFromString(FileResource.Open("CostSchema.graphql"))
.UseField(_ => _ => default)
.ConfigureSchema(s => s.AddCostDirectiveType())
.ModifyRequestOptions(
o =>
{
o.Complexity.Enable = true;
o.Complexity.MaximumAllowed = 1000;
})
.ModifyParserOptions(
o =>
{
o.MaxAllowedFields = 40000;
})
.UseDefaultPipeline()
.BuildRequestExecutorAsync();

var result = await executor.ExecuteAsync(FileResource.Open("aliases.graphql"));

result.MatchSnapshot();
}

[Fact]
public async Task MaxComplexity_Analysis_Skipped()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"errors": [
{
"message": "The maximum allowed operation complexity was exceeded.",
"extensions": {
"code": "HC0047"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"errors": [
{
"message": "The GraphQL request document contains more than 2048 fields. Parsing aborted.",
"locations": [
{
"line": 1,
"column": 46014
}
],
"extensions": {
"code": "HC0014"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"errors": [
{
"message": "The maximum allowed operation complexity was exceeded.",
"extensions": {
"complexity": 2048,
"allowedComplexity": 1000,
"code": "HC0047"
}
}
]
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,7 @@ private async Task ExpectErrors(
// arrange
schema ??= ValidationUtils.CreateSchema();
validator ??= CreateValidator();
var query = Utf8GraphQLParser.Parse(sourceText);
var query = Utf8GraphQLParser.Parse(sourceText, new ParserOptions(maxAllowedFields: int.MaxValue));

// act
var result = await validator.ValidateAsync(
Expand Down
78 changes: 57 additions & 21 deletions src/HotChocolate/Fusion/src/Core/Execution/Nodes/Introspect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text.Json;
using HotChocolate.Execution.Processing;
using HotChocolate.Language;
using HotChocolate.Types;
using static HotChocolate.Fusion.Utilities.Utf8QueryPlanPropertyNames;

namespace HotChocolate.Fusion.Execution.Nodes;
Expand All @@ -21,8 +22,7 @@ namespace HotChocolate.Fusion.Execution.Nodes;
/// </exception>
internal sealed class Introspect(int id, SelectionSet selectionSet) : QueryPlanNode(id)
{
private readonly SelectionSet _selectionSet = selectionSet
?? throw new ArgumentNullException(nameof(selectionSet));
private readonly SelectionSet _selectionSet = selectionSet ?? throw new ArgumentNullException(nameof(selectionSet));

/// <summary>
/// Gets the kind of this node.
Expand All @@ -46,30 +46,66 @@ protected override async Task OnExecuteAsync(
RequestState state,
CancellationToken cancellationToken)
{
if (state.TryGetState(_selectionSet, out var values))
if (state.TryGetState(_selectionSet, out List<ExecutionState>? values))
{
var value = values[0];
var operationContext = context.OperationContext;
var rootSelections = _selectionSet.Selections;

for (var i = 0; i < rootSelections.Count; i++)
List<Task>? asyncTasks = null;
ExecutePureFieldsAndEnqueueResolvers(context, value, cancellationToken, ref asyncTasks);
if(asyncTasks is { Count: > 0 })
{
var selection = rootSelections[i];
await Task.WhenAll(asyncTasks).ConfigureAwait(false);
}
}
}

if (selection.Field.IsIntrospectionField)
{
var resolverTask = operationContext.CreateResolverTask(
selection,
operationContext.RootValue,
value.SelectionSetResult,
i,
operationContext.PathFactory.Append(Path.Root, selection.ResponseName),
ImmutableDictionary<string, object?>.Empty);
resolverTask.BeginExecute(cancellationToken);
private static void ExecutePureFieldsAndEnqueueResolvers(
FusionExecutionContext context,
ExecutionState value,
CancellationToken ct,
ref List<Task>? asyncTasks)
{
var operationContext = context.OperationContext;
var rootSelectionSet = Unsafe.As<SelectionSet>(context.Operation.RootSelectionSet);
ref var selection = ref rootSelectionSet.GetSelectionsReference();
ref var end = ref Unsafe.Add(ref selection, rootSelectionSet.Selections.Count);
var rootTypeName = selection.DeclaringType.Name;
var i = 0;

await resolverTask.WaitForCompletionAsync(cancellationToken);
}
while (Unsafe.IsAddressLessThan(ref selection, ref end))
{
var field = Unsafe.As<ObjectField>(selection.Field);
var result = value.SelectionSetResult;

if (!field.IsIntrospectionField)
{
goto NEXT;
}

if (field.IsTypeNameField)
{
// if the request just asks for the __typename field we immediately resolve it without
// going through the resolver pipeline.
result.SetValueUnsafe(i, selection.ResponseName, rootTypeName, false);
goto NEXT;
}

// only for proper introspection fields we will execute the resolver pipeline.
var resolverTask = operationContext.CreateResolverTask(
selection,
operationContext.RootValue,
result,
i,
operationContext.PathFactory.Append(Path.Root, selection.ResponseName),
ImmutableDictionary<string, object?>.Empty);
resolverTask.BeginExecute(ct);

asyncTasks ??= new List<Task>();
asyncTasks.Add(resolverTask.WaitForCompletionAsync(ct));

NEXT:
selection = ref Unsafe.Add(ref selection, 1)!;
i++;
}
}

Expand All @@ -93,4 +129,4 @@ protected override void FormatProperties(Utf8JsonWriter writer)
var selectionSetNode = new SelectionSetNode(null, rootSelectionNodes);
writer.WriteString(DocumentProp, selectionSetNode.ToString(false));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
using System.Runtime.CompilerServices;
using HotChocolate.Execution.Processing;
using HotChocolate.Fusion.Metadata;
using HotChocolate.Fusion.Utilities;
using HotChocolate.Language;
using HotChocolate.Types;
using HotChocolate.Types.Introspection;
using HotChocolate.Utilities;
using ThrowHelper = HotChocolate.Fusion.Utilities.ThrowHelper;

namespace HotChocolate.Fusion.Planning.Pipeline;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using HotChocolate.Execution.Processing;
using HotChocolate.Fusion.Metadata;
using HotChocolate.Fusion.Utilities;
using HotChocolate.Utilities;
using static System.StringComparer;
using ThrowHelper = HotChocolate.Fusion.Utilities.ThrowHelper;

namespace HotChocolate.Fusion.Planning.Pipeline;

Expand Down Expand Up @@ -267,11 +267,11 @@ private static ResolverDefinition SelectResolver(

static bool FulfillsRequirements(
ResolverDefinition resolver,
HashSet<string> variabesInContext)
HashSet<string> variablesInContext)
{
foreach (var requirement in resolver.Requires)
{
if (!variabesInContext.Contains(requirement))
if (!variablesInContext.Contains(requirement))
{
return false;
}
Expand Down
Loading