Skip to content

Commit 77586ac

Browse files
committed
Prevent overflow for self-referencing schemas
1 parent f437237 commit 77586ac

File tree

5 files changed

+117
-4
lines changed

5 files changed

+117
-4
lines changed

src/Microsoft.OpenApi.Readers/ParsingContext.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ public class ParsingContext
2323
private readonly Dictionary<string, IOpenApiReferenceable> _referenceStore = new Dictionary<string, IOpenApiReferenceable>();
2424
private readonly Dictionary<string, object> _tempStorage = new Dictionary<string, object>();
2525
private IOpenApiVersionService _versionService;
26+
private readonly Dictionary<string, Stack<string>> _loopStacks = new Dictionary<string, Stack<string>>();
27+
2628
internal RootNode RootNode { get; set; }
2729
internal List<OpenApiTag> Tags { get; private set; } = new List<OpenApiTag>();
28-
30+
2931

3032
/// <summary>
3133
/// Initiates the parsing process. Not thread safe and should only be called once on a parsing context
@@ -197,5 +199,52 @@ public void StartObject(string objectName)
197199
{
198200
_currentLocation.Push(objectName);
199201
}
202+
203+
/// <summary>
204+
/// Maintain history of traversals to avoid stack overflows from cycles
205+
/// </summary>
206+
/// <param name="loopId">Any unique identifier for a stack</param>
207+
/// <param name="key">Identifier used to </param>
208+
/// <returns></returns>
209+
public bool PushLoop(string loopId, string key)
210+
{
211+
Stack<string> stack;
212+
if (!_loopStacks.TryGetValue(loopId, out stack))
213+
{
214+
stack = new Stack<string>();
215+
_loopStacks.Add(loopId, stack);
216+
}
217+
218+
if (!stack.Contains(key))
219+
{
220+
stack.Push(key);
221+
return true;
222+
} else
223+
{
224+
return false; // Loop detected
225+
}
226+
}
227+
228+
/// <summary>
229+
/// Reset loop tracking stack
230+
/// </summary>
231+
/// <param name="loopid"></param>
232+
internal void ClearLoop(string loopid)
233+
{
234+
_loopStacks[loopid].Clear();
235+
}
236+
237+
/// <summary>
238+
/// Exit from the context in cycle detection
239+
/// </summary>
240+
/// <param name="loopid"></param>
241+
public void PopLoop(string loopid)
242+
{
243+
if (_loopStacks[loopid].Count > 0)
244+
{
245+
_loopStacks[loopid].Pop();
246+
}
247+
}
248+
200249
}
201250
}

src/Microsoft.OpenApi.Readers/V3/OpenApiSchemaDeserializer.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,15 +234,30 @@ internal static partial class OpenApiV3Deserializer
234234
{s => s.StartsWith("x-"), (o, p, n) => o.AddExtension(p, n.CreateAny())}
235235
};
236236

237+
private const string schemaLoopId = "schema";
238+
237239
public static OpenApiSchema LoadSchema(ParseNode node)
238240
{
239-
var mapNode = node.CheckMapNode("schema");
241+
var mapNode = node.CheckMapNode(OpenApiConstants.Schema);
240242

241243
var pointer = mapNode.GetReferencePointer();
242244

243245
if (pointer != null)
244246
{
245-
return mapNode.GetReferencedObject<OpenApiSchema>(ReferenceType.Schema, pointer);
247+
if (node.Context.PushLoop(schemaLoopId, pointer))
248+
{
249+
var schema = mapNode.GetReferencedObject<OpenApiSchema>(ReferenceType.Schema, pointer);
250+
node.Context.PopLoop(schemaLoopId);
251+
return schema;
252+
} else
253+
{
254+
node.Context.ClearLoop(schemaLoopId);
255+
//TODO. How do we make the object graph have a cycle. Or should we break the cycle in the graph?
256+
return new OpenApiSchema()
257+
{
258+
Reference = node.Context.VersionService.ConvertToOpenApiReference(pointer,ReferenceType.Schema)
259+
};
260+
}
246261
}
247262

248263
var domainObject = new OpenApiSchema();

test/Microsoft.OpenApi.Readers.Tests/Microsoft.OpenApi.Readers.Tests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
<SignAssembly>true</SignAssembly>
1313
<AssemblyOriginatorKeyFile>..\..\src\Microsoft.OpenApi.snk</AssemblyOriginatorKeyFile>
1414
</PropertyGroup>
15+
<ItemGroup>
16+
<None Remove="V3Tests\Samples\OpenApiSchema\selfReferencingSchema.yaml" />
17+
</ItemGroup>
1518
<ItemGroup>
1619
<EmbeddedResource Include="ReferenceService\Samples\multipleReferences.v2.yaml">
1720
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
@@ -106,6 +109,7 @@
106109
<EmbeddedResource Include="V3Tests\Samples\OpenApiSchema\primitiveSchema.yaml">
107110
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
108111
</EmbeddedResource>
112+
<EmbeddedResource Include="V3Tests\Samples\OpenApiSchema\selfReferencingSchema.yaml" />
109113
<EmbeddedResource Include="V3Tests\Samples\OpenApiSchema\simpleSchema.yaml">
110114
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
111115
</EmbeddedResource>

test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiSchemaTests.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ public void ParseAdvancedSchemaWithReferenceShouldSucceed()
280280

281281
// Assert
282282
var components = openApiDoc.Components;
283-
283+
284284
diagnostic.ShouldBeEquivalentTo(
285285
new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 });
286286

@@ -430,5 +430,24 @@ public void ParseAdvancedSchemaWithReferenceShouldSucceed()
430430
});
431431
}
432432
}
433+
434+
435+
[Fact]
436+
public void ParseSelfReferencingSchemaShouldNotStackOverflow()
437+
{
438+
using (var stream = Resources.GetStream(Path.Combine(SampleFolderPath, "selfReferencingSchema.yaml")))
439+
{
440+
// Act
441+
var openApiDoc = new OpenApiStreamReader().Read(stream, out var diagnostic);
442+
443+
// Assert
444+
var components = openApiDoc.Components;
445+
446+
diagnostic.ShouldBeEquivalentTo(
447+
new OpenApiDiagnostic() { SpecificationVersion = OpenApiSpecVersion.OpenApi3_0 });
448+
449+
450+
}
451+
}
433452
}
434453
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
openapi: 3.0.0
2+
info:
3+
title: Self Referencing schema
4+
version: 1.0.0
5+
paths: {}
6+
components:
7+
schemas:
8+
microsoft.graph.schemaExtension:
9+
allOf:
10+
-
11+
title: "schemaExtension"
12+
type: "object"
13+
properties:
14+
description:
15+
type: "string"
16+
nullable: true
17+
targetTypes:
18+
type: "array"
19+
items:
20+
type: "string"
21+
status:
22+
type: "string"
23+
owner:
24+
type: "string"
25+
child:
26+
$ref: "#/components/schemas/microsoft.graph.schemaExtension"

0 commit comments

Comments
 (0)