Skip to content

Commit a1869ef

Browse files
Treicy Sanchez Gutierrez (from Dev Box)Copilot
andcommitted
feat: make YAML conversion limits configurable
Expose YamlConverter.MaxDepth and MaxNodeCount as public static properties (defaulting to DefaultMaxDepth=64 and DefaultMaxNodeCount=5,000,000) so consumers can raise the limits for legitimately large/deep documents or lower them to fail faster on known-small inputs, without needing a library change. Setters validate that the value is greater than zero. Public API entries added. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 022bbd4f-e5e7-447a-bcdf-b2a4efaf75c3
1 parent 00c52aa commit a1869ef

3 files changed

Lines changed: 105 additions & 7 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
#nullable enable
2+
const Microsoft.OpenApi.YamlReader.YamlConverter.DefaultMaxDepth = 64 -> int
3+
const Microsoft.OpenApi.YamlReader.YamlConverter.DefaultMaxNodeCount = 5000000 -> int
4+
static Microsoft.OpenApi.YamlReader.YamlConverter.MaxDepth.get -> int
5+
static Microsoft.OpenApi.YamlReader.YamlConverter.MaxDepth.set -> void
6+
static Microsoft.OpenApi.YamlReader.YamlConverter.MaxNodeCount.get -> int
7+
static Microsoft.OpenApi.YamlReader.YamlConverter.MaxNodeCount.set -> void

src/Microsoft.OpenApi.YamlReader/YamlConverter.cs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,58 @@ public static class YamlConverter
1919
/// Mirrors the default System.Text.Json depth limit (64) that already bounds the JSON reader path,
2020
/// protecting the recursive conversion from stack exhaustion on deeply nested documents.
2121
/// </summary>
22-
internal const int DefaultMaxDepth = 64;
22+
public const int DefaultMaxDepth = 64;
2323

2424
/// <summary>
2525
/// Default maximum number of JSON nodes that may be materialized from a single YAML document.
2626
/// Guards against YAML anchor/alias expansion ("billion laughs") attacks, where a tiny document
2727
/// expands exponentially when its shared node graph is materialized into an independent JSON tree.
28-
/// Increase this only if legitimate large documents are being rejected.
2928
/// </summary>
30-
internal const int DefaultMaxNodeCount = 5_000_000;
29+
public const int DefaultMaxNodeCount = 5_000_000;
30+
31+
private static int _maxDepth = DefaultMaxDepth;
32+
private static int _maxNodeCount = DefaultMaxNodeCount;
33+
34+
/// <summary>
35+
/// Gets or sets the maximum nesting depth allowed when converting a YAML node graph into JSON nodes.
36+
/// Defaults to <see cref="DefaultMaxDepth"/>. Raise this if legitimate deeply nested documents are
37+
/// being rejected, or lower it to fail faster when only shallow documents are expected.
38+
/// </summary>
39+
/// <exception cref="ArgumentOutOfRangeException">Thrown when set to a value less than 1.</exception>
40+
public static int MaxDepth
41+
{
42+
get => _maxDepth;
43+
set
44+
{
45+
if (value < 1)
46+
{
47+
throw new ArgumentOutOfRangeException(nameof(value), "MaxDepth must be greater than zero.");
48+
}
49+
50+
_maxDepth = value;
51+
}
52+
}
53+
54+
/// <summary>
55+
/// Gets or sets the maximum number of JSON nodes that may be materialized from a single YAML document.
56+
/// Defaults to <see cref="DefaultMaxNodeCount"/>, guarding against YAML anchor/alias expansion
57+
/// ("billion laughs") attacks. Raise this if legitimate large documents are being rejected, or lower
58+
/// it to fail faster when only small documents are expected.
59+
/// </summary>
60+
/// <exception cref="ArgumentOutOfRangeException">Thrown when set to a value less than 1.</exception>
61+
public static int MaxNodeCount
62+
{
63+
get => _maxNodeCount;
64+
set
65+
{
66+
if (value < 1)
67+
{
68+
throw new ArgumentOutOfRangeException(nameof(value), "MaxNodeCount must be greater than zero.");
69+
}
70+
71+
_maxNodeCount = value;
72+
}
73+
}
3174

3275
/// <summary>
3376
/// Tracks and enforces resource limits while converting a YAML node graph into JSON nodes,
@@ -39,7 +82,7 @@ private sealed class YamlConversionBudget
3982
private readonly int _maxNodeCount;
4083
private int _nodeCount;
4184

