diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreHostingBuilderExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreHostingBuilderExtensions.cs
index 6cce725e6f6..93ade12e034 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreHostingBuilderExtensions.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreHostingBuilderExtensions.cs
@@ -21,14 +21,14 @@ public static class HotChocolateAspNetCoreHostingBuilderExtensions
///
/// The max allowed GraphQL request size.
///
- ///
- /// Defines if the cost analyzer should be disabled.
+ ///
+ /// Defines if the default security policy should be disabled.
///
///
public static IRequestExecutorBuilder AddGraphQL(
this IHostApplicationBuilder builder,
string? schemaName = default,
int maxAllowedRequestSize = MaxAllowedRequestSize,
- bool disableCostAnalyzer = false)
- => builder.Services.AddGraphQLServer(schemaName, maxAllowedRequestSize, disableCostAnalyzer);
+ bool disableDefaultSecurity = false)
+ => builder.Services.AddGraphQLServer(schemaName, maxAllowedRequestSize, disableDefaultSecurity);
}
diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs
index 110eb35b5bd..562eb42469b 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore/Extensions/HotChocolateAspNetCoreServiceCollectionExtensions.cs
@@ -94,8 +94,8 @@ public static IServiceCollection AddGraphQLServerCore(
///
/// The max allowed GraphQL request size.
///
- ///
- /// Defines if the cost analyzer should be disabled.
+ ///
+ /// Defines if the default security policy should be disabled.
///
///
/// Returns the so that configuration can be chained.
@@ -104,7 +104,7 @@ public static IRequestExecutorBuilder AddGraphQLServer(
this IServiceCollection services,
string? schemaName = default,
int maxAllowedRequestSize = MaxAllowedRequestSize,
- bool disableCostAnalyzer = false)
+ bool disableDefaultSecurity = false)
{
var builder = services
.AddGraphQLServerCore(maxAllowedRequestSize)
@@ -112,14 +112,20 @@ public static IRequestExecutorBuilder AddGraphQLServer(
.AddDefaultHttpRequestInterceptor()
.AddSubscriptionServices();
- if (!disableCostAnalyzer)
+ if (!disableDefaultSecurity)
{
builder.AddCostAnalyzer();
builder.AddIntrospectionAllowedRule(
(sp, _) =>
{
var environment = sp.GetService();
- return (environment?.IsDevelopment() ?? true) == false;
+ return environment?.IsDevelopment() == false;
+ });
+ builder.AddMaxAllowedFieldCycleDepthRule(
+ isEnabled: (sp, _) =>
+ {
+ var environment = sp.GetService();
+ return environment?.IsDevelopment() == false;
});
}
@@ -145,7 +151,7 @@ public static IRequestExecutorBuilder AddGraphQLServer(
this IRequestExecutorBuilder builder,
string? schemaName = default,
bool disableCostAnalyzer = false)
- => builder.Services.AddGraphQLServer(schemaName, disableCostAnalyzer: disableCostAnalyzer);
+ => builder.Services.AddGraphQLServer(schemaName, disableDefaultSecurity: disableCostAnalyzer);
///
/// Registers the GraphQL Upload Scalar.
diff --git a/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs b/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs
index 9c87bf56e4e..33dc5c3dd5c 100644
--- a/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs
+++ b/src/HotChocolate/Caching/test/Caching.Tests/SchemaTests.cs
@@ -13,7 +13,7 @@ public async Task Allow_CacheControl_On_FieldDefinition()
{
var schema =
await new ServiceCollection()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddTypeExtension(typeof(Query))
.ConfigureSchema(
b => b.TryAddRootType(
diff --git a/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs b/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
index 933414ead22..dad7fd68e68 100644
--- a/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
+++ b/src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
@@ -310,6 +310,16 @@ public static class Validation
/// The introspection is not allowed for the current request
///
public const string IntrospectionNotAllowed = "HC0046";
+
+ ///
+ /// The maximum allowed introspection depth was exceeded.
+ ///
+ public const string MaxIntrospectionDepthOverflow = "HC0086";
+
+ ///
+ /// The maximum allowed coordinate cycle depth was exceeded.
+ ///
+ public const string MaxCoordinateCycleDepthOverflow = "HC0087";
}
///
diff --git a/src/HotChocolate/Core/src/Authorization/AuthorizeValidationRule.cs b/src/HotChocolate/Core/src/Authorization/AuthorizeValidationRule.cs
index c990879a054..4b95641c9f5 100644
--- a/src/HotChocolate/Core/src/Authorization/AuthorizeValidationRule.cs
+++ b/src/HotChocolate/Core/src/Authorization/AuthorizeValidationRule.cs
@@ -8,6 +8,8 @@ internal sealed class AuthorizeValidationRule(AuthorizationCache cache) : IDocum
private readonly AuthorizeValidationVisitor _visitor = new();
private readonly AuthorizationCache _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ public ushort Priority => ushort.MaxValue;
+
public bool IsCacheable => false;
public void Validate(IDocumentValidatorContext context, DocumentNode document)
diff --git a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Validation.cs b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Validation.cs
index 07cf4be931e..ab5066b6719 100644
--- a/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Validation.cs
+++ b/src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Validation.cs
@@ -1,3 +1,4 @@
+using HotChocolate;
using HotChocolate.Execution.Configuration;
using HotChocolate.Validation;
using HotChocolate.Validation.Options;
@@ -187,6 +188,9 @@ public static IRequestExecutorBuilder AddValidationResultAggregator(
///
/// Defines if request depth overrides are allowed on a per request basis.
///
+ ///
+ /// Defines if the validation rule is enabled.
+ ///
///
/// Returns the for configuration chaining.
///
@@ -194,7 +198,8 @@ public static IRequestExecutorBuilder AddMaxExecutionDepthRule(
this IRequestExecutorBuilder builder,
int maxAllowedExecutionDepth,
bool skipIntrospectionFields = false,
- bool allowRequestOverrides = false)
+ bool allowRequestOverrides = false,
+ Func? isEnabled = null)
{
if (builder is null)
{
@@ -206,7 +211,8 @@ public static IRequestExecutorBuilder AddMaxExecutionDepthRule(
b => b.AddMaxExecutionDepthRule(
maxAllowedExecutionDepth,
skipIntrospectionFields,
- allowRequestOverrides));
+ allowRequestOverrides,
+ isEnabled));
return builder;
}
@@ -280,6 +286,45 @@ public static IRequestExecutorBuilder SetMaxAllowedValidationErrors(
return builder;
}
+ ///
+ /// Adds a validation rule that restricts the coordinate cycle depth in a GraphQL operation.
+ ///
+ public static IRequestExecutorBuilder AddMaxAllowedFieldCycleDepthRule(
+ this IRequestExecutorBuilder builder,
+ ushort? defaultCycleLimit = 3,
+ (SchemaCoordinate Coordinate, ushort MaxAllowed)[]? coordinateCycleLimits = null,
+ Func? isEnabled = null)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ ConfigureValidation(
+ builder,
+ b => b.AddMaxAllowedFieldCycleDepthRule(
+ defaultCycleLimit,
+ coordinateCycleLimits,
+ isEnabled));
+
+ return builder;
+ }
+
+ ///
+ /// Removes the validation rule that restricts the coordinate cycle depth in a GraphQL operation.
+ ///
+ public static IRequestExecutorBuilder RemoveMaxAllowedFieldCycleDepthRule(
+ this IRequestExecutorBuilder builder)
+ {
+ if (builder is null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ ConfigureValidation(builder, b => b.RemoveMaxAllowedFieldCycleDepthRule());
+ return builder;
+ }
+
private static IRequestExecutorBuilder ConfigureValidation(
IRequestExecutorBuilder builder,
Action configure)
diff --git a/src/HotChocolate/Core/src/Validation/CoordinateLimit.cs b/src/HotChocolate/Core/src/Validation/CoordinateLimit.cs
new file mode 100644
index 00000000000..2717b7cc7bd
--- /dev/null
+++ b/src/HotChocolate/Core/src/Validation/CoordinateLimit.cs
@@ -0,0 +1,27 @@
+namespace HotChocolate.Validation;
+
+internal sealed class CoordinateLimit
+{
+ public ushort MaxAllowed { get; private set; }
+
+ public ushort Count { get; private set; }
+
+ public bool Add()
+ {
+ if (Count < MaxAllowed)
+ {
+ Count++;
+ return true;
+ }
+
+ return false;
+ }
+
+ public void Remove() => Count--;
+
+ public void Reset(ushort maxAllowed)
+ {
+ MaxAllowed = maxAllowed;
+ Count = 0;
+ }
+}
diff --git a/src/HotChocolate/Core/src/Validation/DocumentValidator.cs b/src/HotChocolate/Core/src/Validation/DocumentValidator.cs
index 32362f8da70..fcf2f556b92 100644
--- a/src/HotChocolate/Core/src/Validation/DocumentValidator.cs
+++ b/src/HotChocolate/Core/src/Validation/DocumentValidator.cs
@@ -54,6 +54,9 @@ public DocumentValidator(
_nonCacheableRules = _allRules.Where(t => !t.IsCacheable).ToArray();
_aggregators = resultAggregators.ToArray();
_maxAllowedErrors = errorOptions.MaxAllowedErrors;
+
+ Array.Sort(_allRules, (a, b) => a.Priority.CompareTo(b.Priority));
+ Array.Sort(_nonCacheableRules, (a, b) => a.Priority.CompareTo(b.Priority));
}
///
@@ -102,6 +105,11 @@ public ValueTask ValidateAsync(
for (var i = 0; i < length; i++)
{
Unsafe.Add(ref start, i).Validate(context, document);
+
+ if (context.FatalErrorDetected)
+ {
+ break;
+ }
}
if (_aggregators.Length == 0)
diff --git a/src/HotChocolate/Core/src/Validation/DocumentValidatorContext.cs b/src/HotChocolate/Core/src/Validation/DocumentValidatorContext.cs
index 9b645c35b54..f0c913368a1 100644
--- a/src/HotChocolate/Core/src/Validation/DocumentValidatorContext.cs
+++ b/src/HotChocolate/Core/src/Validation/DocumentValidatorContext.cs
@@ -96,6 +96,8 @@ public IOutputType NonNullString
public bool UnexpectedErrorsDetected { get; set; }
+ public bool FatalErrorDetected { get; set; }
+
public int Count { get; set; }
public int Max { get; set; }
@@ -110,6 +112,8 @@ public IOutputType NonNullString
public HashSet ProcessedFieldPairs { get; } = [];
+ public FieldDepthCycleTracker FieldDepth { get; } = new();
+
public IList RentFieldInfoList()
{
var buffer = _buffers.Peek();
@@ -166,7 +170,9 @@ public void Clear()
CurrentFieldPairs.Clear();
NextFieldPairs.Clear();
ProcessedFieldPairs.Clear();
+ FieldDepth.Reset();
UnexpectedErrorsDetected = false;
+ FatalErrorDetected = false;
Count = 0;
Max = 0;
Allowed = 0;
diff --git a/src/HotChocolate/Core/src/Validation/DocumentValidatorRule.cs b/src/HotChocolate/Core/src/Validation/DocumentValidatorRule.cs
index 49fac8dd123..c6935a7654b 100644
--- a/src/HotChocolate/Core/src/Validation/DocumentValidatorRule.cs
+++ b/src/HotChocolate/Core/src/Validation/DocumentValidatorRule.cs
@@ -8,12 +8,18 @@ public class DocumentValidatorRule
{
private readonly TVisitor _visitor;
- public DocumentValidatorRule(TVisitor visitor, bool isCacheable = true)
+ public DocumentValidatorRule(
+ TVisitor visitor,
+ bool isCacheable = true,
+ ushort property = ushort.MaxValue)
{
_visitor = visitor;
IsCacheable = isCacheable;
+ Priority = property;
}
+ public ushort Priority { get; }
+
public bool IsCacheable { get; }
public void Validate(IDocumentValidatorContext context, DocumentNode document)
diff --git a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs
index e20a4671a79..e1984388a14 100644
--- a/src/HotChocolate/Core/src/Validation/ErrorHelper.cs
+++ b/src/HotChocolate/Core/src/Validation/ErrorHelper.cs
@@ -707,4 +707,33 @@ public static IError StreamOnNonListField(
.SpecifiedBy("sec-Stream-Directives-Are-Used-On-List-Fields")
.SetPath(context.CreateErrorPath())
.Build();
+
+ public static void ReportMaxIntrospectionDepthOverflow(
+ this IDocumentValidatorContext context,
+ ISyntaxNode selection)
+ {
+ context.FatalErrorDetected = true;
+ context.ReportError(
+ ErrorBuilder.New()
+ .SetMessage("Maximum allowed introspection depth exceeded.")
+ .SetCode(ErrorCodes.Validation.MaxIntrospectionDepthOverflow)
+ .SetLocations([selection])
+ .SetPath(context.CreateErrorPath())
+ .Build());
+ }
+
+ public static void ReportMaxCoordinateCycleDepthOverflow(
+ this IDocumentValidatorContext context,
+ ISyntaxNode selection)
+ {
+ context.FatalErrorDetected = true;
+
+ context.ReportError(
+ ErrorBuilder.New()
+ .SetMessage("Maximum allowed coordinate cycle depth was exceeded.")
+ .SetCode(ErrorCodes.Validation.MaxIntrospectionDepthOverflow)
+ .SetLocations([selection])
+ .SetPath(context.CreateErrorPath())
+ .Build());
+ }
}
diff --git a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs
index e8ddc3f5cb2..c0439f486da 100644
--- a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs
+++ b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.Rules.cs
@@ -1,3 +1,5 @@
+using System.Collections.Immutable;
+using HotChocolate;
using HotChocolate.Validation.Options;
using HotChocolate.Validation.Rules;
@@ -111,7 +113,7 @@ public static IValidationBuilder AddDocumentRules(
/// Field selections on scalars or enums are never allowed,
/// because they are the leaf nodes of any GraphQL query.
///
- /// Conversely the leaf field selections of GraphQL queries
+ /// Conversely, the leaf field selections of GraphQL queries
/// must be of type scalar or enum. Leaf selections on objects,
/// interfaces, and unions without subfields are disallowed.
///
@@ -169,7 +171,7 @@ public static IValidationBuilder AddFieldRules(
/// AND
///
/// The graph of fragment spreads must not form any cycles including
- /// spreading itself. Otherwise an operation could infinitely spread or
+ /// spreading itself. Otherwise, an operation could infinitely spread or
/// infinitely execute on cycles in the underlying data.
///
/// https://spec.graphql.org/June2018/#sec-Fragment-spreads-must-not-form-cycles
@@ -312,7 +314,10 @@ public static IValidationBuilder AddOperationRules(
/// Specifies if depth analysis is skipped for introspection queries.
///
///
- /// Defines if request depth overrides are allowed on a per request basis.
+ /// Defines if request depth overrides are allowed on a per-request basis.
+ ///
+ ///
+ /// A delegate that defines if the rule is enabled.
///
///
/// Returns the for configuration chaining.
@@ -321,12 +326,15 @@ public static IValidationBuilder AddMaxExecutionDepthRule(
this IValidationBuilder builder,
int maxAllowedExecutionDepth,
bool skipIntrospectionFields = false,
- bool allowRequestOverrides = false)
+ bool allowRequestOverrides = false,
+ Func? isEnabled = null)
{
return builder
.TryAddValidationVisitor(
(_, o) => new MaxExecutionDepthVisitor(o),
- isCacheable: !allowRequestOverrides)
+ priority: 2,
+ isCacheable: !allowRequestOverrides,
+ isEnabled: isEnabled)
.ModifyValidationOptions(o =>
{
o.MaxAllowedExecutionDepth = maxAllowedExecutionDepth;
@@ -334,6 +342,13 @@ public static IValidationBuilder AddMaxExecutionDepthRule(
});
}
+ ///
+ /// Removes a validation rule that restricts the depth of a GraphQL request.
+ ///
+ public static IValidationBuilder RemoveMaxExecutionDepthRule(
+ this IValidationBuilder builder)
+ => builder.TryRemoveValidationVisitor();
+
///
/// Adds a validation rule that only allows requests to use `__schema` or `__type`
/// if the request carries an introspection allowed flag.
@@ -341,14 +356,47 @@ public static IValidationBuilder AddMaxExecutionDepthRule(
public static IValidationBuilder AddIntrospectionAllowedRule(
this IValidationBuilder builder,
Func? isEnabled = null)
- => builder.TryAddValidationVisitor((_, _) => new IntrospectionVisitor(), false, isEnabled);
+ => builder.TryAddValidationVisitor(
+ (_, _) => new IntrospectionVisitor(),
+ priority: 0,
+ isCacheable: false,
+ isEnabled: isEnabled);
///
/// Removes a validation rule that only allows requests to use `__schema` or `__type`
/// if the request carries an introspection allowed flag.
///
public static IValidationBuilder RemoveIntrospectionAllowedRule(
+ this IValidationBuilder builder)
+ => builder.TryRemoveValidationVisitor();
+
+ ///
+ /// Adds a validation rule that restricts the depth of a GraphQL introspection request.
+ ///
+ public static IValidationBuilder AddIntrospectionDepthRule(
+ this IValidationBuilder builder)
+ => builder.TryAddValidationVisitor(priority: 1);
+
+ ///
+ /// Adds a validation rule that restricts the depth of coordinate cycles in GraphQL operations.
+ ///
+ public static IValidationBuilder AddMaxAllowedFieldCycleDepthRule(
this IValidationBuilder builder,
+ ushort? defaultCycleLimit = 3,
+ (SchemaCoordinate Coordinate, ushort MaxAllowed)[]? coordinateCycleLimits = null,
Func? isEnabled = null)
- => builder.TryRemoveValidationVisitor();
+ => builder.TryAddValidationVisitor(
+ (_, _) => new MaxAllowedFieldCycleDepthVisitor(
+ coordinateCycleLimits?.ToImmutableArray()
+ ?? ImmutableArray<(SchemaCoordinate, ushort)>.Empty,
+ defaultCycleLimit),
+ priority: 3,
+ isEnabled: isEnabled);
+
+ ///
+ /// Removes a validation rule that restricts the depth of coordinate cycles in GraphQL operations.
+ ///
+ public static IValidationBuilder RemoveMaxAllowedFieldCycleDepthRule(
+ this IValidationBuilder builder)
+ => builder.TryRemoveValidationVisitor();
}
diff --git a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.cs b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.cs
index 477835a3e3d..887fb2b555d 100644
--- a/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.cs
+++ b/src/HotChocolate/Core/src/Validation/Extensions/ValidationBuilderExtensions.cs
@@ -110,13 +110,17 @@ internal static IValidationBuilder ModifyValidationOptions(
/// Specifies if the validation visitor`s results are cacheable or
/// if the visitor needs to be rerun on every request.
///
+ ///
+ /// The priority of the validation visitor. The lower the value the earlier the visitor is executed.
+ ///
/// The validation visitor type.
///
/// Returns the validation builder for configuration chaining.
///
public static IValidationBuilder TryAddValidationVisitor(
this IValidationBuilder builder,
- bool isCacheable = true)
+ bool isCacheable = true,
+ ushort priority = ushort.MaxValue)
where T : DocumentValidatorVisitor, new()
{
return builder.ConfigureValidation(m =>
@@ -124,7 +128,7 @@ public static IValidationBuilder TryAddValidationVisitor(
{
if (o.Rules.All(t => t.GetType() != typeof(DocumentValidatorRule)))
{
- o.Rules.Add(new DocumentValidatorRule(new T(), isCacheable));
+ o.Rules.Add(new DocumentValidatorRule(new T(), isCacheable, priority));
}
}));
}
@@ -143,6 +147,9 @@ public static IValidationBuilder TryAddValidationVisitor(
/// Specifies if the validation visitor`s results are cacheable or
/// if the visitor needs to be rerun on every request.
///
+ ///
+ /// The priority of the validation visitor. The lower the value the earlier the visitor is executed.
+ ///
///
/// A delegate to determine if the validation visitor and should be added.
///
@@ -154,6 +161,7 @@ public static IValidationBuilder TryAddValidationVisitor(
this IValidationBuilder builder,
Func factory,
bool isCacheable = true,
+ ushort priority = ushort.MaxValue,
Func? isEnabled = null)
where T : DocumentValidatorVisitor
{
@@ -163,7 +171,7 @@ public static IValidationBuilder TryAddValidationVisitor(
if (o.Rules.All(t => t.GetType() != typeof(DocumentValidatorRule))
&& (isEnabled?.Invoke(s, o) ?? true))
{
- o.Rules.Add(new DocumentValidatorRule(factory(s, o), isCacheable));
+ o.Rules.Add(new DocumentValidatorRule(factory(s, o), isCacheable, priority));
}
}));
}
diff --git a/src/HotChocolate/Core/src/Validation/Extensions/ValidationServiceCollectionExtensions.cs b/src/HotChocolate/Core/src/Validation/Extensions/ValidationServiceCollectionExtensions.cs
index 5ab7538b971..308c8800bea 100644
--- a/src/HotChocolate/Core/src/Validation/Extensions/ValidationServiceCollectionExtensions.cs
+++ b/src/HotChocolate/Core/src/Validation/Extensions/ValidationServiceCollectionExtensions.cs
@@ -28,6 +28,7 @@ public static IValidationBuilder AddValidation(
var builder = new DefaultValidationBuilder(schemaName, services);
builder
+ .AddIntrospectionDepthRule()
.AddDocumentRules()
.AddOperationRules()
.AddFieldRules()
diff --git a/src/HotChocolate/Core/src/Validation/FieldDepthCycleTracker.cs b/src/HotChocolate/Core/src/Validation/FieldDepthCycleTracker.cs
new file mode 100644
index 00000000000..7e001ba2073
--- /dev/null
+++ b/src/HotChocolate/Core/src/Validation/FieldDepthCycleTracker.cs
@@ -0,0 +1,89 @@
+using HotChocolate.Language;
+
+namespace HotChocolate.Validation;
+
+///
+/// Allows to track field cycle depths in a GraphQL query.
+///
+public sealed class FieldDepthCycleTracker
+{
+ private readonly Dictionary _coordinates = new();
+ private readonly List _limits = new();
+ private ushort? _defaultMaxAllowed;
+
+ ///
+ /// Adds a field coordinate to the tracker.
+ ///
+ ///
+ /// A field coordinate.
+ ///
+ ///
+ /// true if the field coordinate has not reached its cycle depth limit;
+ /// otherwise, false.
+ ///
+ public bool Add(SchemaCoordinate coordinate)
+ {
+ if (_coordinates.TryGetValue(coordinate, out var limit))
+ {
+ return limit.Add();
+ }
+
+ if(_defaultMaxAllowed.HasValue)
+ {
+ _limits.TryPop(out limit);
+ limit ??= new CoordinateLimit();
+ limit.Reset(_defaultMaxAllowed.Value);
+ _coordinates.Add(coordinate, limit);
+ return limit.Add();
+ }
+
+ return true;
+ }
+
+ ///
+ /// Removes a field coordinate from the tracker.
+ ///
+ ///
+ /// A field coordinate.
+ ///
+ public void Remove(SchemaCoordinate coordinate)
+ {
+ if (_coordinates.TryGetValue(coordinate, out var limit))
+ {
+ limit.Remove();
+ }
+ }
+
+ ///
+ /// Initializes the field depth tracker with the specified limits.
+ ///
+ ///
+ /// A collection of field coordinates and their cycle depth limits.
+ ///
+ ///
+ /// The default cycle depth limit for coordinates that were not explicitly defined.
+ ///
+ public void Initialize(
+ IEnumerable<(SchemaCoordinate Coordinate, ushort MaxAllowed)> limits,
+ ushort? defaultMaxAllowed = null)
+ {
+ foreach (var (coordinate, maxAllowed) in limits)
+ {
+ _limits.TryPop(out var limit);
+ limit ??= new CoordinateLimit();
+ limit.Reset(maxAllowed);
+ _coordinates.Add(coordinate, limit);
+ }
+
+ _defaultMaxAllowed = defaultMaxAllowed;
+ }
+
+ ///
+ /// Resets the field depth tracker.
+ ///
+ public void Reset()
+ {
+ _limits.AddRange(_coordinates.Values);
+ _coordinates.Clear();
+ }
+}
diff --git a/src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs b/src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs
index b923a9e6c94..99402f7ad9a 100644
--- a/src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs
+++ b/src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs
@@ -153,6 +153,11 @@ public interface IDocumentValidatorContext
///
bool UnexpectedErrorsDetected { get; set; }
+ ///
+ /// Defines that a fatal error was detected and that the analyzer will be aborted.
+ ///
+ bool FatalErrorDetected { get; set; }
+
///
/// A map to store arbitrary visitor data.
///
@@ -161,17 +166,22 @@ public interface IDocumentValidatorContext
///
/// When processing field merging this list holds the field pairs that are processed.
///
- List CurrentFieldPairs { get; }
+ List CurrentFieldPairs { get; }
///
/// When processing field merging this list holds the field pairs that are processed next.
///
- List NextFieldPairs { get; }
+ List NextFieldPairs { get; }
///
/// When processing field merging this set represents the already processed field pairs.
///
- HashSet ProcessedFieldPairs { get; }
+ HashSet ProcessedFieldPairs { get; }
+
+ ///
+ /// Gets the field depth cycle tracker.
+ ///
+ FieldDepthCycleTracker FieldDepth { get; }
///
/// Rents a list of field infos.
diff --git a/src/HotChocolate/Core/src/Validation/IDocumentValidatorRule.cs b/src/HotChocolate/Core/src/Validation/IDocumentValidatorRule.cs
index 42eb261eeb2..17162a75005 100644
--- a/src/HotChocolate/Core/src/Validation/IDocumentValidatorRule.cs
+++ b/src/HotChocolate/Core/src/Validation/IDocumentValidatorRule.cs
@@ -7,6 +7,11 @@ namespace HotChocolate.Validation;
///
public interface IDocumentValidatorRule
{
+ ///
+ /// Gets the priority of this rule. Rules with a lower priority are executed first.
+ ///
+ ushort Priority { get; }
+
///
/// Defines if the result of this rule can be cached and reused on consecutive
/// validations of the same GraphQL request document.
diff --git a/src/HotChocolate/Core/src/Validation/Rules/ArgumentVisitor.cs b/src/HotChocolate/Core/src/Validation/Rules/ArgumentVisitor.cs
index 0355dfc9e76..93989e8c3ed 100644
--- a/src/HotChocolate/Core/src/Validation/Rules/ArgumentVisitor.cs
+++ b/src/HotChocolate/Core/src/Validation/Rules/ArgumentVisitor.cs
@@ -30,16 +30,10 @@ namespace HotChocolate.Validation.Rules;
///
/// http://facebook.github.io/graphql/June2018/#sec-Required-Arguments
///
-internal sealed class ArgumentVisitor : TypeDocumentValidatorVisitor
+internal sealed class ArgumentVisitor()
+ : TypeDocumentValidatorVisitor(
+ new SyntaxVisitorOptions { VisitDirectives = true, })
{
- public ArgumentVisitor()
- : base(new SyntaxVisitorOptions
- {
- VisitDirectives = true,
- })
- {
- }
-
protected override ISyntaxVisitorAction Enter(
FieldNode node,
IDocumentValidatorContext context)
diff --git a/src/HotChocolate/Core/src/Validation/Rules/DocumentRule.cs b/src/HotChocolate/Core/src/Validation/Rules/DocumentRule.cs
index beb698e48b5..c8ce2210229 100644
--- a/src/HotChocolate/Core/src/Validation/Rules/DocumentRule.cs
+++ b/src/HotChocolate/Core/src/Validation/Rules/DocumentRule.cs
@@ -3,22 +3,28 @@
namespace HotChocolate.Validation.Rules;
///
+///
/// GraphQL execution will only consider the executable definitions
/// Operation and Fragment.
-///
+///
+///
/// Type system definitions and extensions are not executable,
/// and are not considered during execution.
-///
+///
+///
/// To avoid ambiguity, a document containing TypeSystemDefinition
/// is invalid for execution.
-///
+///
+///
/// GraphQL documents not intended to be directly executed may
/// include TypeSystemDefinition.
-///
-/// https://spec.graphql.org/June2018/#sec-Executable-Definitions
+///
+/// https://spec.graphql.org/June2018/#sec-Executable-Definitions
///
internal sealed class DocumentRule : IDocumentValidatorRule
{
+ public ushort Priority => ushort.MaxValue;
+
public bool IsCacheable => true;
public void Validate(IDocumentValidatorContext context, DocumentNode document)
diff --git a/src/HotChocolate/Core/src/Validation/Rules/IntrospectionDepthVisitor.cs b/src/HotChocolate/Core/src/Validation/Rules/IntrospectionDepthVisitor.cs
new file mode 100644
index 00000000000..c274f1ab743
--- /dev/null
+++ b/src/HotChocolate/Core/src/Validation/Rules/IntrospectionDepthVisitor.cs
@@ -0,0 +1,77 @@
+using HotChocolate.Language;
+using HotChocolate.Language.Visitors;
+using HotChocolate.Types;
+using HotChocolate.Types.Introspection;
+using HotChocolate.Utilities;
+
+namespace HotChocolate.Validation.Rules;
+
+///
+/// This rules ensures that recursive introspection fields cannot be used
+/// to create endless cycles.
+///
+internal sealed class IntrospectionDepthVisitor : TypeDocumentValidatorVisitor
+{
+ private readonly (SchemaCoordinate Coordinate, ushort MaxAllowed)[] _limits =
+ [
+ (new SchemaCoordinate("__Type", "fields"), 1),
+ (new SchemaCoordinate("__Type", "inputFields"), 1),
+ (new SchemaCoordinate("__Type", "interfaces"), 1),
+ (new SchemaCoordinate("__Type", "possibleTypes"), 1),
+ (new SchemaCoordinate("__Type", "ofType"), 8)
+ ];
+
+ protected override ISyntaxVisitorAction Enter(
+ DocumentNode node,
+ IDocumentValidatorContext context)
+ {
+ context.FieldDepth.Initialize(_limits);
+ return base.Enter(node, context);
+ }
+
+ protected override ISyntaxVisitorAction Enter(
+ FieldNode node,
+ IDocumentValidatorContext context)
+ {
+ if (IntrospectionFields.TypeName.EqualsOrdinal(node.Name.Value))
+ {
+ return Skip;
+ }
+
+ if (context.Types.TryPeek(out var type)
+ && type.NamedType() is IComplexOutputType ot
+ && ot.Fields.TryGetField(node.Name.Value, out var of))
+ {
+ // we are only interested in fields if the root field is either
+ // __type or __schema.
+ if (context.OutputFields.Count == 0
+ && !of.IsIntrospectionField)
+ {
+ return Skip;
+ }
+
+ if (!context.FieldDepth.Add(of.Coordinate))
+ {
+ context.ReportMaxIntrospectionDepthOverflow(node);
+ return Break;
+ }
+
+ context.OutputFields.Push(of);
+ context.Types.Push(of.Type);
+ return Continue;
+ }
+
+ context.UnexpectedErrorsDetected = true;
+ return Skip;
+ }
+
+ protected override ISyntaxVisitorAction Leave(
+ FieldNode node,
+ IDocumentValidatorContext context)
+ {
+ context.FieldDepth.Remove(context.OutputFields.Peek().Coordinate);
+ context.Types.Pop();
+ context.OutputFields.Pop();
+ return Continue;
+ }
+}
diff --git a/src/HotChocolate/Core/src/Validation/Rules/IntrospectionVisitor.cs b/src/HotChocolate/Core/src/Validation/Rules/IntrospectionVisitor.cs
index a907972b97d..b0b576f534c 100644
--- a/src/HotChocolate/Core/src/Validation/Rules/IntrospectionVisitor.cs
+++ b/src/HotChocolate/Core/src/Validation/Rules/IntrospectionVisitor.cs
@@ -36,7 +36,6 @@ protected override ISyntaxVisitorAction Enter(
if (context.Types.TryPeek(out var type))
{
var namedType = type.NamedType();
-
if (context.Schema.QueryType == namedType &&
(IntrospectionFields.Schema.EqualsOrdinal(node.Name.Value) ||
IntrospectionFields.Type.EqualsOrdinal(node.Name.Value)))
@@ -45,36 +44,10 @@ protected override ISyntaxVisitorAction Enter(
return Break;
}
- if (namedType is IComplexOutputType ct)
- {
- if (ct.Fields.TryGetField(node.Name.Value, out var of))
- {
- if (node.SelectionSet is null ||
- node.SelectionSet.Selections.Count == 0 ||
- of.Type.NamedType().IsLeafType())
- {
- return Skip;
- }
-
- context.OutputFields.Push(of);
- context.Types.Push(of.Type);
- return Continue;
- }
-
- return Skip;
- }
+ return Skip;
}
context.UnexpectedErrorsDetected = true;
return Skip;
}
-
- protected override ISyntaxVisitorAction Leave(
- FieldNode node,
- IDocumentValidatorContext context)
- {
- context.OutputFields.Pop();
- context.Types.Pop();
- return Continue;
- }
}
diff --git a/src/HotChocolate/Core/src/Validation/Rules/MaxAllowedFieldCycleDepthVisitor.cs b/src/HotChocolate/Core/src/Validation/Rules/MaxAllowedFieldCycleDepthVisitor.cs
new file mode 100644
index 00000000000..a59ee506cc7
--- /dev/null
+++ b/src/HotChocolate/Core/src/Validation/Rules/MaxAllowedFieldCycleDepthVisitor.cs
@@ -0,0 +1,75 @@
+using System.Collections.Immutable;
+using HotChocolate.Language;
+using HotChocolate.Language.Visitors;
+using HotChocolate.Types;
+using HotChocolate.Types.Introspection;
+using HotChocolate.Utilities;
+
+namespace HotChocolate.Validation.Rules;
+
+///
+/// This rules allows to limit cycles across unique field coordinates.
+///
+///
+/// Specifies specific coordinate cycle limits.
+///
+///
+/// Specifies the default coordinate cycle limit.
+///
+internal sealed class MaxAllowedFieldCycleDepthVisitor(
+ ImmutableArray<(SchemaCoordinate Coordinate, ushort MaxAllowed)> coordinateCycleLimits,
+ ushort? defaultCycleLimit)
+ : TypeDocumentValidatorVisitor
+{
+ protected override ISyntaxVisitorAction Enter(
+ DocumentNode node,
+ IDocumentValidatorContext context)
+ {
+ context.FieldDepth.Initialize(coordinateCycleLimits, defaultCycleLimit);
+ return base.Enter(node, context);
+ }
+
+ protected override ISyntaxVisitorAction Enter(
+ FieldNode node,
+ IDocumentValidatorContext context)
+ {
+ if (IntrospectionFields.TypeName.EqualsOrdinal(node.Name.Value))
+ {
+ return Skip;
+ }
+
+ if (context.Types.TryPeek(out var type)
+ && type.NamedType() is IComplexOutputType ot
+ && ot.Fields.TryGetField(node.Name.Value, out var of))
+ {
+ // we are ignoring introspection fields in this visitor.
+ if (of.IsIntrospectionField)
+ {
+ return Skip;
+ }
+
+ if (!context.FieldDepth.Add(of.Coordinate))
+ {
+ context.ReportMaxCoordinateCycleDepthOverflow(node);
+ return Break;
+ }
+
+ context.OutputFields.Push(of);
+ context.Types.Push(of.Type);
+ return Continue;
+ }
+
+ context.UnexpectedErrorsDetected = true;
+ return Skip;
+ }
+
+ protected override ISyntaxVisitorAction Leave(
+ FieldNode node,
+ IDocumentValidatorContext context)
+ {
+ context.FieldDepth.Remove(context.OutputFields.Peek().Coordinate);
+ context.Types.Pop();
+ context.OutputFields.Pop();
+ return Continue;
+ }
+}
diff --git a/src/HotChocolate/Core/src/Validation/Rules/MaxExecutionDepthVisitor.cs b/src/HotChocolate/Core/src/Validation/Rules/MaxExecutionDepthVisitor.cs
index b1f5802892f..e9fa0eb7d67 100644
--- a/src/HotChocolate/Core/src/Validation/Rules/MaxExecutionDepthVisitor.cs
+++ b/src/HotChocolate/Core/src/Validation/Rules/MaxExecutionDepthVisitor.cs
@@ -5,15 +5,10 @@
namespace HotChocolate.Validation.Rules;
-internal sealed class MaxExecutionDepthVisitor : DocumentValidatorVisitor
+internal sealed class MaxExecutionDepthVisitor(
+ IMaxExecutionDepthOptionsAccessor options)
+ : DocumentValidatorVisitor
{
- private readonly IMaxExecutionDepthOptionsAccessor _options;
-
- public MaxExecutionDepthVisitor(IMaxExecutionDepthOptionsAccessor options)
- {
- _options = options;
- }
-
protected override ISyntaxVisitorAction Enter(
DocumentNode node,
IDocumentValidatorContext context)
@@ -33,9 +28,9 @@ protected override ISyntaxVisitorAction Enter(
}
// otherwise we will go with the configured value
- else if(_options.MaxAllowedExecutionDepth.HasValue)
+ else if(options.MaxAllowedExecutionDepth.HasValue)
{
- context.Allowed = _options.MaxAllowedExecutionDepth.Value;
+ context.Allowed = options.MaxAllowedExecutionDepth.Value;
}
// if there is no configured value we will just stop traversing the graph
@@ -80,7 +75,7 @@ protected override ISyntaxVisitorAction Enter(
FieldNode node,
IDocumentValidatorContext context)
{
- if (_options.SkipIntrospectionFields &&
+ if (options.SkipIntrospectionFields &&
node.Name.Value.StartsWith("__"))
{
return Skip;
diff --git a/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_Validation.Tests.cs b/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_Validation.Tests.cs
index dc08689167b..07415f0c22e 100644
--- a/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_Validation.Tests.cs
+++ b/src/HotChocolate/Core/test/Execution.Tests/DependencyInjection/RequestExecutorBuilderExtensions_Validation.Tests.cs
@@ -207,6 +207,7 @@ public class MockVisitor : DocumentValidatorVisitor;
public class MockRule : IDocumentValidatorRule
{
+ public ushort Priority => ushort.MaxValue;
public bool IsCacheable => true;
public void Validate(IDocumentValidatorContext context, DocumentNode document)
diff --git a/src/HotChocolate/Core/test/Types.Analyzers.Tests/IntegrationTests.cs b/src/HotChocolate/Core/test/Types.Analyzers.Tests/IntegrationTests.cs
index 80bd0bd36d4..497e0886829 100644
--- a/src/HotChocolate/Core/test/Types.Analyzers.Tests/IntegrationTests.cs
+++ b/src/HotChocolate/Core/test/Types.Analyzers.Tests/IntegrationTests.cs
@@ -146,7 +146,7 @@ private static IServiceProvider CreateApplicationServices(
.AddSingleton();
serviceCollection
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddCustomModule()
.AddGlobalObjectIdentification()
.AddMutationConventions();
diff --git a/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorTests.cs b/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorTests.cs
index bea9c2ece04..9744480ffe7 100644
--- a/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorTests.cs
+++ b/src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorTests.cs
@@ -861,6 +861,12 @@ public async Task Produce_Many_Errors_50000_query()
await ExpectErrors(FileResource.Open("50000_query.graphql"));
}
+ [Fact]
+ public async Task Introspection_Cycle_Detected()
+ {
+ await ExpectErrors(FileResource.Open("introspection_with_cycle.graphql"));
+ }
+
private Task ExpectValid(string sourceText) => ExpectValid(null, null, sourceText);
private async Task ExpectValid(ISchema? schema, IDocumentValidator? validator, string sourceText)
diff --git a/src/HotChocolate/Core/test/Validation.Tests/HotChocolate.Validation.Tests.csproj b/src/HotChocolate/Core/test/Validation.Tests/HotChocolate.Validation.Tests.csproj
index ca7f3e84ba0..33e66b35bc2 100644
--- a/src/HotChocolate/Core/test/Validation.Tests/HotChocolate.Validation.Tests.csproj
+++ b/src/HotChocolate/Core/test/Validation.Tests/HotChocolate.Validation.Tests.csproj
@@ -18,5 +18,8 @@
Always
+
+ Always
+
diff --git a/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs b/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs
new file mode 100644
index 00000000000..4f682209691
--- /dev/null
+++ b/src/HotChocolate/Core/test/Validation.Tests/IntrospectionDepthRuleTests.cs
@@ -0,0 +1,205 @@
+using CookieCrumble;
+using HotChocolate.Language;
+using HotChocolate.Validation.Options;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace HotChocolate.Validation;
+
+public class IntrospectionDepthRuleTests()
+ : DocumentValidatorVisitorTestBase(b => b.AddIntrospectionDepthRule())
+{
+ [Fact] public void Introspection_With_Cycles_Will_Fail()
+ {
+ // arrange
+ IDocumentValidatorContext context = ValidationUtils.CreateContext();
+
+ var query = Utf8GraphQLParser.Parse(FileResource.Open("introspection_with_cycle.graphql"));
+ context.Prepare(query);
+
+ // act
+ Rule.Validate(context, query);
+
+ // assert
+ Assert.Equal(
+ "Maximum allowed introspection depth exceeded.",
+ Assert.Single(context.Errors).Message);
+ }
+
+ [Fact]
+ public void Introspection_Without_Cycles()
+ {
+ // arrange
+ IDocumentValidatorContext context = ValidationUtils.CreateContext();
+
+ var query = Utf8GraphQLParser.Parse(FileResource.Open("introspection_without_cycle.graphql"));
+ context.Prepare(query);
+
+ // act
+ Rule.Validate(context, query);
+
+ // assert
+ Assert.Empty(context.Errors);
+ }
+}
+
+public class MaxAllowedFieldCycleDepthRuleTests()
+ : DocumentValidatorVisitorTestBase(b => b.AddMaxAllowedFieldCycleDepthRule())
+{
+ [Fact]
+ public void Max_3_Cycles_Allowed_Success()
+ {
+ // arrange
+ IDocumentValidatorContext context = ValidationUtils.CreateContext();
+
+ var query = Utf8GraphQLParser.Parse(
+ """
+ {
+ human {
+ relatives {
+ relatives {
+ relatives {
+ name
+ }
+ }
+ }
+ }
+ }
+ """);
+ context.Prepare(query);
+
+ // act
+ Rule.Validate(context, query);
+
+ // assert
+ Assert.Empty(context.Errors);
+ Assert.False(context.FatalErrorDetected);
+ Assert.False(context.UnexpectedErrorsDetected);
+ }
+
+ [Fact]
+ public void Max_3_Cycles_Allowed_Fail()
+ {
+ // arrange
+ IDocumentValidatorContext context = ValidationUtils.CreateContext();
+
+ var query = Utf8GraphQLParser.Parse(
+ """
+ {
+ human {
+ relatives {
+ relatives {
+ relatives {
+ relatives {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ """);
+ context.Prepare(query);
+
+ // act
+ Rule.Validate(context, query);
+
+ // assert
+ Assert.Equal(
+ "Maximum allowed coordinate cycle depth was exceeded.",
+ Assert.Single(context.Errors).Message);
+ Assert.True(context.FatalErrorDetected);
+ Assert.False(context.UnexpectedErrorsDetected);
+ }
+
+ [Fact]
+ public void Max_2_Relative_Field_Allowed_Success()
+ {
+ // arrange
+ var serviceCollection = new ServiceCollection();
+
+ var builder = serviceCollection
+ .AddValidation()
+ .ConfigureValidation(c => c.Modifiers.Add(o => o.Rules.Clear()))
+ .ModifyValidationOptions(o => o.MaxAllowedErrors = int.MaxValue);
+ builder.AddMaxAllowedFieldCycleDepthRule(
+ null,
+ [(new SchemaCoordinate("Human", "relatives"), 2)]);
+
+ IServiceProvider services = serviceCollection.BuildServiceProvider();
+
+ var rule = services
+ .GetRequiredService()
+ .GetRules(Schema.DefaultName).First();
+
+ IDocumentValidatorContext context = ValidationUtils.CreateContext();
+
+ var query = Utf8GraphQLParser.Parse(
+ """
+ {
+ human {
+ relatives {
+ relatives {
+ name
+ }
+ }
+ }
+ }
+ """);
+ context.Prepare(query);
+
+ // act
+ rule.Validate(context, query);
+
+ // assert
+ Assert.Empty(context.Errors);
+ Assert.False(context.FatalErrorDetected);
+ Assert.False(context.UnexpectedErrorsDetected);
+ }
+
+ [Fact]
+ public void Max_1_Relative_Field_Allowed_Fail()
+ {
+ // arrange
+ var serviceCollection = new ServiceCollection();
+
+ var builder = serviceCollection
+ .AddValidation()
+ .ConfigureValidation(c => c.Modifiers.Add(o => o.Rules.Clear()))
+ .ModifyValidationOptions(o => o.MaxAllowedErrors = int.MaxValue);
+ builder.AddMaxAllowedFieldCycleDepthRule(
+ null,
+ [(new SchemaCoordinate("Human", "relatives"), 1)]);
+
+ IServiceProvider services = serviceCollection.BuildServiceProvider();
+
+ var rule = services
+ .GetRequiredService()
+ .GetRules(Schema.DefaultName).First();
+
+ IDocumentValidatorContext context = ValidationUtils.CreateContext();
+
+ var query = Utf8GraphQLParser.Parse(
+ """
+ {
+ human {
+ relatives {
+ relatives {
+ name
+ }
+ }
+ }
+ }
+ """);
+ context.Prepare(query);
+
+ // act
+ rule.Validate(context, query);
+
+ // assert
+ Assert.Equal(
+ "Maximum allowed coordinate cycle depth was exceeded.",
+ Assert.Single(context.Errors).Message);
+ Assert.True(context.FatalErrorDetected);
+ Assert.False(context.UnexpectedErrorsDetected);
+ }
+}
diff --git a/src/HotChocolate/Core/test/Validation.Tests/__resources__/introspection_with_cycle.graphql b/src/HotChocolate/Core/test/Validation.Tests/__resources__/introspection_with_cycle.graphql
new file mode 100644
index 00000000000..ac0a3e54237
--- /dev/null
+++ b/src/HotChocolate/Core/test/Validation.Tests/__resources__/introspection_with_cycle.graphql
@@ -0,0 +1,93 @@
+query IntrospectionQuery {
+ __schema {
+ queryType {
+ name
+ }
+ mutationType {
+ name
+ }
+ types {
+ ... FullType
+ }
+ directives {
+ name
+ description
+ args {
+ ... InputValue
+ }
+ onOperation
+ onFragment
+ onField
+ }
+ }
+}
+
+fragment FullType on __Type {
+ kind
+ name
+ description
+ fields(includeDeprecated: true) {
+ name
+ description
+ args {
+ ... InputValue
+ }
+ type {
+ ... TypeRef
+ fields {
+ name
+ }
+ }
+ isDeprecated
+ deprecationReason
+ }
+ inputFields {
+ ... InputValue
+ }
+ interfaces {
+ ... TypeRef
+ }
+ enumValues(includeDeprecated: true) {
+ name
+ description
+ isDeprecated
+ deprecationReason
+ }
+ possibleTypes {
+ ... TypeRef
+ }
+}
+
+fragment InputValue on __InputValue {
+ name
+ description
+ type {
+ ... TypeRef
+ }
+ defaultValue
+}
+
+fragment TypeRef on __Type {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/HotChocolate/Core/test/Validation.Tests/__resources__/introspection_without_cycle.graphql b/src/HotChocolate/Core/test/Validation.Tests/__resources__/introspection_without_cycle.graphql
new file mode 100644
index 00000000000..bab1d210a34
--- /dev/null
+++ b/src/HotChocolate/Core/test/Validation.Tests/__resources__/introspection_without_cycle.graphql
@@ -0,0 +1,90 @@
+query IntrospectionQuery {
+ __schema {
+ queryType {
+ name
+ }
+ mutationType {
+ name
+ }
+ types {
+ ... FullType
+ }
+ directives {
+ name
+ description
+ args {
+ ... InputValue
+ }
+ onOperation
+ onFragment
+ onField
+ }
+ }
+}
+
+fragment FullType on __Type {
+ kind
+ name
+ description
+ fields(includeDeprecated: true) {
+ name
+ description
+ args {
+ ... InputValue
+ }
+ type {
+ ... TypeRef
+ }
+ isDeprecated
+ deprecationReason
+ }
+ inputFields {
+ ... InputValue
+ }
+ interfaces {
+ ... TypeRef
+ }
+ enumValues(includeDeprecated: true) {
+ name
+ description
+ isDeprecated
+ deprecationReason
+ }
+ possibleTypes {
+ ... TypeRef
+ }
+}
+
+fragment InputValue on __InputValue {
+ name
+ description
+ type {
+ ... TypeRef
+ }
+ defaultValue
+}
+
+fragment TypeRef on __Type {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/DocumentValidatorTests.Introspection_Cycle_Detected.snap b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/DocumentValidatorTests.Introspection_Cycle_Detected.snap
new file mode 100644
index 00000000000..5599ef7c648
--- /dev/null
+++ b/src/HotChocolate/Core/test/Validation.Tests/__snapshots__/DocumentValidatorTests.Introspection_Cycle_Detected.snap
@@ -0,0 +1,41 @@
+[
+ {
+ "Message": "Maximum allowed introspection depth exceeded.",
+ "Code": "HC0086",
+ "Path": {
+ "Name": "type",
+ "Parent": {
+ "Name": "fields",
+ "Parent": {
+ "Name": "types",
+ "Parent": {
+ "Name": "__schema",
+ "Parent": {
+ "Parent": null,
+ "Length": 0,
+ "IsRoot": true
+ },
+ "Length": 1,
+ "IsRoot": false
+ },
+ "Length": 2,
+ "IsRoot": false
+ },
+ "Length": 3,
+ "IsRoot": false
+ },
+ "Length": 4,
+ "IsRoot": false
+ },
+ "Locations": [
+ {
+ "Line": 37,
+ "Column": 7
+ }
+ ],
+ "Extensions": {
+ "code": "HC0086"
+ },
+ "Exception": null
+ }
+]
diff --git a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs
index bf78691069c..39fd20bca4e 100644
--- a/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs
+++ b/src/HotChocolate/Fusion/src/Core/DependencyInjection/FusionRequestExecutorBuilderExtensions.cs
@@ -53,7 +53,7 @@ public static FusionGatewayBuilder AddFusionGatewayServer(
sp.GetRequiredService()));
var builder = services
- .AddGraphQLServer(graphName, disableCostAnalyzer: true)
+ .AddGraphQLServer(graphName, disableDefaultSecurity: true)
.UseField(next => next)
.AddOperationCompilerOptimizer()
.AddOperationCompilerOptimizer()
diff --git a/src/HotChocolate/Fusion/test/Shared/DemoProject.cs b/src/HotChocolate/Fusion/test/Shared/DemoProject.cs
index a2889806c47..a5abf239d40 100644
--- a/src/HotChocolate/Fusion/test/Shared/DemoProject.cs
+++ b/src/HotChocolate/Fusion/test/Shared/DemoProject.cs
@@ -92,7 +92,7 @@ public static async Task CreateAsync(CancellationToken ct = default
s => s
.AddRouting()
.AddSingleton()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.AddMutationType()
.AddSubscriptionType()
@@ -115,7 +115,7 @@ public static async Task CreateAsync(CancellationToken ct = default
s => s
.AddRouting()
.AddSingleton()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.AddMutationType()
.AddSubscriptionType()
@@ -138,7 +138,7 @@ public static async Task CreateAsync(CancellationToken ct = default
s => s
.AddRouting()
.AddSingleton()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.AddMutationType()
.AddMutationConventions()
@@ -159,7 +159,7 @@ public static async Task CreateAsync(CancellationToken ct = default
s => s
.AddRouting()
.AddSingleton()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.AddMutationType()
.AddGlobalObjectIdentification()
@@ -180,7 +180,7 @@ public static async Task CreateAsync(CancellationToken ct = default
var shipping = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.ConfigureSchema(b => b.SetContextData(GlobalIdSupportEnabled, 1))
.AddConvention(_ => new DefaultNamingConventions()),
@@ -198,7 +198,7 @@ public static async Task CreateAsync(CancellationToken ct = default
var shipping2 = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.ConfigureSchema(b => b.SetContextData(GlobalIdSupportEnabled, 1))
.AddConvention(_ => new DefaultNamingConventions()),
@@ -216,7 +216,7 @@ public static async Task CreateAsync(CancellationToken ct = default
var appointment = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.AddObjectType()
.AddObjectType()
@@ -236,7 +236,7 @@ public static async Task CreateAsync(CancellationToken ct = default
var patient1 = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.AddGlobalObjectIdentification()
.AddConvention(_ => new DefaultNamingConventions()),
@@ -254,7 +254,7 @@ public static async Task CreateAsync(CancellationToken ct = default
var books = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.AddConvention(_ => new DefaultNamingConventions()),
c => c
@@ -271,7 +271,7 @@ public static async Task CreateAsync(CancellationToken ct = default
var authors = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.AddConvention(_ => new DefaultNamingConventions()),
c => c
@@ -288,7 +288,7 @@ public static async Task CreateAsync(CancellationToken ct = default
var resale = testServerFactory.Create(
s => s
.AddRouting()
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddQueryType()
.AddGlobalObjectIdentification()
.AddMutationConventions()
diff --git a/src/HotChocolate/Language/src/Language/ListExtensions.cs b/src/HotChocolate/Language/src/Language/ListExtensions.cs
index c4e6bb5cfe3..92ba6f4ab10 100644
--- a/src/HotChocolate/Language/src/Language/ListExtensions.cs
+++ b/src/HotChocolate/Language/src/Language/ListExtensions.cs
@@ -16,7 +16,7 @@ public static T Pop(this IList list)
return p;
}
- public static bool TryPop(this IList list, [NotNullWhen(true)] out T item)
+ public static bool TryPop(this IList list, [MaybeNullWhen(false)] out T item)
{
if (list.Count > 0)
{
diff --git a/src/HotChocolate/Raven/test/Data.Raven.Tests/AnnotationBasedTests.cs b/src/HotChocolate/Raven/test/Data.Raven.Tests/AnnotationBasedTests.cs
index f8b2dc13c1e..bb36949a61f 100644
--- a/src/HotChocolate/Raven/test/Data.Raven.Tests/AnnotationBasedTests.cs
+++ b/src/HotChocolate/Raven/test/Data.Raven.Tests/AnnotationBasedTests.cs
@@ -242,7 +242,7 @@ public async Task Executable_Should_Work()
public ValueTask CreateExecutorAsync() => new ServiceCollection()
.AddSingleton(CreateDocumentStore())
- .AddGraphQLServer(disableCostAnalyzer: true)
+ .AddGraphQLServer(disableDefaultSecurity: true)
.AddRavenFiltering()
.AddRavenProjections()
.AddRavenSorting()