Skip to content

Commit 5fd5a50

Browse files
committed
Added Introspection Cycle Detection
1 parent fdb584c commit 5fd5a50

14 files changed

+435
-6
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77

88
jobs:
99
release:
10-
runs-on: ubuntu-latest
10+
runs-on: ubuntu-22.04
1111
steps:
1212
- name: Checkout
1313
uses: actions/checkout@v3

src/HotChocolate/Core/src/Abstractions/ErrorCodes.cs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,21 @@ public static class Execution
6565

6666
/// <summary>
6767
/// The Oneof Input Objects `{0}` require that exactly one field must be supplied and that
68-
/// field must not be `null`. Oneof Input Objects are a special variant of Input Objects
68+
/// field must not be `null`. Oneof Input Objects are a special variant of Input Objects
6969
/// where the type system asserts that exactly one of the fields must be set and non-null.
7070
/// </summary>
7171
public const string OneOfNoFieldSet = "HC0054";
7272

7373
/// <summary>
74-
/// More than one field of the Oneof Input Object `{0}` is set. Oneof Input Objects
75-
/// are a special variant of Input Objects where the type system asserts that exactly
74+
/// More than one field of the Oneof Input Object `{0}` is set. Oneof Input Objects
75+
/// are a special variant of Input Objects where the type system asserts that exactly
7676
/// one of the fields must be set and non-null.
7777
/// </summary>
7878
public const string OneOfMoreThanOneFieldSet = "HC0055";
7979

8080
/// <summary>
81-
/// `null` was set to the field `{0}`of the Oneof Input Object `{1}`. Oneof Input Objects
82-
/// are a special variant of Input Objects where the type system asserts that exactly
81+
/// `null` was set to the field `{0}`of the Oneof Input Object `{1}`. Oneof Input Objects
82+
/// are a special variant of Input Objects where the type system asserts that exactly
8383
/// one of the fields must be set and non-null.
8484
/// </summary>
8585
public const string OneOfFieldIsNull = "HC0056";
@@ -268,6 +268,11 @@ public static class Validation
268268
/// The introspection is not allowed for the current request
269269
/// </summary>
270270
public const string IntrospectionNotAllowed = "HC0046";
271+
272+
/// <summary>
273+
/// The maximum allowed introspection depth was exceeded.
274+
/// </summary>
275+
public const string MaxIntrospectionDepthOverflow = "HC0086";
271276
}
272277

273278
/// <summary>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
namespace HotChocolate.Validation;
2+
3+
internal sealed class CoordinateLimit
4+
{
5+
public ushort MaxAllowed { get; private set; }
6+
7+
public ushort Count { get; private set; }
8+
9+
public bool Add()
10+
{
11+
if (Count < MaxAllowed)
12+
{
13+
Count++;
14+
return true;
15+
}
16+
17+
return false;
18+
}
19+
20+
public void Remove() => Count--;
21+
22+
public void Reset(ushort maxAllowed)
23+
{
24+
MaxAllowed = maxAllowed;
25+
Count = 0;
26+
}
27+
}

