Skip to content
datahacks.dev
YAML

Safe Loaders in Python Ruby and Java Compared and Tested

Which function to call in PyYAML, Ruby Psych and SnakeYAML to load untrusted YAML safely, what each default does and how to grep for the unsafe ones.

6 min read
Summarize in AI
Flat illustration of a tree of nested objects beside the document it came from, with the Python logo.

Choosing the safe function sounds like a trivial matter and is the place where most YAML vulnerabilities start. Each language names its functions differently, the defaults have changed between versions, and old examples still circulate with the unsafe call. This post puts the three most common libraries side by side with output from current versions, then gives you a checklist and a grep to audit an existing codebase. It belongs to the cluster on YAML security pitfalls, and the underlying mechanism is in why loading untrusted YAML can run code.

Python: PyYAML

PyYAML offers four loading entry points, and the names do not say which is safe.

CallBuilds arbitrary objectsUse for untrusted input
yaml.safe_loadNo, standard tags onlyYes
yaml.full_loadLimited, refused our payloadNo, prefer safe_load
yaml.load(x, Loader=yaml.Loader)Yes, in older versionsNo
yaml.load(x, Loader=yaml.UnsafeLoader)YesNever

In our test a !!python/object/apply:os.system document was refused by safe_load and full_load with ConstructorError, and executed by UnsafeLoader, which wrote a marker file. A call with no Loader argument:

Text
TypeError load() missing 1 required positional argument: 'Loader'

That error is the PyYAML 6 protection against the old pattern. When someone silences it with Loader=yaml.Loader, they have chosen the loader that can construct Python objects; that line in a pull request deserves a question. For performance, yaml.CSafeLoader is the same safe behavior on libyaml, and yaml.load(text, Loader=yaml.CSafeLoader) is the accepted fast path. Its name has Safe in it for a reason.

The writing side has the same split: yaml.safe_dump writes only basic types and raises RepresenterError on others, while yaml.dump can write Python-specific tags that only PyYAML can read back, so do not use it for files other tools consume.

Ruby: Psych

Ruby's YAML is Psych. The history is the important part.

Call (Psych 4 and later)Builds arbitrary objectsAliases
YAML.safe_loadNo, allow-list onlyOff unless aliases: true
YAML.loadNo, same restrictionsOff unless aliases: true
YAML.unsafe_loadYesOn

With Psych 5.2.2, loading --- !ruby/object:Admin (a class we defined) in YAML.load and YAML.safe_load:

Text
YAML.load: Psych::DisallowedClass: Tried to load unspecified class: Admin
YAML.safe_load: Psych::DisallowedClass: Tried to load unspecified class: Admin
YAML.unsafe_load: Admin

Both refused; only unsafe_load built the object. That is the modern behavior, and it is a break from Psych 3 and older, where YAML.load was the unsafe call. If your application runs on an old Ruby or a locked old Psych, or a gem calls YAML.load on a version before the change, assume the unsafe behavior until you check Psych::VERSION.

To permit specific classes, pass them: YAML.safe_load(text, permitted_classes: [Symbol, Date]). Keep the list short and review each addition. Aliases are separate. A document with one anchor and one alias raised:

Text
safe_load with alias: Psych::AliasesNotEnabled: Alias parsing was not enabled. To enable it, pass `aliases: true` to `Psych::load` or `Psych::safe_load`.

and with aliases: true returned {"a" => [1, 2], "b" => [1, 2]}. Rails' own YAML config loading enables aliases, which is why Rails apps keep working with anchors. Enable them where your files use them and nowhere else, since aliases are the lever of alias bomb attacks.

Java: SnakeYAML

SnakeYAML is the common Java library, and it changed its defaults in 2.0.

Java
Object o = new Yaml().load("!!java.io.File [/tmp/x]");

With SnakeYAML 2.4:

Text
default Yaml: ComposerException: Global tag is not allowed: tag:yaml.org,2002:java.io.File

