From 92725e6bc70f0f3bb0a8050e86d7c994457dc171 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 12 Aug 2026 21:10:38 +0000
Subject: [PATCH 1/2] fix(readers): bound YAML anchor/alias expansion to
prevent OOM (billion laughs)
Ports the fix merged on main (#3000) to the support/v1 reader, which walks the
SharpYaml node graph directly. Aliases share a single source node, so a tiny
document expands exponentially when materialized into independent OpenApi any
trees, exhausting process memory (CWE-400).
Adds a per-parse node budget enforced by ParsingContext and a nesting depth
limit enforced while materializing any values. Limits are configurable through
the new OpenApiReaderLimits type and default to 5,000,000 nodes and depth 64
(mirroring the System.Text.Json default) as on main.
Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
---
.../OpenApiReaderLimits.cs | 72 +++++++++
.../ParseNodes/ListNode.cs | 5 +-
.../ParseNodes/MapNode.cs | 5 +-
.../ParseNodes/ParseNode.cs | 24 ++-
.../ParseNodes/PropertyNode.cs | 2 +-
.../ParseNodes/ValueNode.cs | 3 +-
.../ParsingContext.cs | 15 ++
.../YamlAliasExpansionTests.cs | 142 ++++++++++++++++++
8 files changed, 261 insertions(+), 7 deletions(-)
create mode 100644 src/Microsoft.OpenApi.Readers/OpenApiReaderLimits.cs
create mode 100644 test/Microsoft.OpenApi.Readers.Tests/YamlAliasExpansionTests.cs
diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderLimits.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderLimits.cs
new file mode 100644
index 000000000..df32f27d8
--- /dev/null
+++ b/src/Microsoft.OpenApi.Readers/OpenApiReaderLimits.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+using System;
+
+namespace Microsoft.OpenApi.Readers
+{
+ ///
+ /// Resource limits applied while reading an OpenAPI description, protecting the reader from
+ /// hostile documents that would otherwise exhaust memory or the stack.
+ ///
+ public static class OpenApiReaderLimits
+ {
+ ///
+ /// Default maximum nesting depth allowed when materializing values from a YAML/JSON node graph.
+ /// Mirrors the default System.Text.Json depth limit (64), protecting the recursive readers
+ /// from stack exhaustion on deeply nested documents.
+ ///
+ public const uint DefaultMaxDepth = 64;
+
+ ///
+ /// Default maximum number of nodes that may be materialized from a single document.
+ /// Guards against YAML anchor/alias expansion ("billion laughs") attacks, where a tiny document
+ /// expands exponentially when its shared node graph is materialized into an independent tree.
+ ///
+ public const uint DefaultMaxNodeCount = 5_000_000;
+
+ private static uint _maxDepth = DefaultMaxDepth;
+ private static uint _maxNodeCount = DefaultMaxNodeCount;
+
+ ///
+ /// Gets or sets the maximum nesting depth allowed when materializing values from a node graph.
+ /// Defaults to . Raise this if legitimate deeply nested documents are
+ /// being rejected, or lower it to fail faster when only shallow documents are expected.
+ ///
+ /// Thrown when set to zero.
+ public static uint MaxDepth
+ {
+ get => _maxDepth;
+ set
+ {
+ if (value == 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "MaxDepth must be greater than zero.");
+ }
+
+ _maxDepth = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the maximum number of nodes that may be materialized from a single document.
+ /// Defaults to , guarding against YAML anchor/alias expansion
+ /// ("billion laughs") attacks. Raise this if legitimate large documents are being rejected, or lower
+ /// it to fail faster when only small documents are expected.
+ ///
+ /// Thrown when set to zero.
+ public static uint MaxNodeCount
+ {
+ get => _maxNodeCount;
+ set
+ {
+ if (value == 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "MaxNodeCount must be greater than zero.");
+ }
+
+ _maxNodeCount = value;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/ListNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/ListNode.cs
index dbeadb5d1..02fb80416 100644
--- a/src/Microsoft.OpenApi.Readers/ParseNodes/ListNode.cs
+++ b/src/Microsoft.OpenApi.Readers/ParseNodes/ListNode.cs
@@ -64,12 +64,13 @@ IEnumerator IEnumerable.GetEnumerator()
/// Create a
///
/// The created Any object.
- public override IOpenApiAny CreateAny()
+ internal override IOpenApiAny CreateAny(uint depth)
{
+ EnsureDepthWithinLimit(depth);
var array = new OpenApiArray();
foreach (var node in this)
{
- array.Add(node.CreateAny());
+ array.Add(node.CreateAny(depth + 1));
}
return array;
diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/MapNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/MapNode.cs
index 61f609817..e348f3258 100644
--- a/src/Microsoft.OpenApi.Readers/ParseNodes/MapNode.cs
+++ b/src/Microsoft.OpenApi.Readers/ParseNodes/MapNode.cs
@@ -213,12 +213,13 @@ public string GetScalarValue(ValueNode key)
/// Create a
///
/// The created Any object.
- public override IOpenApiAny CreateAny()
+ internal override IOpenApiAny CreateAny(uint depth)
{
+ EnsureDepthWithinLimit(depth);
var apiObject = new OpenApiObject();
foreach (var node in this)
{
- apiObject.Add(node.Name, node.Value.CreateAny());
+ apiObject.Add(node.Name, node.Value.CreateAny(depth + 1));
}
return apiObject;
diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs
index 028371c39..a559f978d 100644
--- a/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs
+++ b/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs
@@ -16,6 +16,7 @@ internal abstract class ParseNode
protected ParseNode(ParsingContext parsingContext)
{
Context = parsingContext;
+ Context?.CountNode();
}
public ParsingContext Context { get; }
@@ -73,11 +74,32 @@ public virtual Dictionary CreateSimpleMap(Func map)
throw new OpenApiReaderException("Cannot create simple map from this type of node.", Context);
}
- public virtual IOpenApiAny CreateAny()
+ public IOpenApiAny CreateAny()
+ {
+ return CreateAny(0);
+ }
+
+ ///
+ /// Materializes the node, and everything below it, into an .
+ ///
+ /// Nesting depth of the current node, bounded by .
+ internal virtual IOpenApiAny CreateAny(uint depth)
{
throw new OpenApiReaderException("Cannot create an Any object this type of node.", Context);
}
+ ///
+ /// Fails fast when the node graph is nested more deeply than the reader supports,
+ /// protecting the recursive readers from stack exhaustion.
+ ///
+ protected void EnsureDepthWithinLimit(uint depth)
+ {
+ if (depth > OpenApiReaderLimits.MaxDepth)
+ {
+ throw new OpenApiReaderException($"The document exceeds the maximum supported nesting depth of {OpenApiReaderLimits.MaxDepth}.", Context);
+ }
+ }
+
public virtual string GetRaw()
{
throw new OpenApiReaderException("Cannot get raw value from this type of node.", Context);
diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs
index 6a059c348..8646646e5 100644
--- a/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs
+++ b/src/Microsoft.OpenApi.Readers/ParseNodes/PropertyNode.cs
@@ -82,7 +82,7 @@ public void ParseField(
}
}
- public override IOpenApiAny CreateAny()
+ internal override IOpenApiAny CreateAny(uint depth)
{
throw new NotImplementedException();
}
diff --git a/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs
index 1aeccb8e7..d57bb69d1 100644
--- a/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs
+++ b/src/Microsoft.OpenApi.Readers/ParseNodes/ValueNode.cs
@@ -31,8 +31,9 @@ public override string GetScalarValue()
/// Create a
///
/// The created Any object.
- public override IOpenApiAny CreateAny()
+ internal override IOpenApiAny CreateAny(uint depth)
{
+ EnsureDepthWithinLimit(depth);
var value = GetScalarValue();
return new OpenApiString(value, this._node.Style is ScalarStyle.SingleQuoted or ScalarStyle.DoubleQuoted or ScalarStyle.Literal or ScalarStyle.Folded);
}
diff --git a/src/Microsoft.OpenApi.Readers/ParsingContext.cs b/src/Microsoft.OpenApi.Readers/ParsingContext.cs
index 6227514b8..b52713bda 100644
--- a/src/Microsoft.OpenApi.Readers/ParsingContext.cs
+++ b/src/Microsoft.OpenApi.Readers/ParsingContext.cs
@@ -25,6 +25,7 @@ public class ParsingContext
private readonly Dictionary _tempStorage = new();
private readonly Dictionary