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>

0 commit comments

Comments
 (0)