Safeguard
Vulnerability Analysis

CVE-2020-1747: PyYAML full_load still allows code execution

CVE-2020-1747 shows PyYAML's FullLoader and full_load() could still trigger arbitrary code execution on untrusted YAML before 5.3.1. Here's the full breakdown.

Vikram Iyer
Security Researcher
8 min read

In March 2020, the Python community learned that PyYAML's "safe-by-design" full loader wasn't safe at all. CVE-2020-1747 affects PyYAML versions before 5.3.1: applications that parse untrusted YAML using the yaml.full_load() method or the FullLoader class can be tricked into executing arbitrary Python code. Because PyYAML is one of the most widely vendored packages in the Python ecosystem — pulled in transitively by tools ranging from Ansible to AWS CLI to countless ML pipelines — this is a textbook example of how a single deserialization flaw in a low-visibility dependency can become an organization-wide exposure. This post breaks down the pyyaml full_load vulnerability, what changed between PyYAML's loader classes, and how to remediate it in real environments.

Background: why FullLoader existed in the first place

To understand CVE-2020-1747, it helps to understand the CVE that came before it. PyYAML's original yaml.load() function, when called without specifying a Loader, used a fully permissive loader capable of instantiating arbitrary Python objects from YAML tags such as !!python/object/apply. This was the subject of CVE-2017-18342, and it meant that any application deserializing untrusted YAML with plain yaml.load() was effectively handing attackers a code-execution primitive.

In response, PyYAML 5.1 (released in 2019) introduced a set of explicit loader classes: SafeLoader (restricted to basic YAML types, safe for untrusted input), FullLoader (intended to support the full YAML specification while still blocking arbitrary code execution), and UnsafeLoader/Loader (the old permissive behavior, kept for backward compatibility). PyYAML also began emitting a deprecation warning when yaml.load() was called without an explicit Loader argument, steering developers toward FullLoader as the new safer default via yaml.full_load().

The problem: FullLoader was marketed and documented as safe for untrusted input, but it wasn't.

The vulnerability

CVE-2020-1747 describes a case where FullLoader's object-construction logic could still be abused to reach arbitrary code execution. The loader's handling of YAML tags used to reconstruct Python objects (the python/object/new and related constructors) applied an incomplete restriction on which classes and behaviors were reachable. By crafting a YAML document that chained together permitted constructs, an attacker could get the loader to instantiate objects and invoke behavior far beyond what FullLoader was supposed to allow — ultimately enabling arbitrary code execution on the host parsing the document.

In short: any application that took untrusted or semi-trusted YAML input (configuration files, uploaded manifests, API payloads, CI/CD pipeline definitions) and parsed it with yaml.full_load() or yaml.load(data, Loader=yaml.FullLoader) was potentially exploitable, with no need for the attacker to have any other foothold. This distinguishes it clearly from yaml.safe_load() / SafeLoader, which was not affected — a distinction that matters a great deal for triage, since many codebases use both loaders in different places.

Affected versions and components

  • Affected: PyYAML before 5.3.1, specifically when using yaml.full_load() or yaml.load() with Loader=FullLoader.
  • Not affected: Code paths using yaml.safe_load() or Loader=SafeLoader.
  • Fixed in: PyYAML 5.3.1, which further restricted the set of objects and tags FullLoader would construct.

Because PyYAML is rarely a direct, top-level dependency for most teams — it's usually pulled in by a framework, orchestration tool, or SDK — the practical affected surface is much broader than "projects that import yaml." Any Docker base image, CI runner, or application server with an older PyYAML version installed and any code path (first-party or third-party) that calls full_load() on attacker-influenced input is in scope.

It's also worth noting that CVE-2020-1747 was not the last word on this class of bug. A related issue, CVE-2020-14343, was later identified in the same FullLoader/full_load() code path and required a further fix in PyYAML 5.4. Teams that only patched to 5.3.1 and stopped tracking PyYAML advisories should confirm they're on a version that addresses both.

CVSS, EPSS, and KEV context

NVD's entry for CVE-2020-1747 carries a CVSS v3.1 base score of 9.8 (Critical), reflecting a network attack vector, low attack complexity, no privileges or user interaction required, and high impact to confidentiality, integrity, and availability — consistent with an unauthenticated remote code execution primitive once untrusted YAML reaches the vulnerable loader.

Real-world exploitability is more nuanced than the base score suggests, because the vulnerability requires a specific coding pattern: an application must actually pass attacker-controlled data to full_load()/FullLoader rather than safe_load(). This "requires a bad call site" characteristic tends to lower observed exploitation activity relative to vulnerabilities exploitable through default configuration alone, which is generally reflected in a comparatively modest EPSS (Exploit Prediction Scoring System) probability for this CVE relative to other 9.8-rated flaws. As of this writing, CVE-2020-1747 does not appear on CISA's Known Exploited Vulnerabilities (KEV) catalog, meaning there is no confirmed record of active, widespread in-the-wild exploitation tracked by CISA — though the absence of a KEV listing should never be read as "safe to ignore," particularly for internet-facing services that deserialize YAML from users.

Timeline

  • 2017 — CVE-2017-18342 is disclosed, covering the original unrestricted yaml.load() default loader.
  • 2019 — PyYAML 5.1 ships FullLoader, SafeLoader, and UnsafeLoader, along with deprecation warnings for calling yaml.load() without an explicit loader, positioning FullLoader/full_load() as the safer default for full-YAML use cases.
  • Early 2020 — Researchers determine that FullLoader's object-construction restrictions can be bypassed, allowing arbitrary code execution despite the loader's "safe" positioning. The issue is tracked as CVE-2020-1747.
  • March 2020 — PyYAML 5.3.1 is released, tightening the constructor restrictions in FullLoader to close the bypass.
  • Later in 2020–2021 — A further related bypass (CVE-2020-14343) in the same loader family is identified and fixed in PyYAML 5.4, underscoring that this was an iterative hardening effort rather than a single clean fix.

Remediation steps

  1. Upgrade PyYAML. Move to a version that includes both the 5.3.1 and 5.4 fixes for the FullLoader/full_load() bypasses. Treat this as a hard floor for any service that touches YAML.
  2. Prefer yaml.safe_load() / SafeLoader for untrusted input. Unless your application genuinely needs to reconstruct arbitrary Python objects from YAML (rare), safe_load() is the correct default and was not affected by this vulnerability.
  3. Eliminate implicit yaml.load() calls. Audit for any remaining calls to yaml.load() without an explicit Loader= argument — these inherit the old unsafe behavior on older PyYAML versions and are a code smell even on patched versions.
  4. Trace the dependency graph, not just direct imports. Since PyYAML is usually a transitive dependency, check container images, lockfiles, and vendored code — not just your requirements.txt — for outdated copies.
  5. Add a regression test or lint rule. Once fixed, add a static check (or a bandit/semgrep rule) that flags new full_load()/FullLoader usage on data that isn't fully trusted, so the fix doesn't silently regress in future commits.
  6. Re-verify after patching. Confirm the fix by checking the installed PyYAML version at runtime (python -c "import yaml; print(yaml.__version__)") in the actual deployed artifact, not just the source repo — pinned base images and cached build layers are a common source of "fixed in code, not fixed in production."

How Safeguard Helps

CVE-2020-1747 is a clear illustration of why vulnerability management can't stop at "is this package listed in my manifest." PyYAML's exposure here lives several layers deep in most software supply chains, and the real risk hinges on how a specific function is called — details generic dependency lists don't capture.

Safeguard is built for exactly this problem:

  • Continuous SCA and SBOM visibility across direct and transitive dependencies, so a vulnerable PyYAML version buried inside a CI tool, base image, or third-party SDK doesn't go unnoticed just because it isn't a top-level requirement.
  • Version-aware vulnerability matching that maps installed PyYAML versions against known-affected ranges for CVE-2020-1747 and its follow-on advisories, rather than relying on stale point-in-time scans.
  • Build and deployment gating that can block or flag artifacts shipping known-vulnerable PyYAML versions before they reach production, closing the gap between "we patched the code" and "the fix actually made it to the running service."
  • Continuous monitoring for newly disclosed advisories affecting packages already in your environment, so teams aren't relying on manual changelog-reading to catch iterative fixes like the 5.3.1 → 5.4 progression seen here.
  • Prioritization grounded in reachability and context — factoring in CVSS severity, EPSS exploitation likelihood, and KEV status alongside how a package is actually used in your codebase — so security and engineering teams can focus remediation effort on what's genuinely exploitable rather than triaging every CVE with equal urgency.

Deserialization bugs in widely embedded libraries like PyYAML rarely announce themselves loudly. Treating dependency risk as a continuous, supply-chain-wide discipline — rather than a point-in-time scan — is what turns disclosures like CVE-2020-1747 into a routine patch cycle instead of an incident.

Never miss an update

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