Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>

<PackageReference Include="SharpYaml" Version="2.1.4" />
<PackageReference Include="SharpYaml" Version="2.1.5" />
<PackageReference Include="System.Text.Json" Version="[8.0.5,)" />
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-hh2w-p6rv-4g7w" />
<NuGetAuditSuppress Include="https://github.com/advisories/GHSA-8g4q-xg66-9fp4" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.OpenApi.YamlReader;
using System;
using Microsoft.OpenApi.YamlReader;

namespace Microsoft.OpenApi.Reader;

Expand All @@ -17,4 +18,18 @@
settings.TryAddReader(OpenApiConstants.Yaml, yamlReader);
settings.TryAddReader(OpenApiConstants.Yml, yamlReader);
}

/// <summary>
/// Adds a YAML reader for the specified format using per-reader resource limits.
/// </summary>
/// <param name="settings">The settings to add the reader to.</param>
/// <param name="yamlSettings">The YAML reader settings.</param>
public static void AddYamlReader(this OpenApiReaderSettings settings, OpenApiYamlReaderSettings yamlSettings)
{
if (settings is null) throw new ArgumentNullException(nameof(settings));

Check warning on line 29 in src/Microsoft.OpenApi.YamlReader/OpenApiReaderSettingsExtensions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'ArgumentNullException.ThrowIfNull' instead of explicitly throwing a new exception instance

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AaAWuuPNwMzhS_2gV9o5&open=AaAWuuPNwMzhS_2gV9o5&pullRequest=3027
if (yamlSettings is null) throw new ArgumentNullException(nameof(yamlSettings));

Check warning on line 30 in src/Microsoft.OpenApi.YamlReader/OpenApiReaderSettingsExtensions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'ArgumentNullException.ThrowIfNull' instead of explicitly throwing a new exception instance

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AaAWuuPNwMzhS_2gV9o6&open=AaAWuuPNwMzhS_2gV9o6&pullRequest=3027
var yamlReader = new OpenApiYamlReader(yamlSettings);
settings.TryAddReader(OpenApiConstants.Yaml, yamlReader);
settings.TryAddReader(OpenApiConstants.Yml, yamlReader);
}
}
167 changes: 141 additions & 26 deletions src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,55 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.OpenApi.Reader;
using SharpYaml.Serialization;
using SharpYaml;
using System;
using System.Linq;
using System.Text;

