Skip to content

Commit ba6dd7d

Browse files
authored
Merge pull request #1102 from EdwardCooke/ec-security
Security improvements +semver:feature
2 parents 941fe12 + 65a1367 commit ba6dd7d

12 files changed

Lines changed: 266 additions & 14 deletions

File tree

SECURITY.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Reporting
2+
3+
Please submit reports of security vulnerabilities and, if possible, code to reproduce the vulnerability.
4+
5+
That code will be the basis for the fix.
6+
7+
Please send reports to <edward@frakkingsweet.com> and <antoine@aaubry.net> instead of directly opening an issue detailing the finding.
8+
9+
We will reach out and let you know when it's appropriate to open that issue and post that information.
10+
11+
Though, likely the reproduction will be in the unit test and details included in the PR around the vulnerability.
12+
13+
Reports will promptly be investigated and responded to.

YamlDotNet.Core7AoTCompileTest/YamlDotNet.Core7AoTCompileTest.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFramework>net10.0</TargetFramework>
66
<PublishAot>true</PublishAot>
77
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
88
<Nullable>enable</Nullable>

YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>net8.0;net47</TargetFrameworks>
3+
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">net10.0;net8.0;net4.7</TargetFrameworks>
4+
<TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('Windows'))">net10.0;net8.0</TargetFrameworks>
45
<IsPackable>false</IsPackable>
56
<AssemblyOriginatorKeyFile>..\YamlDotNet.snk</AssemblyOriginatorKeyFile>
67
<SignAssembly>true</SignAssembly>

YamlDotNet.Samples.Fsharp/YamlDotNet.Samples.Fsharp.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFrameworks>net10.0</TargetFrameworks>
66
<IsPackable>false</IsPackable>
77
<LangVersion></LangVersion>
88
<WarningLevel>5</WarningLevel>

YamlDotNet.Samples/YamlDotNet.Samples.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<IsPackable>false</IsPackable>
66
</PropertyGroup>
77

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// This file is part of YamlDotNet - A .NET library for YAML.
2+
// Copyright (c) Antoine Aubry and contributors
3+
//
4+
// Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
// this software and associated documentation files (the "Software"), to deal in
6+
// the Software without restriction, including without limitation the rights to
7+
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
8+
// of the Software, and to permit persons to whom the Software is furnished to do
9+
// so, subject to the following conditions:
10+
//
11+
// The above copyright notice and this permission notice shall be included in all
12+
// copies or substantial portions of the Software.
13+
//
14+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
// SOFTWARE.
21+
22+
using System;
23+
using Xunit;
24+
using YamlDotNet.Core;
25+
using YamlDotNet.Core.Events;
26+
27+
namespace YamlDotNet.Test.Core
28+
{
29+
public class StringInterningTests
30+
{
31+
[Fact]
32+
public void AnchorNameDoesNotInternUniqueValues()
33+
{
34+
var value = UniqueValue("anchor");
35+
Assert.Null(string.IsInterned(value));
36+
37+
var anchor = new AnchorName(value);
38+
39+
Assert.Equal(value, anchor.Value);
40+
Assert.Null(string.IsInterned(value));
41+
}
42+
43+
[Fact]
44+
public void TagNameDoesNotInternUniqueValues()
45+
{
46+
var value = "!" + UniqueValue("tag");
47+
Assert.Null(string.IsInterned(value));
48+
49+
var tag = new TagName(value);
50+
51+
Assert.Equal(value, tag.Value);
52+
Assert.Null(string.IsInterned(value));
53+
}
54+
55+
[Fact]
56+
public void ScalarKeyDoesNotInternUniqueValues()
57+
{
58+
var value = UniqueValue("key");
59+
Assert.Null(string.IsInterned(value));
60+
61+
var scalar = new Scalar(
62+
AnchorName.Empty,
63+
TagName.Empty,
64+
value,
65+
ScalarStyle.Plain,
66+
isPlainImplicit: true,
67+
isQuotedImplicit: false,
68+
Mark.Empty,
69+
Mark.Empty,
70+
isKey: true);
71+
72+
Assert.True(scalar.IsKey);
73+
Assert.Equal(value, scalar.Value);
74+
Assert.Null(string.IsInterned(value));
75+
}
76+
77+
private static string UniqueValue(string prefix)
78+
{
79+
return string.Concat(prefix, "_", Guid.NewGuid().ToString("N"));
80+
}
81+
}
82+
}

