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> _scopedTempStorage = new(); private readonly Dictionary> _loopStacks = new(); + private uint _nodeCount; internal Dictionary> ExtensionParsers { get; set; } = new(); internal RootNode RootNode { get; set; } internal List Tags { get; private set; } = new(); @@ -198,6 +199,20 @@ public void StartObject(string objectName) _currentLocation.Push(objectName); } + /// + /// Counts a node materialized while parsing the current document and fails fast when the + /// document expands beyond . YAML anchors and + /// aliases share a single node in the source graph, so a tiny document can expand + /// exponentially ("billion laughs") when it is materialized into an independent tree. + /// + internal void CountNode() + { + if (++_nodeCount > OpenApiReaderLimits.MaxNodeCount) + { + throw new OpenApiReaderException($"The document expands to more than the maximum supported number of nodes ({OpenApiReaderLimits.MaxNodeCount}). This may indicate a YAML anchor/alias expansion (billion laughs) attack."); + } + } + /// /// Maintain history of traversals to avoid stack overflows from cycles /// diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlAliasExpansionTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlAliasExpansionTests.cs new file mode 100644 index 000000000..99e10c83b --- /dev/null +++ b/test/Microsoft.OpenApi.Readers.Tests/YamlAliasExpansionTests.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +using System; +using FluentAssertions; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Readers; +using Microsoft.OpenApi.Readers.Exceptions; +using Microsoft.OpenApi.Readers.ParseNodes; +using Xunit; + +namespace Microsoft.OpenApi.Tests +{ + [Collection("DefaultSettings")] + public class YamlAliasExpansionTests + { + // A "billion laughs" YAML bomb: each level references the previous one multiple times, + // so materializing the shared node graph into an independent object tree expands + // exponentially. The conversion must fail fast instead of exhausting memory. + private const string YamlBomb = + """ + a: &a ["x","x","x","x","x","x","x","x","x"] + b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a] + c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b] + d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c] + e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d] + f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e] + g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f] + h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g] + i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h] + """; + + [Fact] + public void ExponentialAliasExpansionIsRejected() + { + var node = ParseNode.Create(new(new()), YamlHelper.ParseYamlString(YamlBomb)); + + Assert.Throws(() => node.CreateAny()); + } + + [Fact] + public void ExcessiveNestingDepthIsRejected() + { + // Deeper than the conversion depth limit, which protects the recursive + // converter from stack exhaustion. + const int depth = 70; + var deeplyNested = new string('[', depth) + new string(']', depth); + + var node = ParseNode.Create(new(new()), YamlHelper.ParseYamlString(deeplyNested)); + + Assert.Throws(() => node.CreateAny()); + } + + [Fact] + public void ReadReturnsDiagnosticErrorForExponentialAliasExpansion() + { + // A "billion laughs" YAML bomb must surface as a diagnostic error + // rather than throwing or exhausting memory. + var input = + $$""" + openapi: 3.0.0 + info: + title: bomb + version: 1.0.0 + paths: {} + x-bomb: + {{YamlBombIndented()}} + """; + + var reader = new OpenApiStringReader(); + reader.Read(input, out var diagnostic); + + diagnostic.Errors.Should().NotBeEmpty(); + } + + [Fact] + public void LegitimateAliasesStillConvert() + { + var input = + """ + a: &val hello + b: *val + """; + + var node = ParseNode.Create(new(new()), YamlHelper.ParseYamlString(input)); + + var anyObject = Assert.IsType(node.CreateAny()); + Assert.Equal("hello", ((OpenApiString)anyObject["a"]).Value); + Assert.Equal("hello", ((OpenApiString)anyObject["b"]).Value); + } + + [Fact] + public void ConversionLimitsDefaultToDocumentedValues() + { + Assert.Equal(64u, OpenApiReaderLimits.DefaultMaxDepth); + Assert.Equal(5_000_000u, OpenApiReaderLimits.DefaultMaxNodeCount); + Assert.Equal(OpenApiReaderLimits.DefaultMaxDepth, OpenApiReaderLimits.MaxDepth); + Assert.Equal(OpenApiReaderLimits.DefaultMaxNodeCount, OpenApiReaderLimits.MaxNodeCount); + } + + [Fact] + public void SettingMaxDepthToZeroThrows() + { + Assert.Throws(() => OpenApiReaderLimits.MaxDepth = 0); + // The invalid assignment must not have changed the effective limit. + Assert.Equal(OpenApiReaderLimits.DefaultMaxDepth, OpenApiReaderLimits.MaxDepth); + } + + [Fact] + public void SettingMaxNodeCountToZeroThrows() + { + Assert.Throws(() => OpenApiReaderLimits.MaxNodeCount = 0); + // The invalid assignment must not have changed the effective limit. + Assert.Equal(OpenApiReaderLimits.DefaultMaxNodeCount, OpenApiReaderLimits.MaxNodeCount); + } + + [Fact] + public void RaisingMaxDepthAllowsDocumentsDeeperThanTheDefault() + { + // A document nested deeper than the default depth limit (64) is rejected by default + // but can be permitted by a consumer that opts into a higher limit. + const int depth = 70; + var deeplyNested = new string('[', depth) + new string(']', depth); + + try + { + OpenApiReaderLimits.MaxDepth = (uint)(depth + 10); + var node = ParseNode.Create(new(new()), YamlHelper.ParseYamlString(deeplyNested)); + Assert.IsType(node.CreateAny()); + } + finally + { + OpenApiReaderLimits.MaxDepth = OpenApiReaderLimits.DefaultMaxDepth; + } + } + + private static string YamlBombIndented() + { + return " " + YamlBomb.Replace("\r\n", "\n").Replace("\n", "\n "); + } + } +} From f814c72f17819d85ac81821e3062ea85816af65b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:40:16 +0000 Subject: [PATCH 2/2] refactor(readers): move YAML expansion limits into OpenApiReaderSettings Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../OpenApiReaderLimits.cs | 72 ------------------- .../OpenApiReaderSettings.cs | 58 +++++++++++++++ .../OpenApiYamlDocumentReader.cs | 12 +++- .../ParseNodes/ParseNode.cs | 7 +- .../ParsingContext.cs | 8 ++- .../YamlAliasExpansionTests.cs | 38 +++++----- 6 files changed, 95 insertions(+), 100 deletions(-) delete mode 100644 src/Microsoft.OpenApi.Readers/OpenApiReaderLimits.cs diff --git a/src/Microsoft.OpenApi.Readers/OpenApiReaderLimits.cs b/src/Microsoft.OpenApi.Readers/OpenApiReaderLimits.cs deleted file mode 100644 index df32f27d8..000000000 --- a/src/Microsoft.OpenApi.Readers/OpenApiReaderLimits.cs +++ /dev/null @@ -1,72 +0,0 @@ -// 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/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/ParseNode.cs b/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs index a559f978d..ba870f192 100644 --- a/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs +++ b/src/Microsoft.OpenApi.Readers/ParseNodes/ParseNode.cs @@ -82,7 +82,7 @@ public IOpenApiAny CreateAny() /// /// Materializes the node, and everything below it, into an . /// - /// Nesting depth of the current node, bounded by . + /// 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); @@ -94,9 +94,10 @@ internal virtual IOpenApiAny CreateAny(uint depth) /// protected void EnsureDepthWithinLimit(uint depth) { - if (depth > OpenApiReaderLimits.MaxDepth) + var maxDepth = Context?.MaxDepth ?? OpenApiReaderSettings.DefaultMaxDepth; + if (depth > maxDepth) { - throw new OpenApiReaderException($"The document exceeds the maximum supported nesting depth of {OpenApiReaderLimits.MaxDepth}.", Context); + throw new OpenApiReaderException($"The document exceeds the maximum supported nesting depth of {maxDepth}.", Context); } } diff --git a/src/Microsoft.OpenApi.Readers/ParsingContext.cs b/src/Microsoft.OpenApi.Readers/ParsingContext.cs index b52713bda..228151be1 100644 --- a/src/Microsoft.OpenApi.Readers/ParsingContext.cs +++ b/src/Microsoft.OpenApi.Readers/ParsingContext.cs @@ -26,6 +26,8 @@ public class ParsingContext private readonly Dictionary> _scopedTempStorage = new(); private readonly Dictionary> _loopStacks = new(); private uint _nodeCount; + internal uint MaxDepth { get; set; } = OpenApiReaderSettings.DefaultMaxDepth; + internal uint MaxNodeCount { get; set; } = OpenApiReaderSettings.DefaultMaxNodeCount; internal Dictionary> ExtensionParsers { get; set; } = new(); internal RootNode RootNode { get; set; } internal List Tags { get; private set; } = new(); @@ -201,15 +203,15 @@ public void StartObject(string objectName) /// /// Counts a node materialized while parsing the current document and fails fast when the - /// document expands beyond . YAML anchors and + /// document expands beyond . YAML anchors and /// aliases share a single node in the source graph, so a tiny document can expand /// exponentially ("billion laughs") when it is materialized into an independent tree. /// internal void CountNode() { - if (++_nodeCount > OpenApiReaderLimits.MaxNodeCount) + if (++_nodeCount > MaxNodeCount) { - throw new OpenApiReaderException($"The document expands to more than the maximum supported number of nodes ({OpenApiReaderLimits.MaxNodeCount}). This may indicate a YAML anchor/alias expansion (billion laughs) attack."); + throw new OpenApiReaderException($"The document expands to more than the maximum supported number of nodes ({MaxNodeCount}). This may indicate a YAML anchor/alias expansion (billion laughs) attack."); } } diff --git a/test/Microsoft.OpenApi.Readers.Tests/YamlAliasExpansionTests.cs b/test/Microsoft.OpenApi.Readers.Tests/YamlAliasExpansionTests.cs index 99e10c83b..53ba6852f 100644 --- a/test/Microsoft.OpenApi.Readers.Tests/YamlAliasExpansionTests.cs +++ b/test/Microsoft.OpenApi.Readers.Tests/YamlAliasExpansionTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; @@ -92,26 +92,32 @@ public void LegitimateAliasesStillConvert() [Fact] public void ConversionLimitsDefaultToDocumentedValues() { - Assert.Equal(64u, OpenApiReaderLimits.DefaultMaxDepth); - Assert.Equal(5_000_000u, OpenApiReaderLimits.DefaultMaxNodeCount); - Assert.Equal(OpenApiReaderLimits.DefaultMaxDepth, OpenApiReaderLimits.MaxDepth); - Assert.Equal(OpenApiReaderLimits.DefaultMaxNodeCount, OpenApiReaderLimits.MaxNodeCount); + var settings = new OpenApiReaderSettings(); + + Assert.Equal(64u, OpenApiReaderSettings.DefaultMaxDepth); + Assert.Equal(5_000_000u, OpenApiReaderSettings.DefaultMaxNodeCount); + Assert.Equal(OpenApiReaderSettings.DefaultMaxDepth, settings.MaxDepth); + Assert.Equal(OpenApiReaderSettings.DefaultMaxNodeCount, settings.MaxNodeCount); } [Fact] public void SettingMaxDepthToZeroThrows() { - Assert.Throws(() => OpenApiReaderLimits.MaxDepth = 0); + var settings = new OpenApiReaderSettings(); + + Assert.Throws(() => settings.MaxDepth = 0); // The invalid assignment must not have changed the effective limit. - Assert.Equal(OpenApiReaderLimits.DefaultMaxDepth, OpenApiReaderLimits.MaxDepth); + Assert.Equal(OpenApiReaderSettings.DefaultMaxDepth, settings.MaxDepth); } [Fact] public void SettingMaxNodeCountToZeroThrows() { - Assert.Throws(() => OpenApiReaderLimits.MaxNodeCount = 0); + var settings = new OpenApiReaderSettings(); + + Assert.Throws(() => settings.MaxNodeCount = 0); // The invalid assignment must not have changed the effective limit. - Assert.Equal(OpenApiReaderLimits.DefaultMaxNodeCount, OpenApiReaderLimits.MaxNodeCount); + Assert.Equal(OpenApiReaderSettings.DefaultMaxNodeCount, settings.MaxNodeCount); } [Fact] @@ -122,16 +128,10 @@ public void RaisingMaxDepthAllowsDocumentsDeeperThanTheDefault() const int depth = 70; var deeplyNested = new string('[', depth) + new string(']', depth); - try - { - OpenApiReaderLimits.MaxDepth = (uint)(depth + 10); - var node = ParseNode.Create(new(new()), YamlHelper.ParseYamlString(deeplyNested)); - Assert.IsType(node.CreateAny()); - } - finally - { - OpenApiReaderLimits.MaxDepth = OpenApiReaderLimits.DefaultMaxDepth; - } + var context = new ParsingContext(new()) { MaxDepth = depth + 10 }; + var node = ParseNode.Create(context, YamlHelper.ParseYamlString(deeplyNested)); + + Assert.IsType(node.CreateAny()); } private static string YamlBombIndented()