namespace Microsoft.OpenApi.YamlReader
{
/// <summary>
/// Reader for parsing YAML files into an OpenAPI document.
/// </summary>
/// <remarks>
/// Input is converted directly from SharpYaml parser events so resource limits are enforced
/// before SharpYaml's recursive YAML model loader can compose or expand the document.
/// </remarks>
public class OpenApiYamlReader : IOpenApiReader
{
private const int copyBufferSize = 4096;
private static readonly OpenApiJsonReader _jsonReader = new();
private readonly OpenApiYamlReaderSettings _yamlSettings;

/// <summary>
/// Initializes a YAML reader using the current legacy global conversion limits.
/// </summary>
public OpenApiYamlReader()
: this(new()
{
MaxDepth = YamlConverter.MaxDepth,
MaxNodeCount = YamlConverter.MaxNodeCount,
MaxAliasExpansionNodeCount = YamlConverter.MaxAliasExpansionNodeCount,
})
{
}

/// <summary>
/// Initializes a YAML reader with immutable per-reader resource limits.
/// </summary>
/// <param name="settings">The YAML reader settings.</param>
public OpenApiYamlReader(OpenApiYamlReaderSettings settings)
{
if (settings is null) throw new ArgumentNullException(nameof(settings));

Check warning on line 48 in src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'ArgumentNullException.ThrowIfNull' instead of explicitly throwing a new exception instance

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AaAWuuPzwMzhS_2gV9o8&open=AaAWuuPzwMzhS_2gV9o8&pullRequest=3027
settings.Validate();
_yamlSettings = new()
{
MaxDepth = settings.MaxDepth,
MaxNodeCount = settings.MaxNodeCount,
MaxAliasExpansionNodeCount = settings.MaxAliasExpansionNodeCount,
MaxInputByteCount = settings.MaxInputByteCount,
MaxScalarLength = settings.MaxScalarLength,
};
}

/// <inheritdoc/>
public async Task<ReadResult> ReadAsync(Stream input,
Expand All @@ -29,65 +64,122 @@
CancellationToken cancellationToken = default)
{
if (input is null) throw new ArgumentNullException(nameof(input));
if (settings is null) throw new ArgumentNullException(nameof(settings));
if (input is MemoryStream memoryStream)
{
return UpdateFormat(Read(memoryStream, location, settings));
return ReadCore(memoryStream, location, settings, cancellationToken);
}
else
{
using var preparedStream = new MemoryStream();
await input.CopyToAsync(preparedStream, copyBufferSize, cancellationToken).ConfigureAwait(false);
try
{
await CopyToMemoryStreamAsync(
input,
preparedStream,
_yamlSettings.MaxInputByteCount,
cancellationToken).ConfigureAwait(false);
}
catch (OpenApiReaderException ex)
{
return new()
{
Document = null,
Diagnostic = CreateDiagnostic(new(ex)),
};
}

preparedStream.Position = 0;
return UpdateFormat(Read(preparedStream, location, settings));
return ReadCore(preparedStream, location, settings, cancellationToken);
}
}

/// <inheritdoc/>
public ReadResult Read(MemoryStream input,
Uri location,
OpenApiReaderSettings settings)
=> ReadCore(input, location, settings, CancellationToken.None);

private ReadResult ReadCore(MemoryStream input,
Uri location,
OpenApiReaderSettings settings,
CancellationToken cancellationToken)
{
if (input is null) throw new ArgumentNullException(nameof(input));
if (settings is null) throw new ArgumentNullException(nameof(settings));

Check warning on line 109 in src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'ArgumentNullException.ThrowIfNull' instead of explicitly throwing a new exception instance

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AaAWuuPzwMzhS_2gV9o_&open=AaAWuuPzwMzhS_2gV9o_&pullRequest=3027
cancellationToken.ThrowIfCancellationRequested();
JsonNode jsonNode;

// Parse the YAML text in the stream into a sequence of JsonNodes
try
{
EnsureInputWithinLimit(input, _yamlSettings.MaxInputByteCount);
#if NET
// this represents net core, net5 and up
using var stream = new StreamReader(input, default, true, -1, settings.LeaveStreamOpen);
#else
// the implementation differs and results in a null reference exception in NETFX
using var stream = new StreamReader(input, Encoding.UTF8, true, 4096, settings.LeaveStreamOpen);
#endif
jsonNode = LoadJsonNodesFromYamlDocument(stream);
jsonNode = LoadJsonNodesFromYamlDocument(stream, cancellationToken);
}
catch (JsonException ex)
{
var diagnostic = new OpenApiDiagnostic();
diagnostic.Errors.Add(new($"#line={ex.LineNumber}", ex.Message));
diagnostic.Format = OpenApiConstants.Yaml;
return new()
{
Document = null,
Diagnostic = diagnostic,
Diagnostic = CreateDiagnostic(new($"#line={ex.LineNumber}", ex.Message)),
};
}
catch (OpenApiReaderException ex)
{
var diagnostic = new OpenApiDiagnostic();
diagnostic.Errors.Add(new(ex));
diagnostic.Format = OpenApiConstants.Yaml;
return new()
{
Document = null,
Diagnostic = diagnostic,
Diagnostic = CreateDiagnostic(new(ex)),
};
}

cancellationToken.ThrowIfCancellationRequested();
return UpdateFormat(Read(jsonNode, location, settings));
}

private static async Task CopyToMemoryStreamAsync(
Stream input,
MemoryStream output,
uint maxInputByteCount,
CancellationToken cancellationToken)
{
var buffer = new byte[copyBufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await input.ReadAsync(
buffer,
0,
buffer.Length,
cancellationToken).ConfigureAwait(false)) > 0)

Check warning on line 160 in src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change the 'ReadAsync' method call to use the 'Stream.ReadAsync(Memory<byte>, CancellationToken)' overload

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AaAWuuPzwMzhS_2gV9o9&open=AaAWuuPzwMzhS_2gV9o9&pullRequest=3027
{
if (bytesRead > (long)maxInputByteCount - totalBytesRead)
{
throw CreateInputLimitException(maxInputByteCount);
}

await output.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);

Check warning on line 167 in src/Microsoft.OpenApi.YamlReader/OpenApiYamlReader.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change the 'WriteAsync' method call to use the 'Stream.WriteAsync(ReadOnlyMemory<byte>, CancellationToken)' overload

See more on https://sonarcloud.io/project/issues?id=microsoft_OpenAPI.NET&issues=AaAWuuPzwMzhS_2gV9o-&open=AaAWuuPzwMzhS_2gV9o-&pullRequest=3027
totalBytesRead += bytesRead;
}
}

