Skip to content

Commit

Permalink
Added Introspection Cycle Detection Rule
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelstaib committed Sep 27, 2024
1 parent a190f03 commit d4c48d9
Show file tree
Hide file tree
Showing 27 changed files with 473 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:

jobs:
release:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3
Expand Down
5 changes: 5 additions & 0 deletions src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ public static class Validation
/// The introspection is not allowed for the current request
/// </summary>
public const string IntrospectionNotAllowed = "HC0046";

/// <summary>
/// The maximum allowed introspection depth was exceeded.
/// </summary>
public const string MaxIntrospectionDepthOverflow = "HC0086";
}

/// <summary>
Expand Down
6 changes: 3 additions & 3 deletions src/HotChocolate/Core/src/Abstractions/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.7, )",
"resolved": "8.0.7",
"contentHash": "iI52ptEKby2ymQ6B7h4TWbFmm85T4VvLgc/HvS45Yr3lgi4IIFbQtjON3bQbX/Vc94jXNSLvrDOp5Kh7SJyFYQ=="
"requested": "[8.0.8, )",
"resolved": "8.0.8",
"contentHash": "P8wR6MUWwYXIjPJuBaZgo5zlI/GWI6QEAo6NyVIbPefa9CCkohYu7dP2rD/mrqnjEqfRHyl+h9VZrDoGpELqYg=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
Expand Down
6 changes: 3 additions & 3 deletions src/HotChocolate/Core/src/Types.Shared/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.7, )",
"resolved": "8.0.7",
"contentHash": "iI52ptEKby2ymQ6B7h4TWbFmm85T4VvLgc/HvS45Yr3lgi4IIFbQtjON3bQbX/Vc94jXNSLvrDOp5Kh7SJyFYQ=="
"requested": "[8.0.8, )",
"resolved": "8.0.8",
"contentHash": "P8wR6MUWwYXIjPJuBaZgo5zlI/GWI6QEAo6NyVIbPefa9CCkohYu7dP2rD/mrqnjEqfRHyl+h9VZrDoGpELqYg=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
Expand Down
27 changes: 27 additions & 0 deletions src/HotChocolate/Core/src/Validation/CoordinateLimit.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 5 additions & 0 deletions src/HotChocolate/Core/src/Validation/DocumentValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ public ValueTask<DocumentValidatorResult> ValidateAsync(
for (var i = 0; i < length; i++)
{
Unsafe.Add(ref start, i).Validate(context, document);

if (context.FatalErrorDetected)
{
break;
}
}

if (_aggregators.Length == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public IOutputType NonNullString

public bool UnexpectedErrorsDetected { get; set; }

public bool FatalErrorDetected { get; set; }

public int Count { get; set; }

public int Max { get; set; }
Expand All @@ -112,6 +114,8 @@ public IOutputType NonNullString

public HashSet<FieldInfoPair> ProcessedFieldPairs { get; } = [];

public FieldDepthCycleTracker FieldDepth { get; } = new();

public IList<FieldInfo> RentFieldInfoList()
{
var buffer = _buffers.Peek();
Expand Down Expand Up @@ -168,7 +172,9 @@ public void Clear()
CurrentFieldPairs.Clear();
NextFieldPairs.Clear();
ProcessedFieldPairs.Clear();
FieldDepth.Reset();
UnexpectedErrorsDetected = false;
FatalErrorDetected = false;
Count = 0;
Max = 0;
Allowed = 0;
Expand Down
14 changes: 14 additions & 0 deletions src/HotChocolate/Core/src/Validation/ErrorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -725,4 +725,18 @@ 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)
.SetSyntaxNode(selection)
.SetPath(context.CreateErrorPath())
.Build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -342,4 +342,11 @@ public static IValidationBuilder AddMaxExecutionDepthRule(
public static IValidationBuilder AddIntrospectionAllowedRule(
this IValidationBuilder builder) =>
builder.TryAddValidationVisitor((_, _) => new IntrospectionVisitor(), false);

/// <summary>
/// Adds a validation rule that restricts the depth of a GraphQL introspection request.
/// </summary>
public static IValidationBuilder AddIntrospectionDepthRule(
this IValidationBuilder builder)
=> builder.TryAddValidationVisitor<IntrospectionDepthVisitor>();
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static IValidationBuilder AddValidation(
var builder = new DefaultValidationBuilder(schemaName, services);

builder
.AddIntrospectionDepthRule()
.AddDocumentRules()
.AddOperationRules()
.AddFieldRules()
Expand Down
90 changes: 90 additions & 0 deletions src/HotChocolate/Core/src/Validation/FieldDepthCycleTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Collections.Generic;
using HotChocolate.Language;

namespace HotChocolate.Validation;

/// <summary>
/// Allows to track field cycle depths in a GraphQL query.
/// </summary>
public sealed class FieldDepthCycleTracker
{
private readonly Dictionary<FieldCoordinate, CoordinateLimit> _coordinates = new();
private readonly List<CoordinateLimit> _limits = new();
private ushort? _defaultMaxAllowed;

/// <summary>
/// Adds a field coordinate to the tracker.
/// </summary>
/// <param name="coordinate">
/// A field coordinate.
/// </param>
/// <returns>
/// <c>true</c> if the field coordinate has not reached its cycle depth limit;
/// otherwise, <c>false</c>.
/// </returns>
public bool Add(FieldCoordinate 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;
}

/// <summary>
/// Removes a field coordinate from the tracker.
/// </summary>
/// <param name="coordinate">
/// A field coordinate.
/// </param>
public void Remove(FieldCoordinate coordinate)
{
if (_coordinates.TryGetValue(coordinate, out var limit))
{
limit.Remove();
}
}

/// <summary>
/// Initializes the field depth tracker with the specified limits.
/// </summary>
/// <param name="limits">
/// A collection of field coordinates and their cycle depth limits.
/// </param>
/// <param name="defaultMaxAllowed">
/// The default cycle depth limit for coordinates that were not explicitly defined.
/// </param>
public void Initialize(
IEnumerable<(FieldCoordinate 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;
}

/// <summary>
/// Resets the field depth tracker.
/// </summary>
public void Reset()
{
_limits.AddRange(_coordinates.Values);
_coordinates.Clear();
}
}
10 changes: 10 additions & 0 deletions src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ public interface IDocumentValidatorContext : ISyntaxVisitorContext
/// </summary>
bool UnexpectedErrorsDetected { get; set; }

/// <summary>
/// Defines that a fatal error was detected and that the analyzer will be aborted.
/// </summary>
bool FatalErrorDetected { get; set; }

/// <summary>
/// A map to store arbitrary visitor data.
/// </summary>
Expand All @@ -175,6 +180,11 @@ public interface IDocumentValidatorContext : ISyntaxVisitorContext
/// </summary>
HashSet<FieldInfoPair> ProcessedFieldPairs { get; }

/// <summary>
/// Gets the field depth cycle tracker.
/// </summary>
FieldDepthCycleTracker FieldDepth { get; }

/// <summary>
/// Rents a list of field infos.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// This rules ensures that recursive introspection fields cannot be used
/// to create endless cycles.
/// </summary>
internal sealed class IntrospectionDepthVisitor : TypeDocumentValidatorVisitor
{
private readonly (FieldCoordinate Coordinate, ushort MaxAllowed)[] _limits =
[
(new FieldCoordinate("__Type", "fields"), 1),
(new FieldCoordinate("__Type", "inputFields"), 1),
(new FieldCoordinate("__Type", "interfaces"), 1),
(new FieldCoordinate("__Type", "possibleTypes"), 1),
(new FieldCoordinate("__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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,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)
Expand Down
Loading

0 comments on commit d4c48d9

Please sign in to comment.