PyYAML, the de facto standard YAML parser for Python, shipped a fix for arbitrary code execution in 2017 (CVE-2017-18342) — but the fix was incomplete. CVE-2020-14343 documents a follow-on flaw: even when developers moved to the "safer" full_load() function or yaml.FullLoader, specifically constructed YAML input could still instantiate arbitrary Python objects and execute attacker-controlled code on the host running the parser. For any application that deserializes YAML from an untrusted source — a config file uploaded by a customer, a webhook payload, a CI/CD pipeline artifact, an infra-as-code template pulled from a third-party repo — this is a direct path from "parse a file" to remote code execution.
What the vulnerability actually does
PyYAML's load()/full_load()/FullLoader path was intended to strike a balance: richer than SafeLoader (which only builds plain Python types like dicts, lists, and strings), but without the full unrestricted power of the original Loader, which could construct any Python object via YAML tags such as !!python/object/apply. The 2017 fix restricted the constructors registered on FullLoader, but the restriction list was still too permissive. Researchers found that certain complex Python types and constructors remained reachable from FullLoader, which meant a crafted YAML document could still trigger instantiation of objects whose __init__, __reduce__, or other dunder/constructor logic executes arbitrary code — the same class of exploitation technique used against Python's pickle module and other unsafe deserializers.
In practice, an attacker only needs one thing: an application that calls yaml.load(), yaml.full_load(), or explicitly instantiates yaml.FullLoader on YAML content they can influence. From there, a payload resembling the well-known os.system/subprocess YAML gadget chains used against the original Loader vulnerability can be adapted to work through the incompletely-restricted FullLoader path, resulting in code execution in the context of the parsing process.
Affected versions and components
- Library: PyYAML (
pyyamlon PyPI), the most widely depended-on YAML library in the Python ecosystem, pulled in transitively by an enormous number of frameworks, SDKs, and CLI tools (Ansible, Flask extensions, AWS/GCP/Azure SDKs, CI tooling, and countless internal services). - Vulnerable versions: PyYAML releases prior to 5.4 that expose
full_load()/FullLoader(and, depending on usage,load()with an explicit or default loader that resolves to the vulnerable constructor set). - Fixed version: PyYAML 5.4 (and later), which removed the dangerous constructors from
FullLoader/UnsafeLoaderhandling and tightened the default behavior ofyaml.load()to require an explicit, safe loader. - Indirect exposure: Because PyYAML is a transitive dependency of thousands of packages, teams are frequently exposed without a direct
import yamlanywhere in their own codebase. Any service that accepts YAML — Kubernetes manifests, Helm values files, GitHub Actions/GitLab CI definitions, OpenAPI specs, Ansible playbooks, application config — is a candidate for review.
CVSS, EPSS, and KEV context
CVE-2020-14343 was assigned a CVSS v3 base score in the critical range (9.8), reflecting network-exploitable access, no privileges or user interaction required, and the potential for full compromise of confidentiality, integrity, and availability once code execution is achieved. As with most deserialization CVEs, the practical severity for any given organization depends heavily on reachability: whether attacker-controlled data actually flows into a vulnerable load()/full_load()/FullLoader call, and whether that call path is exposed to untrusted input at all (versus, say, parsing only trusted, internally generated YAML).
EPSS (Exploit Prediction Scoring System) data for this CVE has historically sat well below the threshold associated with widespread automated exploitation — consistent with the fact that abuse requires an application-specific gadget path rather than a single universal exploit chain. This CVE has not been observed on CISA's Known Exploited Vulnerabilities (KEV) catalog. That combination — high CVSS, comparatively low observed-in-the-wild exploitation — is exactly the profile where naive severity-only triage leads teams astray: a scanner that only reports "CVSS 9.8, critical" without checking whether the vulnerable loader is actually reachable from untrusted input will generate a flood of tickets, many of which are not truly exploitable, while genuinely exposed instances get lost in the noise.
Timeline
- 2017: The original unrestricted-
Loaderarbitrary code execution issue is identified and tracked as CVE-2017-18342, prompting PyYAML maintainers to introduceFullLoaderand steer users away from the bareyaml.load()default. - Mid-2020: Security researchers determine that the
FullLoader/full_load()mitigation is incomplete — certain Python constructors remain reachable and can still be abused to achieve code execution, effectively re-opening the same class of bug under a "safer" name. - 2020: The issue is documented publicly via a GitHub Security Advisory for PyYAML (tracked alongside related advisories for the same underlying loader-restriction gap) and assigned CVE-2020-14343.
- January 2021: PyYAML 5.4 ships, tightening the constructor allowlist across
FullLoaderandUnsafeLoader, and changingyaml.load()so that omitting an explicitLoaderargument raises a deprecation warning rather than silently falling back to unsafe behavior. - Ongoing: Because PyYAML sits deep in the dependency tree of frameworks like Ansible and countless CI/CD and infrastructure tools, pinned or vendored copies of pre-5.4 PyYAML continue to surface in dependency audits years after the fix shipped — this is one of the longest-tailed "already fixed, still everywhere" vulnerabilities in the Python ecosystem.
Remediation steps
-
Upgrade PyYAML to 5.4 or later across all direct and, where possible, transitive dependencies. Run
pip show pyyamlor check your lockfile (requirements.txt,poetry.lock,Pipfile.lock) to confirm the resolved version, not just the version declared insetup.py/pyproject.toml. -
Replace unsafe loader calls with
yaml.safe_load()oryaml.SafeLoaderanywhere YAML from an external, uploaded, or otherwise untrusted source is parsed.safe_load()only constructs plain Python built-in types and cannot instantiate arbitrary objects — it is the correct default for nearly all application config, API payload, and file-upload parsing. -
Audit every call site of
yaml.load(),yaml.full_load(), andyaml.FullLoaderin your own code and in vendored/forked dependencies. Grep for these patterns repo-wide; do not assume a single "we fixed it once" pass caught every instance, especially in older modules, internal tooling, and scripts outside the main application. -
Never deserialize YAML from users, partners, or third-party integrations with anything other than
SafeLoader. If your product ingests customer-supplied YAML (config-as-code, pipeline definitions, IaC templates), treat that parsing path as an explicit trust boundary and add a regression test asserting that a known exploit payload is rejected. -
Pin and continuously monitor PyYAML's version via your SCA/dependency scanning pipeline, since it is frequently reintroduced transitively by unrelated package upgrades (a new version of an SDK or CI tool can silently downgrade or re-pin PyYAML).
-
Generate or refresh your SBOM after remediation to confirm PyYAML 5.4+ is what actually ships in your build artifacts and containers — declared dependency ranges in a manifest don't guarantee what got resolved and baked into the image.
How Safeguard Helps
A CVSS 9.8 rating on a library as ubiquitous as PyYAML generates a lot of tickets — most security teams don't have time to manually trace whether their specific call sites reach full_load() with attacker-controlled YAML, or whether it's only ever parsing trusted, internally generated files. Safeguard's reachability analysis traces data flow from actual entry points (HTTP handlers, file uploads, CI job inputs) down to vulnerable loader calls, so teams can immediately separate "PyYAML 5.3 sitting in a container that never parses untrusted YAML" from "PyYAML 5.3 parsing customer-uploaded config on an internet-facing endpoint" — and prioritize accordingly. Griffin, Safeguard's AI-powered detection engine, recognizes the specific unsafe-loader patterns (yaml.load() without an explicit safe loader, full_load(), FullLoader) directly in source, flagging risky call sites even when the vulnerable dependency itself is several layers deep in the tree. Continuous SBOM generation and ingestion keep an accurate, build-time-verified inventory of exactly which PyYAML version ships in each artifact, closing the gap between what your manifest declares and what actually got compiled into production. And where remediation is as mechanical as bumping a pinned version or swapping a loader call, Safeguard can open an auto-fix pull request with the corrected dependency version and, where reachability confirms real exposure, flag the specific unsafe-loader call sites for a targeted code fix — turning a sprawling CVE cleanup into a short, reviewable diff.