private static void EnsureInputWithinLimit(MemoryStream input, uint maxInputByteCount)
{
if (input.Length - input.Position > maxInputByteCount)
{
throw CreateInputLimitException(maxInputByteCount);
}
}

private static OpenApiReaderException CreateInputLimitException(uint maxInputByteCount)
=> new($"The YAML input exceeds the maximum supported size of {maxInputByteCount} bytes.");

private static ReadResult UpdateFormat(ReadResult result)
{
result.Diagnostic ??= new OpenApiDiagnostic();
Expand All @@ -114,13 +206,22 @@
// Parse the YAML
try
{
using var stream = new StreamReader(input);
jsonNode = LoadJsonNodesFromYamlDocument(stream);
EnsureInputWithinLimit(input, _yamlSettings.MaxInputByteCount);
#if NET
using var stream = new StreamReader(input, default, true, -1, settings?.LeaveStreamOpen ?? false);
#else
using var stream = new StreamReader(input, Encoding.UTF8, true, 4096, settings?.LeaveStreamOpen ?? false);
#endif
jsonNode = LoadJsonNodesFromYamlDocument(stream, CancellationToken.None);
}
catch (JsonException ex)
{
diagnostic = new();
diagnostic.Errors.Add(new($"#line={ex.LineNumber}", ex.Message));
diagnostic = CreateDiagnostic(new($"#line={ex.LineNumber}", ex.Message));
return default;
}
catch (OpenApiReaderException ex)
{
diagnostic = CreateDiagnostic(new(ex));
return default;
}

Expand All @@ -134,20 +235,34 @@
}

/// <summary>
/// Helper method to turn streams into a sequence of JsonNodes
/// Converts the first YAML document in a stream into a JSON node.
/// </summary>
/// <param name="input">Stream containing YAML formatted text</param>
/// <returns>Instance of a YamlDocument</returns>
static JsonNode LoadJsonNodesFromYamlDocument(TextReader input)
/// <param name="cancellationToken">Propagates notification that parsing should be cancelled.</param>
/// <returns>The converted JSON node.</returns>
private JsonNode LoadJsonNodesFromYamlDocument(TextReader input, CancellationToken cancellationToken)
{
var yamlStream = new YamlStream();
yamlStream.Load(input);
if (yamlStream.Documents.Any() && yamlStream.Documents[0].ToJsonNode() is { } jsonNode)
try
{
return jsonNode;
return new YamlJsonParser(_yamlSettings).Parse(input, cancellationToken);
}
catch (YamlException ex)
{
var location = ex.Start.Line >= 0
? $" at line {ex.Start.Line + 1}, column {ex.Start.Column + 1}"
: string.Empty;
throw new OpenApiReaderException($"Unable to parse the YAML document{location}: {ex.Message}", ex);
}
}

throw new InvalidOperationException("No documents found in the YAML stream.");
private static OpenApiDiagnostic CreateDiagnostic(OpenApiError error)
{
var diagnostic = new OpenApiDiagnostic
{
Format = OpenApiConstants.Yaml,
};
diagnostic.Errors.Add(error);
return diagnostic;
}
}
}
73 changes: 73 additions & 0 deletions src/Microsoft.OpenApi.YamlReader/OpenApiYamlReaderSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;

namespace Microsoft.OpenApi.YamlReader;

