Skip to content

Commit d4c48d9

Browse files
committed
Added Introspection Cycle Detection Rule
1 parent a190f03 commit d4c48d9

27 files changed

+473
-38
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,11 @@ public static class Validation
321321
/// The introspection is not allowed for the current request
322322
/// </summary>
323323
public const string IntrospectionNotAllowed = "HC0046";
324+
325+
/// <summary>
326+
/// The maximum allowed introspection depth was exceeded.
327+
/// </summary>
328+
public const string MaxIntrospectionDepthOverflow = "HC0086";
324329
}
325330

326331
/// <summary>

src/HotChocolate/Core/src/Abstractions/packages.lock.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,9 @@
284284
"net8.0": {
285285
"Microsoft.NET.ILLink.Tasks": {
286286
"type": "Direct",
287-
"requested": "[8.0.7, )",
288-
"resolved": "8.0.7",
289-
"contentHash": "iI52ptEKby2ymQ6B7h4TWbFmm85T4VvLgc/HvS45Yr3lgi4IIFbQtjON3bQbX/Vc94jXNSLvrDOp5Kh7SJyFYQ=="
287+
"requested": "[8.0.8, )",
288+
"resolved": "8.0.8",
289+
"contentHash": "P8wR6MUWwYXIjPJuBaZgo5zlI/GWI6QEAo6NyVIbPefa9CCkohYu7dP2rD/mrqnjEqfRHyl+h9VZrDoGpELqYg=="
290290
},
291291
"Microsoft.SourceLink.GitHub": {
292292
"type": "Direct",

src/HotChocolate/Core/src/Types.Shared/packages.lock.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,9 @@
158158
"net8.0": {
159159
"Microsoft.NET.ILLink.Tasks": {
160160
"type": "Direct",
161-
"requested": "[8.0.7, )",
162-
"resolved": "8.0.7",
163-
"contentHash": "iI52ptEKby2ymQ6B7h4TWbFmm85T4VvLgc/HvS45Yr3lgi4IIFbQtjON3bQbX/Vc94jXNSLvrDOp5Kh7SJyFYQ=="
161+
"requested": "[8.0.8, )",
162+
"resolved": "8.0.8",
163+
"contentHash": "P8wR6MUWwYXIjPJuBaZgo5zlI/GWI6QEAo6NyVIbPefa9CCkohYu7dP2rD/mrqnjEqfRHyl+h9VZrDoGpELqYg=="
164164
},
165165
"Microsoft.SourceLink.GitHub": {
166166
"type": "Direct",
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
@@ -110,6 +110,11 @@ public ValueTask<DocumentValidatorResult> ValidateAsync(
110110
for (var i = 0; i < length; i++)
111111
{
112112
Unsafe.Add(ref start, i).Validate(context, document);
113+
114+
if (context.FatalErrorDetected)
115+
{
116+
break;
117+
}
113118
}
114119

115120
if (_aggregators.Length == 0)

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

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

9999
public bool UnexpectedErrorsDetected { get; set; }
100100

101+
public bool FatalErrorDetected { get; set; }
102+
101103
public int Count { get; set; }
102104

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

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

117+
public FieldDepthCycleTracker FieldDepth { get; } = new();
118+
115119
public IList<FieldInfo> RentFieldInfoList()
116120
{
117121
var buffer = _buffers.Peek();
@@ -168,7 +172,9 @@ public void Clear()
168172
CurrentFieldPairs.Clear();
169173
NextFieldPairs.Clear();
170174
ProcessedFieldPairs.Clear();
175+
FieldDepth.Reset();
171176
UnexpectedErrorsDetected = false;
177+
FatalErrorDetected = false;
172178
Count = 0;
173179
Max = 0;
174180
Allowed = 0;

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,4 +725,18 @@ public static IError StreamOnNonListField(
725725
.SpecifiedBy("sec-Stream-Directives-Are-Used-On-List-Fields")
726726
.SetPath(context.CreateErrorPath())
727727
.Build();
728+
729+
public static void ReportMaxIntrospectionDepthOverflow(
730+
this IDocumentValidatorContext context,
731+
ISyntaxNode selection)
732+
{
733+
context.FatalErrorDetected = true;
734+
context.ReportError(
735+
ErrorBuilder.New()
736+
.SetMessage("Maximum allowed introspection depth exceeded.")
737+
.SetCode(ErrorCodes.Validation.MaxIntrospectionDepthOverflow)
738+
.SetSyntaxNode(selection)
739+
.SetPath(context.CreateErrorPath())
740+
.Build());
741+
}
728742
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,4 +342,11 @@ public static IValidationBuilder AddMaxExecutionDepthRule(
342342
public static IValidationBuilder AddIntrospectionAllowedRule(
343343
this IValidationBuilder builder) =>
344344
builder.TryAddValidationVisitor((_, _) => new IntrospectionVisitor(), false);
345+
346+
/// <summary>
347+
/// Adds a validation rule that restricts the depth of a GraphQL introspection request.
348+
/// </summary>
349+
public static IValidationBuilder AddIntrospectionDepthRule(
350+
this IValidationBuilder builder)
351+
=> builder.TryAddValidationVisitor<IntrospectionDepthVisitor>();
345352
}

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
@@ -155,6 +155,11 @@ public interface IDocumentValidatorContext : ISyntaxVisitorContext
155155
/// </summary>
156156
bool UnexpectedErrorsDetected { get; set; }
157157

158+
/// <summary>
159+
/// Defines that a fatal error was detected and that the analyzer will be aborted.
160+
/// </summary>
161+
bool FatalErrorDetected { get; set; }
162+
158163
/// <summary>
159164
/// A map to store arbitrary visitor data.
160165
/// </summary>
@@ -175,6 +180,11 @@ public interface IDocumentValidatorContext : ISyntaxVisitorContext
175180
/// </summary>
176181
HashSet<FieldInfoPair> ProcessedFieldPairs { get; }
177182

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

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,12 @@ public async Task Produce_Many_Errors_50000_query()
865865
await ExpectErrors(FileResource.Open("50000_query.graphql"));
866866
}
867867

868+
[Fact]
869+
public async Task Introspection_Cycle_Detected()
870+
{
871+
await ExpectErrors(FileResource.Open("introspection_with_cycle.graphql"));
872+
}
873+
868874
private Task ExpectValid(string sourceText) => ExpectValid(null, null, sourceText);
869875

870876
private async Task ExpectValid(ISchema schema, IDocumentValidator validator, string sourceText)

0 commit comments

Comments
 (0)