The Watchdog Bites Back: CVE-2026-22871 in DataDog GuardDog
Jan 14, 2026·5 min read
Executive Summary (TL;DR)
GuardDog, a tool meant to identify malicious Python packages, contained a logic flaw in how it extracted ZIP archives. By misusing `os.path.join` and `zipfile.extract`, it allowed malicious packages to break out of the temporary sandbox and write files anywhere on the file system. This grants attackers RCE on the machines of security researchers and CI/CD pipelines scanning their code.
A critical path traversal vulnerability in GuardDog's archive extraction logic allows malicious PyPI packages to overwrite arbitrary files on the host system, turning the security scanner into an attack vector.
The Hook: When the Shield Becomes the Sword
In the world of supply chain security, we build tools to gaze into the abyss so you don't have to. GuardDog is one of those tools—a CLI utility from DataDog designed to sniff out malicious PyPI packages before they wreck your environment. It effectively creates a sandbox, rips open the package, and looks for signs of evil. It's the digital equivalent of a bomb disposal robot.
But here is the dark irony: in versions prior to 2.7.1, the bomb disposal robot was rigged to explode the moment it touched a specific type of bomb. CVE-2026-22871 is a path traversal vulnerability that turns GuardDog against its master. Instead of protecting you from malware, GuardDog effectively executes the malware's deployment phase simply by scanning it.
This isn't just a bug; it's a perfect example of how security tooling itself is a high-value target. If I'm an attacker, why try to phish a developer when I can just upload a "test" package to PyPI that compromises the very security researchers analyzing it?
The Flaw: A Tale of Two Paths
The vulnerability lies in guarddog/utils/archives.py, specifically within the safe_extract() function. The developers needed to unzip Python packages (which are just ZIP files) to a temporary directory for analysis. To do this, they iterated through the files in the archive and extracted them one by one.
However, they committed a cardinal sin of Python development: blindly trusting os.path.join(). In Python, os.path.join(path, *paths) has a sharp edge. If any component in *paths is an absolute path (e.g., /etc/passwd or C:\Windows), it discards all previous components. It assumes that if you provided an absolute path, you meant it.
Furthermore, the developers passed this tainted path directly to zipfile.ZipFile.extract(). Now, extract() does its own sanitization, but only on the filename relative to the target directory. It assumes the path argument passed to it (the target directory) is trusted. By calculating the path before calling extract, GuardDog accidentally treated the untrusted filename from the ZIP as the trusted destination directory. It's like checking someone's ID at the front gate, but then letting them choose which room they want to be locked in—including the master bedroom.
The Code: The Smoking Gun
Let's look at the diff. It is a terrifyingly simple logic error. The code iterates over every file in the ZIP archive and decides where to put it.
The Vulnerable Code:
# loops through all files in the zip
zip.extract(file, path=os.path.join(target_directory, file))Here, file is the name of the entry inside the malicious ZIP (controlled by the attacker). If file is /tmp/pwned, os.path.join ignores target_directory entirely and returns /tmp/pwned. The zip.extract method then receives /tmp/pwned as the directory argument.
The Fix (Commit 9aa6a72):
# The fix is to trust the library, not the math
zip.extract(file, path=target_directory)By simply passing the target_directory as the root, the zipfile library takes over responsibility. It looks at file, sees it has absolute paths or traversal characters, and sanitizes them relative to target_directory. The fix wasn't about adding more regex; it was about understanding the API contract.
The Exploit: Dropping the Payload
Exploiting this is trivial and requires no race conditions or complex memory corruption. We just need to craft a valid ZIP file that lies about its contents.
Step 1: The Setup
We create a ZIP file (let's call it payload.whl). We edit the ZIP headers manually or use a python script to insert a file entry with a malicious name. We don't want a standard file; we want a file named with directory traversal characters.
Step 2: The Path Injection
We name our file entry ../../../../home/user/.ssh/authorized_keys. Or, thanks to the os.path.join quirk, we can just name it /home/user/.ssh/authorized_keys.
Step 3: The Trigger
The victim runs guarddog scan payload.whl.
- GuardDog creates a temp dir:
/tmp/guarddog_scan/. - It reads our file entry:
/home/user/.ssh/authorized_keys. - It calls
os.path.join('/tmp/guarddog_scan/', '/home/user/.ssh/authorized_keys'). - Python returns
/home/user/.ssh/authorized_keys. - GuardDog calls
zip.extract(..., path='/home/user/.ssh/authorized_keys').
Step 4: Game Over The library writes our public key content into the victim's SSH directory. The next time we SSH into the victim's machine, we are logged in as them.
The Impact: Who Watches the Watchmen?
The severity here is High (8.7) because of the context. This tool is used in CI/CD pipelines to gatekeeping releases. If an attacker can compromise the pipeline scanner, they can potentially inject code into the software being built, leading to a massive supply chain attack.
For individual researchers, it means that analyzing malware is now a high-risk activity (more than usual). If you downloaded a suspicious package to check it with GuardDog, you just gave that package RCE on your workstation. The vulnerability requires no authentication and happens automatically upon scanning.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
GuardDog DataDog | < 2.7.1 | 2.7.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 |
| Attack Vector | Network / Local (Malicious File) |
| CVSS v4.0 | 8.7 (High) |
| Impact | Arbitrary File Overwrite / RCE |
| Vulnerable API | zipfile.ZipFile.extract |
| Patch Status | Fixed in 2.7.1 |
MITRE ATT&CK Mapping
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.