YamlDotNet.Test/Serialization/MergingParserTests.cs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@
1919
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2020
// SOFTWARE.
2121

22+
using System;
2223
using System.Collections.Generic;
2324
using System.IO;
25+
using System.Text;
26+
using System.Threading;
27+
using System.Threading.Tasks;
2428
using FluentAssertions;
2529
using Xunit;
2630
using YamlDotNet.Core;
@@ -151,5 +155,134 @@ public void MergingParserWithNestedSequence_ShouldNotThrowException()
151155

152156
new SerializerBuilder().Build().Serialize(yamlObject!).NormalizeNewLines().Should().Be(etalonMergedYaml);
153157
}
158+
159+
[Fact]
160+
public async Task MergingParserWithMergeKeyBomb_ShouldThrowExceptionWhenTooManyEvents()
161+
{
162+
// Timebox this test to avoid infinite loops in case of bugs.
163+
// 30 seconds should be more than enough for this test to run even on a slow machine, and if it takes longer than that,
164+
// it's likely that the merging parser is not correctly counting events and enforcing the limit.
165+
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
166+
cancellationTokenSource.Token.Register(() =>
167+
{
168+
throw new TimeoutException("The test took too long, likely due to an infinite loop in the merging parser.");
169+
});
170+
171+
await Task.Run(() =>
172+
{
173+
var sb = new StringBuilder();
174+
175+
// Base anchor
176+
sb.AppendLine("a0: &a0");
177+
sb.AppendLine(" x: 1");
178+
sb.AppendLine();
179+
180+
// Each level merges the previous anchor TWICE (fanout=2), doubling event count
181+
for (int i = 1; i <= 25; i++)
182+
{
183+
sb.AppendLine($"a{i}: &a{i}");
184+
sb.AppendLine($" <<: *a{i - 1}"); // first merge
185+
sb.AppendLine($" <<: *a{i - 1}"); // second merge
186+
sb.AppendLine();
187+
}
188+
189+
sb.AppendLine("final:");
190+
sb.AppendLine(" <<: *a25");
191+
192+
var yaml = sb.ToString();
193+
var parser = new Parser(new StringReader(yaml));
194+
var mergingParser = new MergingParser(parser, 1000);
195+
try
196+
{
197+
while (mergingParser.MoveNext())
198+
{
199+
//move through everything, we're in a timebox so if this takes too long, the cancellation token will trigger and fail the test
200+
}
201+
}
202+
catch (YamlException ex) when (ex.Message.Contains("Too many parsing events"))
203+
{
204+
// Expected exception, test passes
205+
return;
206+
}
207+
catch (Exception ex)
208+
{
209+
throw new Exception($"Unexpected exception", ex);
210+
}
211+
}, cancellationTokenSource.Token);
212+
}
213+
214+
[Fact]
215+
public async Task MergingParserWithManySmallMerges_ShouldThrowExceptionWhenCumulativeEventsExceedLimit()
216+
{
217+
// Timebox this test to avoid infinite loops in case of bugs.
218+
// 30 seconds should be more than enough for this test to run even on a slow machine, and if it takes longer than that,
219+
// it's likely that the merging parser is not correctly counting events and enforcing the limit.
220+
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
221+
cancellationTokenSource.Token.Register(() =>
222+
{
223+
throw new TimeoutException("The test took too long, likely due to an infinite loop in the merging parser.");
224+
});
225+
226+
await Task.Run(() =>
227+
{
228+
var sb = new StringBuilder();
229+
sb.AppendLine("base: &base");
230+
for (var i = 0; i < 25; i++)
231+
{
232+
sb.AppendLine($" k{i}: v{i}");
233+
}
234+
235+
sb.AppendLine();
236+
for (var i = 0; i < 35; i++)
237+
{
238+
sb.AppendLine($"entry{i}:");
239+
sb.AppendLine(" <<: *base");
240+
sb.AppendLine();
241+
}
242+
243+
var parser = new Parser(new StringReader(sb.ToString()));
244+
var mergingParser = new MergingParser(parser, 1000);
245+
246+
Action parse = () =>
247+
{
248+
while (mergingParser.MoveNext())
249+
{
250+
}
251+
};
252+
253+
parse.Should().Throw<YamlException>()
254+
.Where(ex => ex.Message.Contains("Too many parsing events"));
255+
}, cancellationTokenSource.Token);
256+
}
257+
258+
[Fact]
259+
public void MergingParserWithDeepSingleChain_ShouldParseWithinLimit()
260+
{
261+
const int depth = 200;
262+
var sb = new StringBuilder();
263+
264+
sb.AppendLine("a0: &a0");
265+
sb.AppendLine(" root: value");
266+
sb.AppendLine();
267+
268+
for (var i = 1; i <= depth; i++)
269+
{
270+
sb.AppendLine($"a{i}: &a{i}");
271+
sb.AppendLine($" <<: *a{i - 1}");
272+
sb.AppendLine($" level{i}: {i}");
273+
sb.AppendLine();
274+
}
275+
276+
sb.AppendLine("final:");
277+
sb.AppendLine($" <<: *a{depth}");
278+
279+
var parser = new Parser(new StringReader(sb.ToString()));
280+
var mergingParser = new MergingParser(parser, 50000);
281+
var deserializer = new DeserializerBuilder().Build();
282+
283+
var yamlObject = deserializer.Deserialize<Dictionary<string, object>>(mergingParser);
284+
285+
yamlObject.Should().ContainKey("final");
286+
}
154287
}
155288
}

