fix: bound YAML anchor/alias expansion to prevent OOM (billion laughs) - #3000
Merged
Vincent Biret (baywet) merged 3 commits intoAug 11, 2026
Merged
Conversation
The YAML reader converts the SharpYaml node graph - a DAG in which aliases share a single instance - into a System.Text.Json JsonNode tree, allocating a fresh node per path. Because JsonNode is single-parent, shared aliases must be duplicated, so a tiny document with nested anchors/aliases expands exponentially and exhausts process memory (CWE-400, uncontrolled resource consumption). Add a conversion budget to YamlConverter.ToJsonNode that caps the total materialized node count (5,000,000) and nesting depth (64, mirroring the System.Text.Json default already enforced on the JSON reader path). On breach it throws OpenApiReaderException, which OpenApiYamlReader.Read converts into an OpenApiDiagnostic error instead of allowing an OOM. Public API is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 022bbd4f-e5e7-447a-bcdf-b2a4efaf75c3
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
Vincent Biret (baywet)
requested changes
Aug 10, 2026
Vincent Biret (baywet)
approved these changes
Aug 10, 2026
Vincent Biret (baywet)
left a comment
Member
There was a problem hiding this comment.
Thank you for making the changes!
Vincent Biret (baywet)
added a commit
that referenced
this pull request
Aug 11, 2026
fix: bound YAML anchor/alias expansion to prevent OOM (billion laughs) (#3000)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The YAML reader is exposed to an uncontrolled-resource-consumption ("billion laughs") denial of service (CWE-400). A tiny YAML document (well under 1 KB) using nested anchors/aliases can force the process to allocate many gigabytes and be OOM-killed. See https://portal.microsofticm.com/imp/v5/incidents/details/31000000667626/summary
Root cause
OpenApiYamlReaderparses YAML via SharpYaml'sYamlStream.Load, which produces a DAG where an alias (*a) resolves to the same sharedYamlNodeinstance — so the parsed graph stays small. The explosion happens inYamlConverter.ToJsonNode, which converts that DAG into aSystem.Text.JsonJsonNodetree. BecauseJsonNodeis single-parent (a node cannot be attached to two parents), every alias occurrence must be materialized as an independent copy. With no bound,Nnested anchors each referencedktimes expand tok^Nnodes → exponential memory → OOM.Deduplication/sharing is not possible (single-parent constraint), so the only viable fix is to bound the work and fail fast.
Fix
Add a conversion budget threaded through
YamlConverter.ToJsonNodethat enforces two limits:Rationale for the values:
System.Text.JsonMaxDepthalready enforced on the JSON reader path (JsonNode.Parse), so this brings YAML to parity — any document deeper than 64 already fails today when supplied as JSON.On breach the budget throws
OpenApiReaderException, whichOpenApiYamlReader.Readconverts into anOpenApiDiagnosticerror (Document = null) — consistent with the existingJsonExceptionhandling — instead of allowing an OOM.Configurable limits
The two limits are exposed as public static
uintproperties onYamlConverterso consumers are never blocked by the defaults:YamlConverter.MaxDepth(defaultYamlConverter.DefaultMaxDepth= 64)YamlConverter.MaxNodeCount(defaultYamlConverter.DefaultMaxNodeCount= 5,000,000)A consumer that must ingest an unusually large/deep-but-trusted document can raise the limits; a consumer that only ever parses small documents can lower them to fail faster.
uintmakes the non-negative intent explicit at the type level (negative literals fail to compile), and the setters reject0. These are the only additions to the public API (recorded inPublicAPI.Unshipped.txt).Note the limits are process-wide static state, best configured once at startup. They are an escape hatch for the defaults, not per-parse/per-thread configuration.
Tests
YamlConverterTests.ExponentialAliasExpansionIsRejected— a nested anchor/alias bomb is rejected instead of exhausting memory.YamlConverterTests.ExcessiveNestingDepthIsRejected— nesting beyond the depth limit is rejected.YamlConverterTests.LegitimateAliasesStillConvert— normal alias usage still converts correctly.YamlConverterTests.ConversionLimitsDefaultToDocumentedValues— the properties expose the documented defaults.YamlConverterTests.SettingMaxDepthToZeroThrows/SettingMaxNodeCountToZeroThrows— zero limits are rejected and leave the effective limit unchanged.YamlConverterTests.RaisingMaxDepthAllowsDocumentsDeeperThanTheDefault— raising the limit permits a document deeper than the default.OpenApiYamlReaderTests.ReadReturnsDiagnosticErrorForExponentialAliasExpansion— the reader surfaces a diagnostic error (no document), not a throw/OOM.Full
Microsoft.OpenApi.Readers.Testssuite passes.Validation on a large real-world spec (no false positives)
To confirm the limits do not reject legitimately large production descriptions, the Microsoft Graph beta OpenAPI document was loaded end-to-end through the patched reader.
microsoftgraph/msgraph-metadata@73fc270c924975a98f8f9d93d61fd4cff2297084, pathopenapi/beta/openapi.yaml830108DDB021845583F0C9F6D185BCE465CA4CB1E673E50B2F98DB05E54C21ADResult under the patched build (with default limits):
OpenApiDocument.LoadAsyncThe largest realistic production spec sits well below both caps, while the exponential bomb (theoretical ~387M nodes) is rejected almost immediately. The limits target the exponential pathology, not document size.
Notes / scope
$refstack overflow), which was already fixed in 3.5.4.ReadFragmentis still protected by the budget (it throws rather than OOM) but does not convert the exception to a diagnostic — left out of scope intentionally; happy to extend if preferred.support/v2; a companion PR will follow.support/v1uses a different YAML parsing path and needs a separate assessment.