/// <summary>
/// Configures resource limits for an <see cref="OpenApiYamlReader"/>.
/// </summary>
public sealed class OpenApiYamlReaderSettings
{
/// <summary>
/// Default maximum number of input bytes read from a single YAML document (128 MiB).
/// Bounds the buffered copy of a non-seekable stream, so an endless or oversized response body
/// cannot exhaust memory before parsing begins.
/// </summary>
public const uint DefaultMaxInputByteCount = 128 * 1024 * 1024;

/// <summary>
/// Default maximum length of a single YAML scalar value (65,536 UTF-16 code units).
/// Bounds the cost of any one key, string, number, date or block literal. For reference, the
/// longest scalar in the Microsoft Graph beta description is 1,833 code units, so this leaves
/// substantial headroom for legitimate documents.
/// </summary>
public const uint DefaultMaxScalarLength = 64 * 1024;

/// <summary>
/// Gets or sets the maximum YAML nesting depth.
/// Defaults to <see cref="YamlConverter.DefaultMaxDepth"/> and cannot exceed
/// <see cref="YamlConverter.MaximumAllowedDepth"/>.
/// </summary>
public uint MaxDepth { get; set; } = YamlConverter.DefaultMaxDepth;

/// <summary>
/// Gets or sets the maximum number of JSON nodes materialized from one YAML document.
/// Defaults to <see cref="YamlConverter.DefaultMaxNodeCount"/> and cannot exceed
/// <see cref="YamlConverter.MaximumAllowedNodeCount"/>.
/// </summary>
public uint MaxNodeCount { get; set; } = YamlConverter.DefaultMaxNodeCount;

/// <summary>
/// Gets or sets the maximum number of JSON nodes materialized specifically from aliases.
/// Defaults to <see cref="YamlConverter.DefaultMaxAliasExpansionNodeCount"/>.
/// </summary>
public uint MaxAliasExpansionNodeCount { get; set; } = YamlConverter.DefaultMaxAliasExpansionNodeCount;

/// <summary>
/// Gets or sets the maximum number of input bytes read from one YAML document.
/// Defaults to <see cref="DefaultMaxInputByteCount"/>.
/// </summary>
public uint MaxInputByteCount { get; set; } = DefaultMaxInputByteCount;

/// <summary>
/// Gets or sets the maximum length of one YAML scalar value.
/// Defaults to <see cref="DefaultMaxScalarLength"/>.
/// </summary>
public uint MaxScalarLength { get; set; } = DefaultMaxScalarLength;

internal void Validate()
{
YamlConverter.ValidateMaxDepth(MaxDepth, nameof(MaxDepth));
YamlConverter.ValidateMaxNodeCount(MaxNodeCount, nameof(MaxNodeCount));
ValidatePositive(MaxAliasExpansionNodeCount, nameof(MaxAliasExpansionNodeCount));
ValidatePositive(MaxInputByteCount, nameof(MaxInputByteCount));
ValidatePositive(MaxScalarLength, nameof(MaxScalarLength));
}

private static void ValidatePositive(uint value, string parameterName)
{
if (value == 0)
{
throw new ArgumentOutOfRangeException(parameterName, $"{parameterName} must be greater than zero.");
}
}
}
21 changes: 21 additions & 0 deletions src/Microsoft.OpenApi.YamlReader/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
#nullable enable
Microsoft.OpenApi.YamlReader.OpenApiYamlReader.OpenApiYamlReader(Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings! settings) -> void
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxAliasExpansionNodeCount.get -> uint
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxAliasExpansionNodeCount.set -> void
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxDepth.get -> uint
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxDepth.set -> void
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxInputByteCount.get -> uint
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxInputByteCount.set -> void
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxNodeCount.get -> uint
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxNodeCount.set -> void
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxScalarLength.get -> uint
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.MaxScalarLength.set -> void
Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.OpenApiYamlReaderSettings() -> void
const Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.DefaultMaxInputByteCount = 134217728 -> uint
const Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings.DefaultMaxScalarLength = 65536 -> uint
const Microsoft.OpenApi.YamlReader.YamlConverter.DefaultMaxAliasExpansionNodeCount = 5000 -> uint
const Microsoft.OpenApi.YamlReader.YamlConverter.MaximumAllowedDepth = 256 -> uint
const Microsoft.OpenApi.YamlReader.YamlConverter.MaximumAllowedNodeCount = 10000000 -> uint
static Microsoft.OpenApi.Reader.OpenApiReaderSettingsExtensions.AddYamlReader(this Microsoft.OpenApi.Reader.OpenApiReaderSettings! settings, Microsoft.OpenApi.YamlReader.OpenApiYamlReaderSettings! yamlSettings) -> void
static Microsoft.OpenApi.YamlReader.YamlConverter.MaxAliasExpansionNodeCount.get -> uint
static Microsoft.OpenApi.YamlReader.YamlConverter.MaxAliasExpansionNodeCount.set -> void
Loading
Loading