YamlDotNet.Test/YamlDotNet.Test.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>net10.0;net8.0;net47</TargetFrameworks>
3+
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('Windows'))">net10.0;net8.0;net4.7</TargetFrameworks>
4+
<TargetFrameworks Condition="!$([MSBuild]::IsOSPlatform('Windows'))">net10.0;net8.0</TargetFrameworks>
45
<IsPackable>false</IsPackable>
56
<AssemblyOriginatorKeyFile>..\YamlDotNet.snk</AssemblyOriginatorKeyFile>
67
<SignAssembly>true</SignAssembly>

YamlDotNet/Core/AnchorName.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public AnchorName(string value)
5050
$"disallowed characters: []{{}},\nThe value was '{value}'.", nameof(value));
5151
}
5252

53-
this.value = string.Intern(value);
53+
this.value = string.IsInterned(value) ?? value;
5454
}
5555

5656
public override string ToString() => value ?? "[empty]";

YamlDotNet/Core/Events/Scalar.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,8 @@ public sealed class Scalar : NodeEvent
7979
public Scalar(AnchorName anchor, TagName tag, string value, ScalarStyle style, bool isPlainImplicit, bool isQuotedImplicit, Mark start, Mark end, bool isKey = false)
8080
: base(anchor, tag, start, end)
8181
{
82-
// In a real client program, keys are likely to be
83-
// interned already. Thus, IsInterned might work
84-
// much better, but you can't really benchmark that.
8582
this.Value = isKey ?
86-
string.Intern(value) :
83+
string.IsInterned(value) ?? value :
8784
value;
8885
this.Style = style;
8986
this.IsPlainImplicit = isPlainImplicit;

0 commit comments

Comments
 (0)