42-
public YamlConversionBudget(int maxDepth = DefaultMaxDepth, int maxNodeCount = DefaultMaxNodeCount)
85+
public YamlConversionBudget(int maxDepth, int maxNodeCount)
4386
{
4487
_maxDepth = maxDepth;
4588
_maxNodeCount = maxNodeCount;
@@ -87,7 +130,7 @@ public static JsonNode ToJsonNode(this YamlDocument yaml)
87130
/// <exception cref="NotSupportedException">Thrown for YAML that is not compatible with JSON.</exception>
88131
public static JsonNode ToJsonNode(this YamlNode yaml)
89132
{
90-
return yaml.ToJsonNode(new YamlConversionBudget(), 0);
133+
return yaml.ToJsonNode(new YamlConversionBudget(MaxDepth, MaxNodeCount), 0);
91134
}
92135

93136
private static JsonNode ToJsonNode(this YamlNode yaml, YamlConversionBudget budget, int depth)
@@ -130,7 +173,7 @@ public static YamlNode ToYamlNode(this JsonNode json)
130173
/// <returns></returns>
131174
public static JsonObject ToJsonObject(this YamlMappingNode yaml)
132175
{
133-
return yaml.ToJsonObject(new YamlConversionBudget(), 0);
176+
return yaml.ToJsonObject(new YamlConversionBudget(MaxDepth, MaxNodeCount), 0);
134177
}
135178

136179
private static JsonObject ToJsonObject(this YamlMappingNode yaml, YamlConversionBudget budget, int depth)
@@ -160,7 +203,7 @@ private static YamlMappingNode ToYamlMapping(this JsonObject obj)
160203
/// <returns></returns>
161204
public static JsonArray ToJsonArray(this YamlSequenceNode yaml)
162205
{
163-
return yaml.ToJsonArray(new YamlConversionBudget(), 0);
206+
return yaml.ToJsonArray(new YamlConversionBudget(MaxDepth, MaxNodeCount), 0);
164207
}
165208

166209
private static JsonArray ToJsonArray(this YamlSequenceNode yaml, YamlConversionBudget budget, int depth)

test/Microsoft.OpenApi.Readers.Tests/YamlConverterTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,55 @@ public void LegitimateAliasesStillConvert()
381381
Assert.Equal("hello", jsonNode["b"]?.GetValue<string>());
382382
}
383383

384+
[Fact]
385+
public void ConversionLimitsDefaultToDocumentedValues()
386+
{
387+
Assert.Equal(64, YamlConverter.DefaultMaxDepth);
388+
Assert.Equal(5_000_000, YamlConverter.DefaultMaxNodeCount);
389+
Assert.Equal(YamlConverter.DefaultMaxDepth, YamlConverter.MaxDepth);
390+
Assert.Equal(YamlConverter.DefaultMaxNodeCount, YamlConverter.MaxNodeCount);
391+
}
392+
393+
[Theory]
394+
[InlineData(0)]
395+
[InlineData(-1)]
396+
public void SettingMaxDepthBelowOneThrows(int value)
397+
{
398+
Assert.Throws<ArgumentOutOfRangeException>(() => YamlConverter.MaxDepth = value);
399+
// The invalid assignment must not have changed the effective limit.
400+
Assert.Equal(YamlConverter.DefaultMaxDepth, YamlConverter.MaxDepth);
401+
}
402+
403+
[Theory]
404+
[InlineData(0)]
405+
[InlineData(-1)]
406+
public void SettingMaxNodeCountBelowOneThrows(int value)
407+
{
408+
Assert.Throws<ArgumentOutOfRangeException>(() => YamlConverter.MaxNodeCount = value);
409+
// The invalid assignment must not have changed the effective limit.
410+
Assert.Equal(YamlConverter.DefaultMaxNodeCount, YamlConverter.MaxNodeCount);
411+
}
412+
413+
[Fact]
414+
public void RaisingMaxDepthAllowsDocumentsDeeperThanTheDefault()
415+
{
416+
// A document nested deeper than the default depth limit (64) is rejected by default
417+
// but can be permitted by a consumer that opts into a higher limit.
418+
const int depth = 70;
419+
var deeplyNested = new string('[', depth) + new string(']', depth);
420+
421+
try
422+
{
423+
YamlConverter.MaxDepth = depth + 10;
424+
var jsonNode = ConvertYamlStringToJsonNode(deeplyNested);
425+
Assert.IsType<JsonArray>(jsonNode);
426+
}
427+
finally
428+
{
429+
YamlConverter.MaxDepth = YamlConverter.DefaultMaxDepth;
430+
}
431+
}
432+
384433
private static JsonNode ConvertYamlStringToJsonNode(string yamlInput)
385434
{
386435
var yamlDocument = new YamlStream();

0 commit comments

Comments
 (0)