Skip to content

Commit 06cc025

Browse files
committed
fix: avoid stack overflow on cyclical references
Signed-off-by: Vincent Biret <[email protected]>
1 parent c515a76 commit 06cc025

File tree

5 files changed

+51
-17
lines changed

5 files changed

+51
-17
lines changed

src/Microsoft.OpenApi/Models/OpenApiDocument.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -498,10 +498,10 @@ public void SetReferenceHostDocument()
498498
/// <summary>
499499
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="BaseOpenApiReference"/> object
500500
/// </summary>
501-
internal T? ResolveReferenceTo<T>(BaseOpenApiReference reference) where T : IOpenApiReferenceable
501+
internal T? ResolveReferenceTo<T>(BaseOpenApiReference reference, IOpenApiSchema? parentSchema) where T : IOpenApiReferenceable
502502
{
503503

504-
if (ResolveReference(reference, reference.IsExternal) is T result)
504+
if (ResolveReference(reference, reference.IsExternal, parentSchema) is T result)
505505
{
506506
return result;
507507
}
@@ -566,7 +566,7 @@ private static string ConvertByteArrayToString(byte[] hash)
566566
/// <summary>
567567
/// Load the referenced <see cref="IOpenApiReferenceable"/> object from a <see cref="BaseOpenApiReference"/> object
568568
/// </summary>
569-
internal IOpenApiReferenceable? ResolveReference(BaseOpenApiReference? reference, bool useExternal)
569+
internal IOpenApiReferenceable? ResolveReference(BaseOpenApiReference? reference, bool useExternal, IOpenApiSchema? parentSchema)
570570
{
571571
if (reference == null)
572572
{
@@ -621,9 +621,9 @@ private static string ConvertByteArrayToString(byte[] hash)
621621
false => new Uri(uriLocation).AbsoluteUri
622622
};
623623

624-
if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#'))
624+
if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#') && parentSchema is not null)
625625
{
626-
return Workspace?.ResolveJsonSchemaReference(absoluteUri);
626+
return Workspace?.ResolveJsonSchemaReference(absoluteUri, parentSchema);
627627
}
628628

629629
return Workspace?.ResolveReference<IOpenApiReferenceable>(absoluteUri);

src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public virtual U? Target
1515
get
1616
{
1717
if (Reference.HostDocument is null) return default;
18-
return Reference.HostDocument.ResolveReferenceTo<U>(Reference);
18+
return Reference.HostDocument.ResolveReferenceTo<U>(Reference, this as IOpenApiSchema);
1919
}
2020
}
2121
/// <inheritdoc/>

src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -331,8 +331,9 @@ public bool Contains(string location)
331331
/// Recursively resolves a schema from a URI fragment.
332332
/// </summary>
333333
/// <param name="location"></param>
334+
/// <param name="parentSchema">The parent schema to resolve against.</param>
334335
/// <returns></returns>
335-
internal IOpenApiSchema? ResolveJsonSchemaReference(string location)
336+
internal IOpenApiSchema? ResolveJsonSchemaReference(string location, IOpenApiSchema parentSchema)
336337
{
337338
/* Enables resolving references for nested subschemas
338339
* Examples:
@@ -362,14 +363,22 @@ public bool Contains(string location)
362363
{
363364
// traverse remaining segments after fetching the base schema
364365
var remainingSegments = pathSegments.Skip(4).ToArray();
365-
return ResolveSubSchema(targetSchema, remainingSegments);
366+
var stack = new Stack<IOpenApiSchema>();
367+
stack.Push(parentSchema);
368+
return ResolveSubSchema(targetSchema, remainingSegments, stack);
366369
}
367370

368371
return default;
369372
}
370373

371-
internal static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments)
374+
internal static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments, Stack<IOpenApiSchema> visitedSchemas)
372375
{
376+
// Prevent infinite recursion in case of circular references
377+
if (visitedSchemas.Contains(schema))
378+
{
379+
return null;
380+
}
381+
visitedSchemas.Push(schema);
373382
// Traverse schema object to resolve subschemas
374383
if (pathSegments.Length == 0)
375384
{
@@ -383,13 +392,13 @@ public bool Contains(string location)
383392
case OpenApiConstants.Properties:
384393
var propName = pathSegments[0];
385394
if (schema.Properties != null && schema.Properties.TryGetValue(propName, out var propSchema))
386-
return ResolveSubSchema(propSchema, [.. pathSegments.Skip(1)]);
395+
return ResolveSubSchema(propSchema, [.. pathSegments.Skip(1)], visitedSchemas);
387396
break;
388397
case OpenApiConstants.Items:
389-
return schema.Items is OpenApiSchema itemsSchema ? ResolveSubSchema(itemsSchema, pathSegments) : null;
398+
return schema.Items is OpenApiSchema itemsSchema ? ResolveSubSchema(itemsSchema, pathSegments, visitedSchemas) : null;
390399

391400
case OpenApiConstants.AdditionalProperties:
392-
return schema.AdditionalProperties is OpenApiSchema additionalSchema ? ResolveSubSchema(additionalSchema, pathSegments) : null;
401+
return schema.AdditionalProperties is OpenApiSchema additionalSchema ? ResolveSubSchema(additionalSchema, pathSegments, visitedSchemas) : null;
393402
case OpenApiConstants.AllOf:
394403
case OpenApiConstants.AnyOf:
395404
case OpenApiConstants.OneOf:
@@ -405,7 +414,7 @@ public bool Contains(string location)
405414

406415
// recurse into the indexed subschema if valid
407416
if (list != null && index < list.Count)
408-
return ResolveSubSchema(list[index], [.. pathSegments.Skip(1)]);
417+
return ResolveSubSchema(list[index], [.. pathSegments.Skip(1)], visitedSchemas);
409418
break;
410419
}
411420

test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ public async Task ShouldAllowComponentsThatJustContainAReference()
261261
var schema1 = actual.Components.Schemas["AllPets"];
262262
var schema1Reference = Assert.IsType<OpenApiSchemaReference>(schema1);
263263
Assert.False(schema1Reference.UnresolvedReference);
264-
var schema2 = actual.ResolveReferenceTo<OpenApiSchema>(schema1Reference.Reference);
264+
var schema2 = actual.ResolveReferenceTo<OpenApiSchema>(schema1Reference.Reference, schema1Reference);
265265
Assert.IsType<OpenApiSchema>(schema2);
266266
if (string.IsNullOrEmpty(schema1Reference.Reference.Id) || schema1Reference.UnresolvedReference)
267267
{

test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ public void ResolveSubSchema_ShouldTraverseKnownKeywords()
223223

224224
var path = new[] { "properties", "a", "properties", "b" };
225225

226-
var result = OpenApiWorkspace.ResolveSubSchema(schema, path);
226+
var result = OpenApiWorkspace.ResolveSubSchema(schema, path, []);
227227

228228
Assert.NotNull(result);
229229
Assert.Equal(JsonSchemaType.String, result!.Type);
@@ -250,7 +250,7 @@ public void ResolveSubSchema_ShouldHandleUserDefinedKeywordNamedProperty(string[
250250
}
251251
};
252252

253-
var result = OpenApiWorkspace.ResolveSubSchema(schema, pathSegments);
253+
var result = OpenApiWorkspace.ResolveSubSchema(schema, pathSegments, []);
254254

255255
Assert.NotNull(result);
256256
Assert.Equal(JsonSchemaType.String, result!.Type);
@@ -275,7 +275,7 @@ public void ResolveSubSchema_ShouldRecurseIntoAllOfComposition()
275275

276276
var path = new[] { "allOf", "0", "properties", "x" };
277277

278-
var result = OpenApiWorkspace.ResolveSubSchema(schema, path);
278+
var result = OpenApiWorkspace.ResolveSubSchema(schema, path, []);
279279

280280
Assert.NotNull(result);
281281
Assert.Equal(JsonSchemaType.Integer, result!.Type);
@@ -478,5 +478,30 @@ public async Task ShouldResolveReferencesInSchemasFromSystemTextJson()
478478
var requestBodyTagsProperty = Assert.IsType<OpenApiSchemaReference>(requestBodySchema.Properties["tags"]);
479479
Assert.Equal(JsonSchemaType.Object, requestBodyTagsProperty.Items.Type);
480480
}
481+
482+
[Fact]
483+
public void ExitsEarlyOnCyclicalReferences()
484+
{
485+
var document = new OpenApiDocument
486+
{
487+
Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" },
488+
};
489+
var categorySchema = new OpenApiSchema
490+
{
491+
Type = JsonSchemaType.Object,
492+
Properties = new Dictionary<string, IOpenApiSchema>
493+
{
494+
["name"] = new OpenApiSchema { Type = JsonSchemaType.String },
495+
["parent"] = new OpenApiSchemaReference("#/components/schemas/Category", document),
496+
// this is intentionally wrong and cyclical reference
497+
// it tests whether we're going in an infinite resolution loop
498+
["tags"] = new OpenApiSchemaReference("#/components/schemas/Category/properties/parent/properties/tags", document)
499+
}
500+
};
501+
document.AddComponent("Category", categorySchema);
502+
document.RegisterComponents();
503+
504+
Assert.Null(categorySchema.Properties["tags"].Items);
505+
}
481506
}
482507
}

0 commit comments

Comments
 (0)