src/HotChocolate/Core/src/Validation/DocumentValidator.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ public DocumentValidatorResult Validate(
8989
foreach (IDocumentValidatorRule? rule in rules)
9090
{
9191
rule.Validate(context, document);
92+
93+
if (context.FatalErrorDetected)
94+
{
95+
break;
96+
}
9297
}
9398

9499
return context.Errors.Count > 0

src/HotChocolate/Core/src/Validation/DocumentValidatorContext.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public IOutputType NonNullString
9494

9595
public bool UnexpectedErrorsDetected { get; set; }
9696

97+
public bool FatalErrorDetected { get; set; }
98+
9799
public int Count { get; set; }
98100

99101
public int Max { get; set; }
@@ -106,6 +108,8 @@ public IOutputType NonNullString
106108

107109
public HashSet<FieldInfoPair> ProcessedFieldPairs { get; } = new();
108110

111+
public FieldDepthCycleTracker FieldDepth { get; } = new();
112+
109113
public IList<FieldInfo> RentFieldInfoList()
110114
{
111115
FieldInfoListBuffer buffer = _buffers.Peek();
@@ -159,7 +163,9 @@ public void Clear()
159163
InputFields.Clear();
160164
_errors.Clear();
161165
List.Clear();
166+
FieldDepth.Reset();
162167
UnexpectedErrorsDetected = false;
168+
FatalErrorDetected = false;
163169
Count = 0;
164170
Max = 0;
165171
MaxAllowedErrors = 0;

src/HotChocolate/Core/src/Validation/ErrorHelper.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,4 +685,18 @@ public static IError OneOfVariablesMustBeNonNull(
685685
.SetExtension(nameof(field), field.ToString())
686686
.SpecifiedBy("sec-Oneof–Input-Objects-Have-Exactly-One-Field", rfc: 825)
687687
.Build();
688+
689+
public static void ReportMaxIntrospectionDepthOverflow(
690+
this IDocumentValidatorContext context,
691+
ISyntaxNode selection)
692+
{
693+
context.FatalErrorDetected = true;
694+
context.ReportError(
695+
ErrorBuilder.New()
696+
.SetMessage("Maximum allowed introspection depth exceeded.")
697+
.SetCode(ErrorCodes.Validation.MaxIntrospectionDepthOverflow)
698+
.SetSyntaxNode(selection)
699+
.SetPath(context.CreateErrorPath())
700+
.Build());
701+
}
688702
}

src/HotChocolate/Core/src/Validation/Extensions/ValidatiobBuilderExtensions.Rules.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,4 +326,11 @@ public static IValidationBuilder AddMaxExecutionDepthRule(
326326
public static IValidationBuilder AddIntrospectionAllowedRule(
327327
this IValidationBuilder builder) =>
328328
builder.TryAddValidationVisitor((_, _) => new IntrospectionVisitor(), false);
329+
330+
/// <summary>
331+
/// Adds a validation rule that restricts the depth of a GraphQL introspection request.
332+
/// </summary>
333+
public static IValidationBuilder AddIntrospectionDepthRule(
334+
this IValidationBuilder builder)
335+
=> builder.TryAddValidationVisitor<IntrospectionDepthVisitor>();
329336
}

src/HotChocolate/Core/src/Validation/Extensions/ValidationServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public static IValidationBuilder AddValidation(
2828
var builder = new DefaultValidationBuilder(schemaName, services);
2929

3030
builder
31+
.AddIntrospectionDepthRule()
3132
.AddDocumentRules()
3233
.AddOperationRules()
3334
.AddFieldRules()
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using System.Collections.Generic;
2+
using HotChocolate.Language;
3+
4+
namespace HotChocolate.Validation;
5+
6+
/// <summary>
7+
/// Allows to track field cycle depths in a GraphQL query.
8+
/// </summary>
9+
public sealed class FieldDepthCycleTracker
10+
{
11+
private readonly Dictionary<FieldCoordinate, CoordinateLimit> _coordinates = new();
12+
private readonly List<CoordinateLimit> _limits = new();
13+
private ushort? _defaultMaxAllowed;
14+
15+
/// <summary>
16+
/// Adds a field coordinate to the tracker.
17+
/// </summary>
18+
/// <param name="coordinate">
19+
/// A field coordinate.
20+
/// </param>
21+
/// <returns>
22+
/// <c>true</c> if the field coordinate has not reached its cycle depth limit;
23+
/// otherwise, <c>false</c>.
24+
/// </returns>
25+
public bool Add(FieldCoordinate coordinate)
26+
{
27+
if (_coordinates.TryGetValue(coordinate, out var limit))
28+
{
29+
return limit.Add();
30+
}
31+
32+
if(_defaultMaxAllowed.HasValue)
33+
{
34+
_limits.TryPop(out limit);
35+
limit ??= new CoordinateLimit();
36+
limit.Reset(_defaultMaxAllowed.Value);
37+
_coordinates.Add(coordinate, limit);
38+
return limit.Add();
39+
}
40+
41+
return true;
42+
}
43+
44+
/// <summary>
45+
/// Removes a field coordinate from the tracker.
46+
/// </summary>
47+
/// <param name="coordinate">
48+
/// A field coordinate.
49+
/// </param>
50+
public void Remove(FieldCoordinate coordinate)
51+
{
52+
if (_coordinates.TryGetValue(coordinate, out var limit))
53+
{
54+
limit.Remove();
55+
}
56+
}
57+
58+
/// <summary>
59+
/// Initializes the field depth tracker with the specified limits.
60+
/// </summary>
61+
/// <param name="limits">
62+
/// A collection of field coordinates and their cycle depth limits.
63+
/// </param>
64+
/// <param name="defaultMaxAllowed">
65+
/// The default cycle depth limit for coordinates that were not explicitly defined.
66+
/// </param>
67+
public void Initialize(
68+
IEnumerable<(FieldCoordinate Coordinate, ushort MaxAllowed)> limits,
69+
ushort? defaultMaxAllowed = null)
70+
{
71+
foreach (var (coordinate, maxAllowed) in limits)
72+
{
73+
_limits.TryPop(out var limit);
74+
limit ??= new CoordinateLimit();
75+
limit.Reset(maxAllowed);
76+
_coordinates.Add(coordinate, limit);
77+
}
78+
79+
_defaultMaxAllowed = defaultMaxAllowed;
80+
}
81+
82+
/// <summary>
83+
/// Resets the field depth tracker.
84+
/// </summary>
85+
public void Reset()
86+
{
87+
_limits.AddRange(_coordinates.Values);
88+
_coordinates.Clear();
89+
}
90+
}

src/HotChocolate/Core/src/Validation/IDocumentValidatorContext.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ public interface IDocumentValidatorContext : ISyntaxVisitorContext
140140
/// </summary>
141141
bool UnexpectedErrorsDetected { get; set; }
142142

143+
/// <summary>
144+
/// Defines that a fatal error was detected and that the analyzer will be aborted.
145+
/// </summary>
146+
bool FatalErrorDetected { get; set; }
147+
143148
/// <summary>
144149
/// A map to store arbitrary visitor data.
145150
/// </summary>
@@ -160,6 +165,11 @@ public interface IDocumentValidatorContext : ISyntaxVisitorContext
160165
/// </summary>
161166
HashSet<FieldInfoPair> ProcessedFieldPairs { get; }
162167

168+
/// <summary>
169+
/// Gets the field depth cycle tracker.
170+
/// </summary>
171+
FieldDepthCycleTracker FieldDepth { get; }
172+
163173
/// <summary>
164174
/// Rents a list of field infos.
165175
/// </summary>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using HotChocolate.Language;
3+
using HotChocolate.Language.Visitors;
4+
using HotChocolate.Types;
5+
using HotChocolate.Types.Introspection;
6+
using HotChocolate.Utilities;
7+
8+
namespace HotChocolate.Validation.Rules;
9+
10+
/// <summary>
11+
/// This rules ensures that recursive introspection fields cannot be used
12+
/// to create endless cycles.
13+
/// </summary>
14+
internal sealed class IntrospectionDepthVisitor : TypeDocumentValidatorVisitor
15+
{
16+
private readonly (FieldCoordinate Coordinate, ushort MaxAllowed)[] _limits =
17+
new (FieldCoordinate Coordinate, ushort MaxAllowed)[]
18+
{
19+
(new FieldCoordinate("__Type", "fields"), 1),
20+
(new FieldCoordinate("__Type", "inputFields"), 1),
21+
(new FieldCoordinate("__Type", "interfaces"), 1),
22+
(new FieldCoordinate("__Type", "possibleTypes"), 1),
23+
(new FieldCoordinate("__Type", "ofType"), 8),
24+
};
25+
26+
protected override ISyntaxVisitorAction Enter(
27+
DocumentNode node,
28+
IDocumentValidatorContext context)
29+
{
30+
context.FieldDepth.Initialize(_limits);
31+
return base.Enter(node, context);
32+
}
33+
34+
protected override ISyntaxVisitorAction Enter(
35+
FieldNode node,
36+
IDocumentValidatorContext context)
37+
{
38+
if (IntrospectionFields.TypeName.Equals(node.Name.Value, StringComparison.Ordinal))
39+
{
40+
return Skip;
41+
}
42+
43+
if (context.Types.TryPeek(out var type)
44+
&& type.NamedType() is IComplexOutputType ot
45+
&& ot.Fields.TryGetField(node.Name.Value, out var of))
46+
{
47+
// we are only interested in fields if the root field is either
48+
// __type or __schema.
49+
if (context.OutputFields.Count == 0
50+
&& !of.IsIntrospectionField)
51+
{
52+
return Skip;
53+
}
54+
55+
if (!context.FieldDepth.Add(of.Coordinate))
56+
{
57+
context.ReportMaxIntrospectionDepthOverflow(node);
58+
return Break;
59+
}
60+
61+
context.OutputFields.Push(of);
62+
context.Types.Push(of.Type);
63+
return Continue;
64+
}
65+
66+
context.UnexpectedErrorsDetected = true;
67+
return Skip;
68+
}
69+
70+
protected override ISyntaxVisitorAction Leave(
71+
FieldNode node,
72+
IDocumentValidatorContext context)
73+
{
74+
context.FieldDepth.Remove(context.OutputFields.Peek().Coordinate);
75+
context.Types.Pop();
76+
context.OutputFields.Pop();
77+
return Continue;
78+
}
79+
}

src/HotChocolate/Core/test/Validation.Tests/DocumentValidatorTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,12 @@ public void Produce_Many_Errors_50000_query()
813813
ExpectErrors(FileResource.Open("50000_query.graphql"));
814814
}
815815

816+
[Fact]
817+
public void Introspection_Cycle_Detected()
818+
{
819+
ExpectErrors(FileResource.Open("introspection_with_cycle.graphql"));
820+
}
821+
816822
private void ExpectValid(string sourceText) => ExpectValid(null, null, sourceText);
817823

818824
private void ExpectValid(ISchema schema, IDocumentValidator validator, string sourceText)

0 commit comments

Comments
 (0)