Skip to content

StackOverflowException with deeply nested flow sequences/mappings #130

Description

@pawlos

SharpYaml version

3.3.0 (latest)

Environment

  • Linux (WSL2, Ubuntu 22.04)
  • .NET 10.0 (SDK 10.0.103 / Runtime 10.0.3)
  • Also reproducible on any OS / .NET version

Description

Deeply nested YAML flow sequences or mappings cause an unrecoverable StackOverflowException due to unbounded recursion in the parser and DOM loader. The mutual recursion between YamlNode.ReadElement ↔ YamlSequence.Load (and similarly YamlMapping.Load) has no depth limit, so sufficiently nested input exhausts the call stack.

StackOverflowException is particularly severe in .NET because it cannot be caught by try/catch — it terminates the process unconditionally. Any application that parses untrusted YAML input using SharpYaml is vulnerable to denial-of-service via a crafted document.

The threshold is approximately:

  • ~21,000 nested flow sequences ([[[...]]]) for YamlStream.Load
  • ~25,000 for YamlSerializer.Deserialize
  • ~20,000 nested flow mappings ({a: {a: ...}}) for YamlStream.Load

These thresholds vary by platform and available stack size, and could be significantly lower in environments with smaller stacks (e.g., ASP.NET thread pool threads, which default to 1 MB).

Steps to Reproduce

using SharpYaml.Model;

// ~21,000 nested flow sequences — process terminates with StackOverflowException
int depth = 21000;
string input = new string('[', depth) + new string(']', depth);

// This kills the process — cannot be caught
var stream = YamlStream.Load(new StringReader(input));

Also reproducible via YamlSerializer.Deserialize:

using SharpYaml;

int depth = 25000;
string input = new string('[', depth) + new string(']', depth);

_ = YamlSerializer.Deserialize<object>(input);

And with nested flow mappings:

using SharpYaml.Model;
using System.Text;

int depth = 20000;
var sb = new StringBuilder();
for (int i = 0; i < depth; i++) sb.Append("{a: ");
sb.Append("{}");
for (int i = 0; i < depth; i++) sb.Append('}');

var stream = YamlStream.Load(new StringReader(sb.ToString()));

Root Cause

The recursive descent has no depth counter or limit at any level:

  1. DOM layer: YamlNode.ReadElement → YamlSequence.Load → ReadElement → ... (no depth check)
  2. Parser layer: ParseNode → ParseFlowSequenceEntry → ParseNode → ... (no depth check)
  3. Scanner layer: flowLevel tracks depth but is never checked against a maximum

Comparison with Other Libraries

For reference, here's how other .NET parsing libraries handle deeply nested input:

Library Default Max Depth Behavior on Overflow
System.Text.Json 64 Catchable JsonException
Newtonsoft.Json (v13+) 64 Catchable JsonReaderException
SharpYaml 3.3.0 None Unrecoverable StackOverflowException

Both major .NET JSON libraries enforce a default depth limit of 64 with a clean, catchable exception. The YAML spec does not mandate a depth limit, but Java's SnakeYAML added one (configurable, default varies) after similar DoS concerns.

Suggested Fix

