Most YAML code looks harmless because it is "just configuration," but the way you parse it decides whether a YAML file is inert data or a remote code execution vector. The format itself is fine; the danger lives in parsers that will happily instantiate arbitrary objects, expand recursive references, or let untrusted input reach a load function that was never meant for it. This guide covers the three risks that actually matter when you write or load YAML code, and the safe defaults for each major language.
Why YAML is riskier than it looks
YAML was designed to be human-friendly, and to get there it grew features that most people never use: custom tags, object instantiation, anchors, and aliases. Those features are why some parsers can turn a line of code YAML into a live object in your program's memory. If the YAML comes from a trusted developer committing a config file, that is a convenience. If it comes from an upload form, an API body, or a downloaded artifact, it is an attack surface.
The rule to internalize: the trust level of the YAML source determines which loader you are allowed to use.
Risk 1: Unsafe deserialization
This is the headline risk. Several YAML libraries historically shipped a default load function that would construct arbitrary language objects based on tags in the document. In Python's PyYAML, yaml.load() without a safe loader could be coerced into calling constructors, and a crafted document could achieve code execution. The fix is not clever; it is to never use the unsafe entry point on untrusted data.
import yaml
# UNSAFE on untrusted input — can instantiate arbitrary objects
data = yaml.load(untrusted, Loader=yaml.Loader)
# SAFE — only plain scalars, lists, and dicts
data = yaml.safe_load(untrusted)
Modern PyYAML makes yaml.load() require an explicit Loader argument precisely to stop the old habit. Use yaml.safe_load() for anything you did not write yourself, full stop.
The same idea applies elsewhere. In Java, SnakeYAML's default constructor could instantiate arbitrary types; recent versions ship a SafeConstructor and restrict this by default, but if you are on an older release or override the constructor, you can reintroduce the problem. In Node.js, the js-yaml library removed its unsafe load/safeLoad split years ago, and its current load is safe by default, which is the direction the whole ecosystem has moved.
Risk 2: Billion laughs and alias expansion
YAML anchors (&) and aliases (*) let a document reference a value defined elsewhere. Nested aliases can expand exponentially, the YAML equivalent of the classic "billion laughs" XML entity-expansion attack. A tiny document can force a parser to allocate gigabytes and take down the process.
a: &a ["x","x","x","x","x","x","x","x","x"]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b]
Safe loaders still resolve aliases, so safe_load alone does not fully protect you here. Defend by bounding input: cap the size of any YAML you accept from outside, and where your parser supports it, disable or limit alias resolution for untrusted input. Treat this as a denial-of-service control the same way you would cap a JSON body size.
Risk 3: Injection through generated YAML
The third problem is writing YAML code, not reading it. If you build YAML by string concatenation from user input, you can inject structure. A value containing a newline and a colon can create a new key, and a value that looks like !!python/object can smuggle a tag into a document that is later read by an unsafe loader downstream.
Never template YAML by hand. Serialize with your library's dumper, which quotes and escapes correctly:
import yaml
# correct: let the dumper handle escaping
print(yaml.safe_dump({"name": user_supplied_name}))
This matters most in CI/CD, where a value from an environment variable or a webhook can end up in a generated pipeline definition. A YAML injection into a pipeline file can become command execution in your build runner.
A safe-YAML checklist
- Use
safe_load/SafeConstructor/ the default safe loader for any YAML you did not author. - Reserve full-featured loaders for trusted, in-repo config only, and ideally not even then.
- Cap the byte size of externally supplied YAML and constrain alias expansion.
- Serialize with a real dumper; never concatenate strings into YAML.
- Pin and scan your YAML parser like any other dependency. An SCA tool will flag a parser version with a known deserialization CVE before it reaches production.
Where this bites in real systems
Kubernetes manifests, GitHub Actions workflows, Ansible playbooks, and application config are all YAML. The most common real-world incident is not an exotic payload; it is a service that reads a user-provided YAML config with an unsafe loader because a tutorial used yaml.load(). If you audit for one thing, grep your codebase for yaml.load( and new Yaml( calls that lack a safe constructor, and confirm each one only ever sees trusted input. Our academy covers configuration-as-code hardening in more depth for teams standardizing this across services.
FAQ
Is YAML inherently insecure?
No. YAML is a data format. The insecurity comes from parsers that instantiate arbitrary objects from untrusted documents. Use a safe loader and YAML is as safe as JSON for configuration.
What is the difference between yaml.load and yaml.safe_load?
yaml.safe_load only produces plain data types like strings, numbers, lists, and dicts. yaml.load with a full loader can construct arbitrary Python objects, which enables code execution if the input is attacker-controlled. Use safe_load for untrusted input.
Does safe_load protect against the billion-laughs attack?
Not fully. Safe loaders still resolve anchors and aliases, so a document with deeply nested aliases can still cause excessive memory use. Bound the input size and limit alias expansion separately.
Is js-yaml in Node.js safe by default?
Current js-yaml versions parse safely by default; the old unsafe load path was removed. Still keep the library updated and validate the structure of anything you parse from outside your application.