The default now rejects global tags, which are the tags that name Java classes. Earlier versions (1.x) happily constructed whatever the tag named, which allowed chains of gadget classes to run code, and the library was a repeated source of advisories. For the strongest guarantee, construct the parser with SafeConstructor, which supports only standard types:

Java
Yaml safe = new Yaml(new SafeConstructor(new LoaderOptions()));
safe.load("name: orders\nports: [8080, 8443]\n");
Text
safe map: {name=orders, ports=[8080, 8443]}

LoaderOptions is also where limits live: setMaxAliasesForCollections (default 50) refused our alias bomb with Number of aliases for non-scalar nodes exceeds the specified max=50, and there are settings for nesting depth and code point limits. Spring Boot and other frameworks create their own Yaml instances; check their configuration and their SnakeYAML version when you upgrade.

Other languages in one paragraph

Go's gopkg.in/yaml.v3 decodes into types you give it and has no tag-based object construction, so the code-execution class does not apply; it has an alias check that refused our bomb with document contains excessive aliasing. Node's yaml package and js-yaml (version 4 and later) default to a safe schema, and js-yaml 3 had safeLoad and an unsafe load, the same trap in different words. PHP's Symfony Yaml component parses to arrays and does not construct objects unless you pass an object-support flag. Rust's serde_yaml deserializes into your declared types. The general rule across languages: prefer a library that deserializes into types you declare, and read the changelog for the word "unsafe".

Auditing an existing codebase

A grep finds the usual suspects. These patterns are worth searching:

Shell
grep -rnE "yaml\.load\(|yaml\.Loader|UnsafeLoader|yaml\.unsafe_load|full_load" --include=*.py .
grep -rnE "YAML\.load|YAML\.unsafe_load|Psych\.unsafe_load" --include=*.rb .
grep -rnE "new Yaml\(\)|new Yaml\(new Constructor" --include=*.java .

For each hit, answer three questions. Where does the input come from, and who can change it? Which library version is installed (pip show pyyaml, bundle exec gem list psych, the dependency tree for SnakeYAML)? Is there a safe call that would do the same thing? The third answer is almost always yes: for plain configuration, safe_load does the work, and the code that needs custom types is rare and deserves its own review. Automated scanners (Bandit for Python, Brakeman for Rails, SpotBugs rules for Java) flag unsafe YAML loading, and running one in CI keeps the pattern from returning.

Also check transitive dependencies. A library you use may parse YAML with an unsafe default, and the fix then is an upgrade of that library. Dependabot-style alerts cover known advisories.

A short policy to write down

Teams that avoid this class of bug usually have a written rule, and it fits in a few lines. Parse YAML with the safe function of the language, named in the code review checklist. Do not accept Loader=yaml.Loader, unsafe_load or an unconfigured new Yaml() on any input that did not come from the same process. Pin parser versions and upgrade on advisories. Cap the size of any file parsed from outside the repository. Reject documents with aliases where the format does not need them. Write a test that feeds the loader a document with an object tag and asserts that loading fails, so a future refactor that swaps the loader is caught by CI instead of by an attacker. That last test is five lines, and it documents the rule better than a comment.

When a safe loader is not the whole defense

Aliases and size. A safe loader parses an alias bomb without complaint. Add size caps and alias limits, as in limiting alias expansion in a YAML parser.

Values drive actions. A loaded string that becomes a file path, a URL or a command is injection by another name.

The loader is configurable. permitted_classes in Ruby, register_class_map in PyYAML, custom Constructor subclasses in Java: each extension widens what a document can do. Keep the surface small and review it.

Old versions linger. A safe call in a library pinned to a release before the fix does not help. Keep parsers updated.

The functions are documented in the PyYAML documentation, the Psych module reference and the SnakeYAML documentation. For a first look at an unknown document's syntax without loading it into your application, use the YAML formatter.

Also inSecurity
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.