Safeguard
Open Source Security

Python's .pth Files Are a Code Execution Primitive, and Attackers Noticed

The June 2026 PyPI worm wave used a *-setup.pth file to execute at interpreter startup — before your code, before your imports, on every single python invocation. It then fetched the Bun JavaScript runtime to run its payload. If your supply chain model stops at setup.py, it has a hole in it.

Safeguard Research Team
Security Research
7 min read

Most people who write Python have never heard of a .pth file. Most people who have heard of them think they are a list of directories to add to sys.path.

They are that. They are also, if a line begins with import, arbitrary code that the interpreter executes at startup — before your script, before your imports, every time anyone runs python in that environment.

The June 2026 PyPI worm wave used exactly this.

What happened

Per GitGuardian's analysis, a Hades/Miasma-family worm wave hit PyPI in early June, with a second wave around 8 June 2026 affecting roughly 29 packages — concentrated in bioinformatics, graph ML, and MCP libraries.

The mechanism:

  1. The package installed a *-setup.pth file into site-packages.
  2. That .pth executed at Python startup, on every interpreter launch.
  3. From there it fetched the Bun JavaScript runtime and used it to execute the actual payload.
  4. The payload harvested credentials, self-spread using the credentials it stole, and exfiltrated to attacker-controlled GitHub repositories.

Combined with the related Miasma campaign, the tally reached over 100 malicious npm and PyPI packages and 471 malicious artifacts.

Three design choices in there are worth pulling apart.

Why .pth is such a good primitive

It runs on every invocation, not just install. setup.py runs once, at install time, and — as of 2026 — is increasingly bypassed by wheel-only installs and --no-build-isolation hygiene. A .pth file runs on python, pytest, pip, your entrypoint, your cron job, the language server your IDE spawned. It is persistence that does not look like persistence.

It is invisible in every place people look. Nobody diffs site-packages for new .pth files. It is not in your dependency manifest, not in your lockfile, not in your SBOM if your SBOM is generated from the manifest rather than the installed tree. It does not appear in pip list. A file named something like zzz-setup.pth in a directory with hundreds of entries is not going to catch anyone's eye.

It survives the mitigations everyone just deployed. --ignore-scripts is an npm concept and irrelevant here. pip install --no-build-isolation does not help. Wheel-only installs do not help, because a wheel can ship a .pth file as data. This vector was untouched by the July hardening wave in either ecosystem.

Bonus: it fires before your security tooling. If your runtime protection is a Python library imported by your application, the .pth already ran.

Why fetch the Bun runtime?

This is the part that reads as almost cheeky, and it is a genuine tell about where attacker tooling is heading.

The .pth gets you a foothold in Python. But the attacker's payload — credential harvesting across browsers and config files, network egress, self-propagation logic — was presumably already written in JavaScript, most likely shared with the same crew's npm campaigns. Rather than port it, they downloaded a JavaScript runtime.

The operational advantages are real: a single payload codebase maintained across both ecosystems; a runtime that arrives as one self-contained binary with no dependencies; and execution that is invisible to any Python-level monitoring, because the malicious logic never runs in the Python process at all. Your Python security tooling sees a subprocess. That is all it sees.

The general lesson: cross-ecosystem payloads are now normal. An organisation that treats "our Python supply chain" and "our JavaScript supply chain" as separate programmes with separate tools and separate owners is defending against an attacker who does not observe that boundary.

Self-spreading via stolen credentials is the multiplier

The worm property is what turns 29 packages into 471 artifacts. Steal a maintainer's PyPI or npm token, publish a malicious version of everything that token can reach, harvest more tokens, repeat. Each successful compromise increases the attacker's publishing surface.

This is why NPM_TOKEN and PyPI API tokens appeared as explicit named targets in the July typosquat campaign too. Registry credentials are not just loot; they are the propagation mechanism. A single maintainer who publishes twelve widely-used packages and has a long-lived token in their CI environment is a hundred-fold amplifier.

What to do about .pth specifically

  • Inventory the installed tree, not just the manifest. Generate SBOMs from what is actually in site-packages, including data files.
  • Enumerate .pth files in every environment and diff them over time. find . -name "*.pth" in your venvs and images. Any line starting with import deserves an explanation.
  • Build in a container and ship the container. An immutable, scanned image is a far better artifact than a pip install at deploy time.
  • Prefer hash-pinned installs (pip install --require-hashes) so a republished version cannot silently change.
  • Deny egress from build environments — the payload had to fetch Bun from somewhere.
  • Rotate registry tokens, scope them per-package, and set expiries. If your PyPI token has no expiry and full account scope, it is a worm vector.

How Safeguard helps, at scale

SBOMs generated from the installed tree. This is the specific gap .pth exploits, and it is a generation problem rather than a scanning problem. Safeguard produces continuous CycloneDX and SPDX SBOMs that reflect what is actually present in the built environment and container image — including data files and artifacts that never appear in a manifest — so a .pth file that arrived with a package is inventoried rather than invisible.

Eleven scanners with behavioural deltas across ecosystems. Safeguard's Supply Chain Core analyses npm, PyPI, and the rest under one engine and one policy, which is the only configuration that matches an attacker running a shared JavaScript payload across both. A package version that newly ships a .pth file containing an import line, or newly downloads a runtime binary at startup, is a behavioural delta — flagged without waiting for a CVE, and correlated across ecosystems rather than siloed per-language.

500K+ pre-vetted zero-CVE components. The bioinformatics, graph ML, and MCP library ecosystems targeted here are exactly the long-tail, low-download-count packages where maintainer capacity is thinnest and vetting matters most. A curated component feed is the strongest available answer for teams whose scientific-computing dependency trees are deep and unglamorous.

Container image scanning as the enforcement point. Since the right architecture is "build in a container, scan the container, ship the container," Safeguard scans the resulting image as a first-class artifact — catching what was installed regardless of how it got there.

Lion, for the credential propagation. The worm property depends entirely on harvesting registry tokens from environments. Lion's just-in-time secret brokering means PYPI_TOKEN and NPM_TOKEN are not resident to be stolen, its egress allowlists block both the Bun fetch and the GitHub exfiltration, and its signed audit trails record what a compromised environment attempted. At scale, this is what breaks the amplification loop rather than treating each new wave as a fresh incident.

Griffin and Eagle for response across a large estate. Eagle determines which of your services genuinely ran an affected version and what credentials were exposed in those environments; Griffin authors and tests the pin, removal, and rotation PRs across every affected repository at once, through your existing merge gates.

If you take one action from this post: go and list the .pth files in your production Python environments. It takes ten minutes and you will learn something either way.

Sources: GitGuardian: Four More Supply Chain Attacks Hit npm and PyPI · Unit 42: The npm Threat Landscape

Never miss an update

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

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.