Skip to content

fix: bound YAML anchor/alias expansion to prevent OOM (billion laughs) - #3000

Merged
Vincent Biret (baywet) merged 3 commits into
microsoft:mainfrom
Treicysg:fix/yaml-alias-expansion-dos
Aug 11, 2026
Merged

fix: bound YAML anchor/alias expansion to prevent OOM (billion laughs)#3000
Vincent Biret (baywet) merged 3 commits into
microsoft:mainfrom
Treicysg:fix/yaml-alias-expansion-dos

Conversation

@Treicysg

@TreicysgTreicy Sanchez (Treicysg) commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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

OpenApiYamlReader parses YAML via SharpYaml's YamlStream.Load, which produces a DAG where an alias (*a) resolves to the same shared YamlNode instance — so the parsed graph stays small. The explosion happens in YamlConverter.ToJsonNode, which converts that DAG into a System.Text.JsonJsonNode tree. Because JsonNode is single-parent (a node cannot be attached to two parents), every alias occurrence must be materialized as an independent copy. With no bound, N nested anchors each referenced k times expand to k^N nodes → 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.ToJsonNode that enforces two limits:

LimitDefaultProtects against
Max materialized node count5,000,000Exponential anchor/alias expansion — the counter measures expanded nodes, so it trips well before OOM
Max nesting depth64Deep-nesting stack overflow in the recursive converter

Rationale for the values:

  • Depth 64 mirrors the default System.Text.JsonMaxDepth already 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.
  • Node count 5,000,000 is comfortably above legitimate specs (whose node count is linear in document size when they don't rely on alias fan-out), while a bomb trips almost immediately because the counter measures the expansion.

On breach the budget throws OpenApiReaderException, which OpenApiYamlReader.Read converts into an OpenApiDiagnosticerror (Document = null) — consistent with the existing JsonException handling — instead of allowing an OOM.

Configurable limits

The two limits are exposed as public static uint properties on YamlConverter so consumers are never blocked by the defaults:

  • YamlConverter.MaxDepth (default YamlConverter.DefaultMaxDepth = 64)
  • YamlConverter.MaxNodeCount (default YamlConverter.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. uint makes the non-negative intent explicit at the type level (negative literals fail to compile), and the setters reject 0. These are the only additions to the public API (recorded in PublicAPI.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.Tests suite 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.

PropertyValue
Sourcemicrosoftgraph/msgraph-metadata @ 73fc270c924975a98f8f9d93d61fd4cff2297084, path openapi/beta/openapi.yaml
SHA-256830108DDB021845583F0C9F6D185BCE465CA4CB1E673E50B2F98DB05E54C21AD
Size66.4 MB (69,627,334 bytes), OpenAPI 3.0.4
Content18,485 paths, 10,368 component schemas

Result under the patched build (with default limits):

MetricMeasuredLimitUtilization
Materialized JSON nodes1,735,8555,000,000~35% (≈2.9× headroom)
Max nesting depth1464~22% (≈4.5× headroom)
OpenApiDocument.LoadAsyncsuccess, 0 diagnostics

The 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

  • This is distinct from CVE-2026-49451 (circular $ref stack overflow), which was already fixed in 3.5.4.
  • ReadFragment is 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.
  • The same fix applies byte-for-byte to support/v2; a companion PR will follow. support/v1 uses a different YAML parsing path and needs a separate assessment.

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
@Treicysg
Treicy Sanchez (Treicysg) requested a review from a team as a code ownerAugust 7, 2026 17:05
Comment threadsrc/Microsoft.OpenApi.YamlReader/YamlConverter.cs Outdated
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
Comment threadsrc/Microsoft.OpenApi.YamlReader/YamlConverter.cs Outdated

@baywetVincent Biret (baywet) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for making the changes!

@baywet
Vincent Biret (baywet) merged commit 2179326 into microsoft:mainAug 11, 2026
9 checks passed
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)
CopilotAI added a commit that referenced this pull request Aug 12, 2026
…on 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>
github-actionsBot pushed a commit to psmfd/agent-expertise-api that referenced this pull request Aug 17, 2026
Updated [Microsoft.OpenApi](https://github.com/Microsoft/OpenAPI.NET)
from 2.11.0 to 2.12.0.
<details>
<summary>Release notes</summary>
_Sourced from [Microsoft.OpenApi's
releases](https://github.com/Microsoft/OpenAPI.NET/releases)._
## 2.12.0
##
[2.12.0](microsoft/OpenAPI.NET@v2.11.0...v2.12.0)
(2026-08-12)
### Features
* adds deserialization of the example extension
([095ae3b](microsoft/OpenAPI.NET@095ae3b))
* serialize license identifier as extension for earlier versions
([d5cdce8](microsoft/OpenAPI.NET@d5cdce8))
* serialize license identifier as extension for earlier versions
([fde38d8](microsoft/OpenAPI.NET@fde38d8))
### Bug Fixes
* better nullability round-tripping
([7a25659](microsoft/OpenAPI.NET@7a25659))
* bound YAML anchor/alias expansion to prevent OOM (billion laughs)
([#​3000](microsoft/OpenAPI.NET#3000))
([a361360](microsoft/OpenAPI.NET@a361360))
* bound YAML anchor/alias expansion to prevent OOM (billion laughs)
([#​3000](microsoft/OpenAPI.NET#3000))
([4db9af0](microsoft/OpenAPI.NET@4db9af0))
* **library:** serialize multiple schema types as anyOf/oneOf for
OpenAPI 3.0
([6568896](microsoft/OpenAPI.NET@6568896))
* marks deprecated properties from the specification as obsolete
([26aba69](microsoft/OpenAPI.NET@26aba69))
* marks deprecated properties from the specification as obsolete
([abc5301](microsoft/OpenAPI.NET@abc5301))
* **schema:** serialize compatibility examples from examples list
([be57a7c](microsoft/OpenAPI.NET@be57a7c))
* serialize examples as extension in v2/v3
([d27141b](microsoft/OpenAPI.NET@d27141b))
Commits viewable in [compare
view](microsoft/OpenAPI.NET@v2.11.0...v2.12.0).
</details>
[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Microsoft.OpenApi&package-manager=nuget&previous-version=2.11.0&new-version=2.12.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Treicysg@gavinbarron@baywet