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 @@ -149,7 +149,7 @@ public void FragmentHeavyQuery()
private DocumentValidatorContext CreateContext(DocumentNode document)
{
var context = new DocumentValidatorContext();
context.Initialize(_schema, default, document, 100, 100, null);
context.Initialize(_schema, default, document, 100, 100, 1_000, null);
return context;
}

Expand Down
10 changes: 5 additions & 5 deletions src/HotChocolate/Core/src/Types.Analyzers/Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static class Errors

public static readonly DiagnosticDescriptor InterfaceTypeStaticKeywordMissing =
new(
id: "HC0107",
id: "HC0112",
title: "Static Keyword Missing",
messageFormat: "A split interface type class needs to be a static class",
category: "TypeSystem",
Expand All @@ -79,7 +79,7 @@ public static class Errors

public static readonly DiagnosticDescriptor DataLoaderCannotBeGeneric =
new(
id: "HC0085",
id: "HC0111",
title: "DataLoader Cannot Be Generic",
messageFormat: "The DataLoader source generator cannot generate generic DataLoaders",
category: "DataLoader",
Expand All @@ -88,7 +88,7 @@ public static class Errors

public static readonly DiagnosticDescriptor ConnectionSingleGenericTypeArgument =
new(
id: "HC0086",
id: "HC0110",
title: "Invalid Connection Structure",
messageFormat: "A generic connection/edge type must have a single generic type argument that represents the node type",
category: "TypeSystem",
Expand All @@ -97,7 +97,7 @@ public static class Errors

public static readonly DiagnosticDescriptor ConnectionNameFormatIsInvalid =
new(
id: "HC0087",
id: "HC0109",
title: "Invalid Connection/Edge Name Format",
messageFormat: "A connection/edge name must be in the format `{0}Edge` or `{0}Connection`",
category: "TypeSystem",
Expand All @@ -106,7 +106,7 @@ public static class Errors

public static readonly DiagnosticDescriptor ConnectionNameDuplicate =
new(
id: "HC0088",
id: "HC0108",
title: "Invalid Connection/Edge Name",
messageFormat: "The type `{0}` cannot be mapped to the GraphQL type name `{1}` as `{2}` is already mapped to it",
category: "TypeSystem",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,36 @@ public static IRequestExecutorBuilder RemoveMaxAllowedFieldCycleDepthRule(
return builder;
}

/// <summary>
/// Sets the maximum allowed field merge comparisons during
/// overlapping-fields-can-be-merged validation.
/// </summary>
/// <param name="builder">
/// The <see cref="IRequestExecutorBuilder"/>.
/// </param>
/// <param name="maxAllowedFieldMergeComparisons">
/// The maximum number of field-merge comparisons.
/// </param>
/// <returns>
/// Returns an <see cref="IRequestExecutorBuilder"/> that can be used to chain
/// configuration.
/// </returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="builder"/> is <c>null</c>.
/// </exception>
public static IRequestExecutorBuilder SetMaxAllowedFieldMergeComparisons(
this IRequestExecutorBuilder builder,
int maxAllowedFieldMergeComparisons)
{
ArgumentNullException.ThrowIfNull(builder);

ConfigureValidation(
builder,
(_, b) => b.ModifyOptions(o => o.MaxAllowedFieldMergeComparisons = maxAllowedFieldMergeComparisons));

return builder;
}

/// <summary>
/// Configures the underlying <see cref="DocumentValidatorBuilder"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services
maxAllowedNodes: options.MaxAllowedNodes,
maxAllowedTokens: options.MaxAllowedTokens,
maxAllowedFields: options.MaxAllowedFields,
maxAllowedDirectives: options.MaxAllowedDirectives,
maxAllowedRecursionDepth: options.MaxAllowedRecursionDepth);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ public sealed class RequestParserOptions
/// </summary>
public int MaxAllowedFields { get; set; } = 2048;

/// <summary>
/// <para>
/// 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.
/// </para>
/// </summary>
public int MaxAllowedDirectives { get; set; } = 4;

/// <summary>
/// <para>
/// The maximum allowed recursion depth when parsing a document.
Expand Down
10 changes: 8 additions & 2 deletions src/HotChocolate/Core/src/Validation/DocumentValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public sealed class DocumentValidator
private readonly IDocumentValidatorRule[] _nonCacheableRules;
private readonly int _maxAllowedErrors;
private readonly int _maxLocationsPerError;
private readonly int _maxAllowedFragmentVisits;

/// <summary>
/// Initializes a new instance of <see cref="DocumentValidator"/>.
Expand All @@ -34,11 +35,15 @@ public sealed class DocumentValidator
/// <param name="maxLocationsPerError">
/// The maximum number of locations that will be added to a validation error.
/// </param>
/// <param name="maxAllowedFragmentVisits">
/// The maximum number of fragment visits allowed during validation.
/// </param>
internal DocumentValidator(
ObjectPool<DocumentValidatorContext> contextPool,
IDocumentValidatorRule[] rules,
int maxAllowedErrors,
int maxLocationsPerError)
int maxLocationsPerError,
int maxAllowedFragmentVisits)
{
ArgumentNullException.ThrowIfNull(rules);
ArgumentNullException.ThrowIfNull(contextPool);
Expand All @@ -49,6 +54,7 @@ internal DocumentValidator(
_nonCacheableRules = [.. rules.Where(rule => !rule.IsCacheable)];
_maxAllowedErrors = maxAllowedErrors > 0 ? maxAllowedErrors : 1;
_maxLocationsPerError = maxLocationsPerError > 0 ? maxLocationsPerError : 1;
_maxAllowedFragmentVisits = maxAllowedFragmentVisits > 0 ? maxAllowedFragmentVisits : 1;
}

/// <summary>
Expand Down Expand Up @@ -158,7 +164,7 @@ private DocumentValidatorContext RentContext(
IFeatureCollection? features)
{
var context = _contextPool.Get();
context.Initialize(schema, documentId, document, _maxAllowedErrors, _maxLocationsPerError, features);
context.Initialize(schema, documentId, document, _maxAllowedErrors, _maxLocationsPerError, _maxAllowedFragmentVisits, features);
return context;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@ public DocumentValidator Build()
contextPool,
[.. rules],
_options.MaxAllowedErrors,
_options.MaxLocationsPerError);
_options.MaxLocationsPerError,
_options.MaxAllowedFragmentVisits);
}

private static T CreateInstance<T>(
Expand Down
23 changes: 22 additions & 1 deletion src/HotChocolate/Core/src/Validation/DocumentValidatorContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public void Initialize(
DocumentNode document,
int maxAllowedErrors,
int maxLocationsPerError,
int maxAllowedFragmentVisits,
IFeatureCollection? features)
{
ArgumentNullException.ThrowIfNull(schema);
Expand All @@ -156,6 +157,8 @@ public void Initialize(
_maxAllowedErrors = maxAllowedErrors;
_maxLocationsPerError = maxLocationsPerError;

Fragments.SetMaxAllowedFragmentVisits(maxAllowedFragmentVisits);

_features.Initialize(features);

foreach (var definitionNode in document.Definitions)
Expand Down Expand Up @@ -247,6 +250,8 @@ public sealed class FragmentContext
{
private readonly HashSet<string> _visited = [];
private readonly Dictionary<string, FragmentDefinitionNode> _fragments = new(StringComparer.Ordinal);
private int _maxAllowedFragmentVisits;
private int _fragmentVisits;

public IEnumerable<string> Names => _fragments.Keys;

Expand All @@ -267,9 +272,16 @@ public bool TryGet(FragmentSpreadNode spread, [NotNullWhen(true)] out FragmentDe

public bool TryEnter(FragmentSpreadNode spread, [NotNullWhen(true)] out FragmentDefinitionNode? fragment)
{
if (_fragmentVisits >= _maxAllowedFragmentVisits)
{
fragment = null;
return false;
}

if (_visited.Add(spread.Name.Value)
&& _fragments.TryGetValue(spread.Name.Value, out fragment))
{
Comment thread
michaelstaib marked this conversation as resolved.
_fragmentVisits++;
return true;
}

Expand All @@ -286,13 +298,22 @@ public void Leave(FragmentDefinitionNode fragment)
public bool Exists(FragmentSpreadNode spread)
=> _fragments.ContainsKey(spread.Name.Value);

internal void SetMaxAllowedFragmentVisits(int maxAllowedFragmentVisits)
{
_maxAllowedFragmentVisits = maxAllowedFragmentVisits;
}

internal void Reset()
=> _visited.Clear();
{
_visited.Clear();
_fragmentVisits = 0;
}

internal void Clear()
{
_visited.Clear();
_fragments.Clear();
_fragmentVisits = 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public static DocumentValidatorBuilder AddFieldRules(
return builder
.AddRule<FieldSelectionsRule>()
.AddRule<LeafFieldSelectionsRule>()
.AddRule<OverlappingFieldsCanBeMergedRule>();
.AddRule((_, o) => new OverlappingFieldsCanBeMergedRule(o.MaxAllowedFieldMergeComparisons));
}

/// <summary>
Expand Down
46 changes: 46 additions & 0 deletions src/HotChocolate/Core/src/Validation/Options/ValidationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,50 @@ public ushort MaxAllowedListRecursiveDepth
get;
set => field = value > 0 ? value : (ushort)16;
} = 1;

/// <summary>
/// <para>
/// The maximum number of field-merge comparisons allowed during
/// overlapping-fields-can-be-merged validation. This prevents
/// adversarial queries with deeply nested inline fragments from
/// consuming unbounded CPU.
/// </para>
/// <para>Default: <c>100,000</c></para>
/// </summary>
public int MaxAllowedFieldMergeComparisons
{
get;
set
{
if (value < 1)
{
value = 100_000;
}

field = value;
}
} = 100_000;

/// <summary>
/// <para>
/// The maximum number of fragment visits allowed during validation.
/// Each time a fragment spread is entered counts as one visit.
/// This prevents adversarial queries with deeply nested or repeated
/// fragment spreads from consuming unbounded CPU.
/// </para>
/// <para>Default: <c>1,000</c></para>
/// </summary>
public int MaxAllowedFragmentVisits
{
get;
set
{
if (value < 1)
{
value = 1_000;
}

field = value;
}
} = 1_000;
}
Loading
Loading