The Scanner class already tracks nesting depth via flowLevel (incremented in IncreaseFlowLevel, called from FetchFlowCollectionStart when encountering [ or {). It just never checks it against a maximum. Adding a limit there would protect all higher-level APIs (YamlStream, EventReader, YamlSerializer):

// In Scanner.IncreaseFlowLevel() — add a check before incrementing
private void IncreaseFlowLevel()
{
    if (flowLevel >= MaxFlowLevel)
        throw new SemanticErrorException(/*...*/,
            $"Maximum nesting depth of {MaxFlowLevel} exceeded.");

    // existing code
    simpleKeys.Push(new SimpleKey());
    ++flowLevel;
}

A default of 64 would match the .NET ecosystem convention (System.Text.Json and Newtonsoft.Json both default to 64). This converts an unrecoverable process crash into a catchable SemanticErrorException, consistent with how SharpYaml already handles other malformed input.

Impact

  • Severity: High (DoS — process termination, not catchable)
  • Attack vector: Any application parsing untrusted YAML (web APIs, config file processors, CI/CD pipelines)
  • Input size: ~42 KB for flow sequences, ~100 KB for mappings — small enough to include in HTTP requests

Found by coverage-guided fuzzing with SharpFuzz + AFL++.

Activity

  1. xoofx commented on Mar 11, 2026

    @xoofx
    Owner

    Thank you for the report, agreed. This should be fixed via 1535fb0

  2. pawlos commented on Mar 18, 2026

    @pawlos
    Author

    Hi @xoofx - I noticed that a very similar StackOverflowException via unbounded recursion was recently assigned CVE-2026-32933 for AutoMapper.

    The pattern is essentially identical to this issue: deeply nested input → unbounded recursion → uncatchable StackOverflowException → process termination.

    Would it make sense to retroactively file a GitHub Security Advisory for this fix? That would enable a CVE assignment and help downstream users who rely on vulnerability scanners to track these things. This package has is a transitive dependency of Microsoft.OpenApi.Readers - so the blast radius is significant.

    If you enable "Private vulnerability reporting" in the repo's security settings, I can submit the advisory draft - or happy to help however makes sense.

  3. xoofx commented on Mar 18, 2026

    @xoofx
    Owner

    Would it make sense to retroactively file a GitHub Security Advisory for this fix? That would enable a CVE assignment and help downstream users who rely on vulnerability scanners to track these things. This package has is a transitive dependency of Microsoft.OpenApi.Readers - so the blast radius is significant.

    Personally, I have no incentive nor personal time dedicated to retroactively fix such issues in the old version of SharpYaml. I would prefer such project to migrate to the latest version instead. If a company is worried about this, they should fork and patch it with a local version, but I'm skeptical that it is worth it.

  4. pawlos commented on Mar 18, 2026

    @pawlos
    Author

    @xoofx To be clear - filing a security advisory doesn't require backporting anything. The fix is already in 3.4.0. The advisory just formally says "versions < 3.4.0 are affected, upgrade to 3.4.0" - which is what vulnerability scanners need to warn downstream projects. No additional code changes needed.

  5. added a commit that references this issue on Apr 2, 2026
    1535fb0
  6. gavinbarron commented on Aug 17, 2026

    @gavinbarron

    @xoofx my team owns Microsoft.OpenAPI which currently has a direct dependency on SharpYaml v2.x

    Updating to v3 is a major breaking change for us over three current major versions due to the current public API we publish. If we submit a change to backport this fix to v2 would you be willing/able to ship a new v2.1.5 package?

    Forking and creating a local version is infeasible due to having SharpYaml v2 types in the public API surface for Microsoft.OpenAPI.YamlReaders.

  7. baywet commented on Aug 18, 2026

    @baywet
    Contributor

    A supporting argument to back-port the fix is that, as of writing, cumulated downloads of 3.X are dwarfed by downloads of 2.1.4. That would indicate that a large user base still hasn't migrated to 3.X. Arguably Microsoft.OpenAPI is part of that cohort and influencing the numbers.

    Image
  8. baywet commented on Aug 18, 2026

    @baywet
    Contributor

    I put together this pull request to demonstrate the changes required to port the fix to v2.

    baywet#1

  9. baywet commented on Aug 18, 2026

    @baywet
    Contributor

    additionally, here are the CI changes to enable the release. baywet#2

    I couldn't easily find where the version bump is handled. I'm guessing this is either dotnet-releaser doing that part, or based off the tag. But let me know if I missed anything.

  10. xoofx commented on Aug 18, 2026

    @xoofx
    Owner

    @baywet I have created a branch v2_security_fixes_only and you can create a PR against it. I will manage the changes on the CI to allow 2.x version release, thank you.

  11. baywet commented on Aug 18, 2026

    @baywet
    Contributor

    @xoofx is it possible you haven't published/pushed that branch?

  12. xoofx commented on Aug 18, 2026

    @xoofx
    Owner

    @xoofx is it possible you haven't published/pushed that branch?

    Ooops, yes, I pushed it on a completely unrelated repository 🤭 Just back from holidays, so my brain seems to have trouble to reconnect 😅

    Should be fixed now.

  13. baywet commented on Aug 18, 2026

    @baywet
    Contributor

    Thanks! pushed #158, let me know what you think!

    From what I understand from your previous message, you don't want the other PR for CI changes, correct?

  14. baywet commented on Aug 18, 2026

    @baywet
    Contributor

    @xoofx I've also just drafted the vulnerability report, here

    It's important to publish it after the release (even though this vulnerability has been in the open for a while now). If you're comfortable with the details, you can publish it as is, otherwise feel free to edit it. Also, can you please credit others on this thread? (right menu on the report)

    Why is it important to publish a report? It'll get picked up by the GitHub Security Advisory database, nuget.org, dependabot, security tools, etc... all those tools will enable some automatic scanning of the dependencies, and suggest upgrading. Everyone's security posture will improve from this

    Let me know if you have any additional comments or questions.

  15. pawlos commented on Aug 19, 2026

    @pawlos
    Author

    @baywet Thanks for pushing for the advisory. I was also thinking it was important when disclosed this issue but wasn't convincing enough.

    Since I was reporting the original issue would it be fair to credit me in the advisory? Thanks

  16. baywet commented on Aug 19, 2026

    @baywet
    Contributor

    Since I was reporting the original issue would it be fair to credit me in the advisory? Thanks

    Absolutely! The odd thing with the UI there is as you draft the report, you can't credit anyone. It's only later the repo owners who can do it. Otherwise I'd have added you right away. It's already been updated by @xoofx. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions