Safeguard
Vulnerability Analysis

PyYAML Loader arbitrary code execution (CVE-2017-18342)

PyYAML's default yaml.load() Loader lets attackers run arbitrary code via crafted YAML input. Here's how CVE-2017-18342 works and how to fix it.

Nayan Dey
Security Researcher
7 min read

A single call to yaml.load() on attacker-influenced input is enough to hand over a Python process — no sandbox escape, no memory corruption, just YAML doing exactly what its default loader was built to do. That is the core of CVE-2017-18342, a critical deserialization flaw in PyYAML, the ubiquitous Python library used to parse YAML across CI pipelines, configuration loaders, Kubernetes tooling, and countless internal services. Any application that passes untrusted or semi-trusted YAML to PyYAML's default Loader can be coerced into instantiating arbitrary Python objects, and with the right constructor, into executing arbitrary shell commands.

What's actually vulnerable

PyYAML's yaml.load() function was, for years, unsafe by design rather than by accident. Unlike yaml.safe_load(), which restricts deserialization to a fixed set of primitive types (strings, numbers, lists, dicts), the default Loader class supports YAML tags that map directly onto Python's object model — including !!python/object/apply and !!python/object/new. Those tags let a YAML document instruct the parser to call an arbitrary Python callable with attacker-supplied arguments at parse time.

The canonical proof-of-concept looks like this:

!!python/object/apply:subprocess.check_output [['id']]

or the classic:

!!python/object/apply:os.system ["id"]

Feed either string into yaml.load(data) (with no Loader argument, or with Loader=yaml.Loader/Loader=yaml.FullLoader) and PyYAML will happily import os or subprocess, resolve the named callable, and invoke it with the supplied arguments — before your application ever sees a parsed dictionary. There is no need for the caller to opt into anything unsafe; yaml.load() was simply unsafe by default.

Affected versions and components

  • PyYAML < 5.1 is squarely in scope for CVE-2017-18342, where yaml.load() had no way to select a safer loader without the caller explicitly opting in.
  • PyYAML 5.1 began emitting a YAMLLoadWarning when yaml.load() was called without an explicit Loader= argument, nudging developers toward SafeLoader, but did not remove the dangerous default outright — it also introduced FullLoader, which is safer than the original default Loader but is not equivalent to SafeLoader.
  • A related, easily confused issue is CVE-2020-14343, which affects FullLoader and UnsafeLoader in PyYAML before 5.4 and closes a separate gap where FullLoader could still be abused via Python's object.__reduce__ machinery. Teams remediating CVE-2017-18342 should upgrade past both fixes (PyYAML 5.4+) rather than stopping at 5.1.
  • Because PyYAML is a transitive dependency of an enormous swath of the Python ecosystem — Ansible, AWS CLI, Docker Compose, Salt, various Django and Flask extensions, and thousands of internal tools — the practical blast radius extends well beyond code that directly imports yaml. Any service that ingests YAML-formatted configuration, webhook payloads, CI job definitions, or Kubernetes manifests through one of these dependencies inherits the same exposure.

Severity, exploit prediction, and known exploitation

CVE-2017-18342 carries a CVSS v3.1 base score of 9.8 (Critical) under the vector AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — network-exploitable, no privileges or user interaction required, and full compromise of confidentiality, integrity, and availability once triggered. That score reflects the worst-case scenario: an internet-facing service that deserializes attacker-controlled YAML with the default loader.

EPSS (Exploit Prediction Scoring System) context is worth reading carefully rather than at face value. Because this is a library-level deserialization primitive rather than a single exploitable endpoint, EPSS — which models the probability of near-term, in-the-wild exploitation of a specific CVE ID — tends to under-represent real-world risk here. The actual exploitation surface depends entirely on how a downstream application feeds data into yaml.load(), which EPSS's internet-scan-driven model isn't built to capture for library CVEs. Treat CVSS as the ceiling on impact and use reachability analysis, not EPSS alone, to judge whether your specific deployment is exploitable.

As of this writing, CVE-2017-18342 is not listed in CISA's Known Exploited Vulnerabilities (KEV) catalog. That should not be read as a signal of low risk — YAML deserialization gadgets of this shape (mirrored in Ruby's Psych, Java's SnakeYAML, and other ecosystems) have a long history of being weaponized in red-team tooling, CTFs, and real intrusions once a vulnerable code path is identified, even without a dedicated KEV entry for this specific library.

