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.

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.
| Call | Builds arbitrary objects | Use for untrusted input |
|---|---|---|
yaml.safe_load | No, standard tags only | Yes |
yaml.full_load | Limited, refused our payload | No, prefer safe_load |
yaml.load(x, Loader=yaml.Loader) | Yes, in older versions | No |
yaml.load(x, Loader=yaml.UnsafeLoader) | Yes | Never |
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:
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 objects | Aliases |
|---|---|---|
YAML.safe_load | No, allow-list only | Off unless aliases: true |
YAML.load | No, same restrictions | Off unless aliases: true |
YAML.unsafe_load | Yes | On |
With Psych 5.2.2, loading --- !ruby/object:Admin (a class we defined) in YAML.load and YAML.safe_load:
YAML.load: Psych::DisallowedClass: Tried to load unspecified class: Admin
YAML.safe_load: Psych::DisallowedClass: Tried to load unspecified class: Admin
YAML.unsafe_load: AdminBoth 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:
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.
Object o = new Yaml().load("!!java.io.File [/tmp/x]");With SnakeYAML 2.4:
default Yaml: ComposerException: Global tag is not allowed: tag:yaml.org,2002:java.io.FileThe 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:
Yaml safe = new Yaml(new SafeConstructor(new LoaderOptions()));
safe.load("name: orders\nports: [8080, 8443]\n");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:
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.


