By NHI Mgmt Group Editorial TeamBased on Semgrep: “Fully loaded: testing vulnerable PyYAML versions” (October 22, 2025)

TL;DR: yaml.Loader is unsafe in all released versions of PyYAML, while yaml.safe_load remains safe in every version and yaml.load, yaml.full_load, and their _all variants are only known-safe after 5.4, according to Semgrep. The practical lesson is that version-specific API behaviour, not documentation alone, should drive deserialization rules and code scanning.


At a glance

What this is: Semgrep tested PyYAML versions and concluded that loader choice, not the function name alone, determines whether YAML deserialization is safe.

Why it matters: This matters because insecure deserialization can still become code execution, so code scanning rules and secure coding guidance need version-aware exceptions rather than blanket warnings.


Context

PyYAML deserialization is a governance problem because the same API surface can change from dangerous to safe across releases, and static rules that ignore version context will either miss real risk or flood teams with false positives. For identity and access programmes, this is a reminder that security controls on code behaviour need the same lifecycle discipline as credentials and permissions.

The core issue is not YAML itself but how a parser turns structured input into Python objects. When loaders can instantiate arbitrary objects, the boundary between data and code collapses, which makes secure code review, SAST tuning, and dependency management part of the control set.

In this case, the safest pattern is not to assume one function family is always acceptable. Version-aware policy is the only reliable way to distinguish genuinely unsafe deserialization paths from APIs that later became constrained.


Key questions

Q: When does PyYAML deserialization become unsafe in practice?

A: It becomes unsafe when untrusted YAML is parsed with a loader that can construct Python objects or otherwise execute attacker-controlled tags. In this article's findings, yaml.Loader is unsafe across released versions, while safe_load remains safe and some loader variants only became known-safe after 5.4. Version and loader choice both matter.

Q: Why do security teams need version-aware rules for YAML parsing?

A: Because the same API name can have different exploitability across package releases. A rule that ignores the installed PyYAML version will either miss genuine risk or generate false positives on code that uses safer modes in newer releases. Version-aware policy keeps findings tied to actual attack surface.

Q: What do teams get wrong about safe YAML loading?

A: They often treat the presence of a safe API as proof that the whole codebase is safe. In reality, one unsafe call site is enough to reintroduce deserialization risk, especially when legacy code or copied snippets still use yaml.load or an unsafe loader variant.

Q: How should organisations handle unsafe YAML loader usage in older code?

A: They should replace unsafe loaders with yaml.safe_load wherever possible, then document any remaining object-capable parsing as an exception with version context and code-owner review. If an application cannot be upgraded immediately, the parser should be scanned and tightly constrained rather than assumed safe.


Technical breakdown

Why PyYAML loader choice changes the risk profile

PyYAML exposes several ways to parse YAML, but they do not all behave the same. The dangerous pattern is deserialization that can construct Python objects, because that moves input handling from data parsing into object instantiation. In older and unsafe modes, attacker-controlled YAML can trigger arbitrary object creation and, in some cases, code execution. The important technical distinction is between safe parsing, which only yields basic data types, and object-capable loading, which can interpret tags and constructors. That distinction is what makes a single API family appear safe in one version and unsafe in another.

Practical implication: Treat loader selection as a security control, not a coding preference.

Why version-specific behaviour matters more than documentation alone

Library documentation often describes intended behaviour, but security teams need the observed behaviour of the exact version in use. PyYAML changed its defaults over time, added loader variants, and later patched known exploit paths in FullLoader. That means a rule written against the API name alone will over-alert on some versions and under-alert on others. Static analysis has to encode the version boundary if it wants to be actionable. This is the same general problem seen in many dependency-driven risks: the exploitability of a call site depends on the package release, not just the import or function name.

Practical implication: Tune SAST and dependency policies to versioned behaviour, not generic API labels.

How safe defaults reduce deserialization exposure

The safest path in PyYAML is to restrict parsing to the safe loader family, because safe loading avoids object construction altogether. That matters because deserialization attacks usually rely on application code handing parsing power to the attacker through implicit constructors or custom tags. Once that capability is removed, many exploit payloads fail even if malformed YAML is still accepted. For practitioners, this is a design choice as much as a remediation choice: you reduce exposure by making safe parsing the default and treating object-capable loaders as exceptional, explicitly reviewed code paths.

Practical implication: Use safe parsing by default and reserve object-capable loaders for narrowly justified cases.


Threat narrative

Attacker objective: The attacker wants code execution through malicious YAML deserialization.

  1. Entry occurs when an attacker supplies crafted YAML to an application that uses an unsafe loader path.
  2. Credential_harvested is not the right pattern here, because the exploit path is object construction rather than stolen secrets.
  3. Escalation happens when the loader instantiates attacker-controlled Python objects and turns input into executable behaviour.
  4. Impact is arbitrary code execution on the host running the parser.
  • PyPI secrets exposure 2023: Researchers found 3,938 unique secrets in published PyPI packages, 768 still valid; a new release or yank does not remove them.
  • Ultralytics PyPI compromise 2024: A GitHub Actions injection and a leftover PyPI token let attackers publish four Ultralytics releases that installed a cryptominer.

Read and download The State of NHI & AI Agent Breach Report 2026, covering 200+ breaches impacting Non-Human Identities including AI Agents.


NHI Mgmt Group analysis

Version-aware deserialization is a governance requirement, not a nice-to-have. The article shows that API safety changed over time and that blanket scanner rules are too blunt when package behaviour shifts across releases. Security teams need controls that track both the call site and the dependency version. The practical conclusion is that deserialization policy must be version-scoped, or it will either miss exploitability or drown teams in false positives.

Loader selection is the real security boundary in PyYAML. The difference between safe parsing and object-capable loading is the difference between data handling and code execution potential. That boundary belongs in secure coding standards, code review, and SAST rules because the parser choice determines whether untrusted input can influence runtime behaviour. Practitioners should treat unsafe loaders as prohibited except under narrowly reviewed exceptions.

Safe defaults reduce risk only when teams actually standardise on them. The article makes clear that safe_load has remained safe across versions, but the existence of safe APIs does not eliminate misuse elsewhere in the codebase. This is the same control pattern seen in secrets and identity work: the secure option only helps when governance makes it the default. The practitioner takeaway is to convert “use the safe loader” into enforceable policy, not guidance hidden in a wiki.

Static analysis rules should be precise enough to be actionable. Semgrep's adjustment reflects a broader pattern in application security tooling: findings must map to current exploitability, not historic API reputation alone. Overbroad alerts get ignored, while version-aware detections help developers focus on code that is still dangerous. Teams should expect their code scanning logic to evolve as libraries and patch lines change.

What this signals

Version-scoped parser policy is the right control model for deserialization risk. The important change for practitioners is that library reputation is not enough; the installed release determines whether a call site is actually dangerous. That means secure coding standards, SAST tuning, and dependency governance need to be aligned so teams do not carry obsolete warnings or miss current exposure.

Safe loaders only help when code review makes them the default. A mature programme should treat unsafe loaders as exceptions that require explicit justification, not as acceptable alternatives that developers can choose casually. This is especially relevant in codebases with older YAML handling patterns, where a single legacy call site can undermine the broader control posture.


For practitioners

  • Standardise on safe YAML loading Require yaml.safe_load for untrusted input wherever YAML is parsed, and block use of object-capable loaders in code review unless a documented exception exists.
  • Version-gate deserialization findings Update SAST and Semgrep rules so they alert on loader usage only where the installed PyYAML version is actually vulnerable, rather than on every historical API reference.
  • Inventory unsafe loader call sites Search codebases for yaml.load, yaml.full_load, yaml.Loader, and UnsafeLoader usage, then classify each instance by package version and business justification.
  • Pin and upgrade PyYAML dependencies Move applications off older PyYAML releases so the safer API set is available, and track deserialization-risk exceptions as part of dependency governance.

Key takeaways

  • PyYAML deserialization safety is version-dependent, and unsafe loaders can still create code execution risk when they accept untrusted input.
  • Semgrep's testing shows that yaml.Loader remains unsafe across released versions, while yaml.safe_load is the stable safe option.
  • The control that matters is version-aware loader governance, backed by code scanning that reflects the package release actually deployed.

Standards & Framework Alignment

This section maps relevant standards and security frameworks to the operational risks and controls described in this guidance.

OWASP API Security Top 10 addresses the attack and risk surface, while OWASP ASVS, NIST CSF 2.0 and CIS Controls v8 set the governance and control requirements practitioners need to meet.

FrameworkControl / ReferenceRelevance
OWASP API Security Top 10API8 — Security MisconfigurationUnsafe loader choice is a parsing misconfiguration that enables object creation from untrusted input.
Recommendation — Treat unsafe YAML loader paths as API misconfiguration and remove them from exposed code paths.
OWASP ASVSV1 — Encoding and SanitizationYAML parsing safety depends on preventing attacker-controlled input from being interpreted as executable objects.
Recommendation — Validate that untrusted input is parsed as data only, not as executable object graphs.
NIST CSF 2.0PR.DS-01 — Data-at-rest is protectedThe article is about controlling how untrusted data is processed, which falls under secure data handling.
Recommendation — Apply data-handling controls that keep untrusted content from crossing into executable context.
CIS Controls v8CIS-16 — Application Software SecurityCode scanning and secure coding guidance are central to eliminating unsafe deserialization patterns.
Recommendation — Use secure development controls to detect and remove unsafe deserialization calls before release.

Key terms

  • Unsafe Deserialisation: Unsafe deserialisation happens when an application turns untrusted serialized data back into objects without strict validation. In Python, this can become code execution if the format supports callable reconstruction or hidden execution paths. Authentication code should avoid it entirely when processing cookies, sessions, or request data.
  • Loader: A loader is malware whose main job is to retrieve, decrypt, inject, or launch a second-stage payload. In these campaigns, loaders are not the final objective. They are the mechanism that turns an initial click into remote access, persistence, or data theft.
  • Partial Parsing: Partial parsing converts code into a usable tree even when the file is incomplete or syntactically invalid. This lets analysis tools keep working on code that would otherwise fail a strict parser. For security scanning, partial parsing improves resilience and broadens coverage during active development.
  • Static Analysis Rule: A static analysis rule is a pattern-based check that inspects code without running it. In shell security, rules can detect unsafe command use, unquoted expansions, and risky syntax combinations before deployment. The value is early feedback, but the rule set must be kept aligned with parser coverage and known limitations.

Deepen your knowledge

NHI Mgmt Group covers identity security, NHI governance, and agentic AI through independent research, practitioner guides, and the NHI Foundation Level course. It is designed for security practitioners who need to connect control design, governance, and operational enforcement across modern identity programmes.
NHIMG Editorial Note
Published by the NHIMG editorial team on August 2, 2026.
Updated on October 12, 2026.
NHI Mgmt Group, the independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org