diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs
index 5d251802f..8f5bd4fa7 100644
--- a/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs
+++ b/src/Microsoft.OpenApi.Readers/OpenApiReaderSettings.cs
@@ -36,6 +36,64 @@ public enum ReferenceResolutionSetting
///
public class OpenApiReaderSettings
{
+ ///
+ /// 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 uint _maxDepth = DefaultMaxDepth;
+ private 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 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 uint MaxNodeCount
+ {
+ get => _maxNodeCount;
+ set
+ {
+ if (value == 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value), "MaxNodeCount must be greater than zero.");
+ }
+
+ _maxNodeCount = value;
+ }
+ }
+
///
/// Indicates how references in the source document should be handled.
///
diff --git a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs
index af9ebcad1..73349e45d 100644
--- a/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs
+++ b/src/Microsoft.OpenApi.Readers/OpenApiYamlDocumentReader.cs
@@ -47,7 +47,9 @@ public OpenApiDocument Read(YamlDocument input, out OpenApiDiagnostic diagnostic
{
ExtensionParsers = _settings.ExtensionParsers,
BaseUrl = _settings.BaseUrl,
- DefaultContentType = _settings.DefaultContentType
+ DefaultContentType = _settings.DefaultContentType,
+ MaxDepth = _settings.MaxDepth,
+ MaxNodeCount = _settings.MaxNodeCount
};
OpenApiDocument document = null;
@@ -91,7 +93,9 @@ public async Task ReadAsync(YamlDocument input, CancellationToken ca
var context = new ParsingContext(diagnostic)
{
ExtensionParsers = _settings.ExtensionParsers,
- BaseUrl = _settings.BaseUrl
+ BaseUrl = _settings.BaseUrl,
+ MaxDepth = _settings.MaxDepth,
+ MaxNodeCount = _settings.MaxNodeCount
};
OpenApiDocument document = null;
@@ -184,7 +188,9 @@ public T ReadFragment(YamlDocument input, OpenApiSpecVersion version, out Ope
diagnostic = new();
var context = new ParsingContext(diagnostic)
{
- ExtensionParsers = _settings.ExtensionParsers
+ ExtensionParsers = _settings.ExtensionParsers,
+ MaxDepth = _settings.MaxDepth,
+ MaxNodeCount = _settings.MaxNodeCount
};
IOpenApiElement element = null;
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..ba870f192 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,33 @@ 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)
+ {
+ var maxDepth = Context?.MaxDepth ?? OpenApiReaderSettings.DefaultMaxDepth;
+ if (depth > maxDepth)
+ {
+ throw new OpenApiReaderException($"The document exceeds the maximum supported nesting depth of {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..228151be1 100644
--- a/src/Microsoft.OpenApi.Readers/ParsingContext.cs
+++ b/src/Microsoft.OpenApi.Readers/ParsingContext.cs
@@ -25,6 +25,9 @@ public class ParsingContext
private readonly Dictionary _tempStorage = new();
private readonly Dictionary