Timeline

  • Long-standing, pre-disclosure: PyYAML's documentation had, for years, informally acknowledged that yaml.load() behaves like pickle.load() for a subset of tags and should not be used on untrusted input — but this was buried in docs rather than enforced in code.
  • 2017: The behavior was formally catalogued and assigned as part of a batch of retrospective CVEs covering long-known but previously unticketed issues in widely used Python libraries.
  • 2018: CVE-2017-18342 was published in the NVD, bringing the issue into mainstream vulnerability-management tooling and dependency scanners for the first time under a trackable identifier.
  • May 2019 — PyYAML 5.1 released: Introduced FullLoader and began warning when Loader was omitted, marking the first concrete step toward a safer default.
  • 2020: CVE-2020-14343 was filed against FullLoader/UnsafeLoader, showing that the 5.1 mitigation was incomplete.
  • March 2021 — PyYAML 5.4 / 5.4.1 released: Closed the FullLoader gap and made SafeLoader the unambiguous recommendation for any untrusted input, effectively completing the remediation lineage that started with 5.1.

Remediation steps

  1. Inventory your PyYAML footprint. Run pip show pyyaml and check requirements.txt, poetry.lock, Pipfile.lock, or pip freeze output across every service — including containers and CI runners. Don't forget transitive pulls via Ansible, AWS CLI, Docker Compose, and similar tools.
  2. Upgrade to PyYAML 5.4.1 or later. This covers both CVE-2017-18342 and CVE-2020-14343 in one move. pip install --upgrade pyyaml at minimum; pin the floor in your dependency manifest so it can't silently regress.
  3. Grep your codebase for unsafe calls. Search for yaml.load( without an explicit Loader=yaml.SafeLoader (or CSafeLoader) argument, and for any use of yaml.load(..., Loader=yaml.Loader) or Loader=yaml.FullLoader on data that isn't fully trusted and internally generated.
  4. Standardize on yaml.safe_load() / yaml.safe_load_all(). These wrap SafeLoader and are the correct default for parsing configuration files, webhook bodies, user uploads, or anything crossing a trust boundary. Reserve FullLoader or UnsafeLoader for cases where you control both ends of the serialization and have a specific reason to need Python object round-tripping.
  5. Add a lint/CI gate. A simple pre-commit hook or CI grep step (yaml\.load\([^,)]*\) without Loader=) catches regressions before they reach production, since a single new yaml.load() call without a loader argument can reintroduce the vulnerability even after the library upgrade.
  6. Validate and constrain YAML inputs at the boundary. Where possible, apply a schema check (e.g., jsonschema against the loaded structure, or cfn-lint/kubeval-style validators for domain-specific YAML) after safe-loading, so malformed or unexpected structures are rejected regardless of the loader in use.
  7. Monitor for anomalous process behavior. For services that must parse less-trusted YAML, runtime monitoring for unexpected child-process spawning from Python worker processes (e.g., a config-parsing service suddenly forking /bin/sh) provides a defense-in-depth signal if a bypass or misconfiguration slips through.

How Safeguard Helps

Safeguard's SBOM generation and ingestion pipeline identifies every instance of PyYAML — direct or transitive — across your services and containers, flagging exactly which versions predate the 5.4.1 fix line for CVE-2017-18342 and CVE-2020-14343. Rather than stopping at "PyYAML is present," Safeguard's reachability analysis traces whether yaml.load() or an unsafe Loader is actually called on a code path that touches untrusted input, so security teams can triage the handful of genuinely exploitable services instead of chasing every vendored copy of the library. Griffin AI, Safeguard's contextual reasoning engine, reviews the surrounding code to distinguish a safe internal deserialization use case from a real attack surface, cutting through the noise that pure SCA scanners generate on old, high-CVSS library findings like this one. When a fix is warranted, Safeguard opens an auto-fix pull request that bumps PyYAML to a patched version and, where it detects unsafe yaml.load() calls, proposes the safe_load() swap directly in the diff — turning a multi-day remediation sprint into a reviewable PR.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.