ADVISORY · VIGI-002-2026 pystr-codec WORMPYPI vigihq.com

AdvisoryVIGI-002-2026October 31, 20265 min read

pystr-codec runs a worm at every Python start that steals tokens and republishes itself

pystr-codec 0.1.1 adds a startup file that downloads Bun and runs a hidden 17 MB script. The script collects developer keys and uses them to publish poisoned releases.

By Marcello Delcaro

Package
pystr-codec
Bad version
0.1.1
Registry
PyPI
Status
Live on PyPI
First seen
31 Oct 2026, 22:46 UTC
Flagged
31 Oct 2026, 22:58 UTC

The short version

If you install pystr-codec 0.1.1, Python runs its startup file every time Python starts. Once per machine, that file downloads Bun from Bun’s official GitHub releases. Then it runs a 17 MB script that ships inside the package.

The script reads the keys and tokens that a developer keeps on the machine. It checks which ones can publish code. It then uses them to publish new releases of the victim’s own packages and to add workflows to the victim’s GitHub repositories.

Nothing runs during pip install. The code runs at the next Python start.

Timeline

  • Version 0.1.0 goes up on PyPI. It carries the library code only.
  • Version 0.1.1 goes up on PyPI, 25 minutes later. It adds the startup file and the 17 MB script.
  • Vigilance flags it. The watcher compares 0.1.0 against 0.1.1 and sees a file that runs at every Python start. The old version had no such file. Twelve minutes after upload, with no signature and no prior report of this package anywhere.
  • We decode the script offline and report the package to the PyPI security team.
  • PyPI still serves the package. OSV has no record for it.

What the update changed

Version 0.1.1 adds two files to the one wheel the package ships. Every other file stays the same, apart from the package metadata.

pystr_codec-0.1.0 → 0.1.1   py3-none-any

+ pystr_codec-setup.pth        1,391 bytes
+ pystr_codec/ci.js       17,908,149 bytes

Python reads every .pth file in site-packages at start. It runs any line that starts with import. This file holds one such line. Shortened, it does this:

if no file named .bun_ran in the temp folder:
    find ci.js on the Python path
    download bun-v1.2.18/bun-<os>-<arch>.zip from github.com/oven-sh
    unpack bun into <temp>/b/
    run: bun run ci.js
    create <temp>/.bun_ran
A codec has no use for this. String codec code does not need to run at every Python start or download a JavaScript runtime.

What the script does

We decoded ci.js offline in a locked container. It has three layers:

  1. A hex string, shifted by a letter cipher, then passed to eval.
  2. Two AES-128-GCM blobs, with the keys in the file. One is a helper that downloads Bun. The other is a 10.96 MB compressed script that runs in a new Bun process.
  3. A bundle scrambled with a common JavaScript obfuscator. Its text table decodes with a changed base64 alphabet.

The decoded text names each part of the program in its log lines. These are the parts:

Collector Reads ~/.ssh, ~/.aws, ~/.kube, ~/.npmrc, ~/.pypirc and the keychain. Tests GitHub, GitLab, npm, PyPI, RubyGems and JFrog tokens for write access.
Google Cloud Lists projects and reads secrets from Secret Manager.
PyPI Publishes with a stolen token, or exchanges a GitHub Actions identity token for a PyPI token. It names the -setup.pth file pattern that this package uses.
GitHub and npm Lists writable repositories, adds workflows and force-pushes them, to publish through npm.
Artifactory Replaces cached npm packages in a JFrog registry.
Provenance Creates Sigstore and Rekor entries for the releases it publishes.
Dispatcher Sends the collected results out in batches.

We read the text of the program. We did not run it, so we did not see where the dispatcher sends its results.

The same family

This loader matches the “Hades” wave of the Mini Shai-Hulud and Miasma worm. Socket reported that wave in June 2026: 37 wheels across 19 PyPI packages, each with a *-setup.pth file that downloads Bun and runs an obfuscated script. The Hacker News and GitLab covered it too.

SameThe -setup.pth name, the .bun_ran marker, the <temp>/b/bun path, the ROT and AES-128-GCM layers, and the list of secrets.
DifferentThe script is ci.js, not _index.js. The loader downloads Bun 1.2.18, not 1.3.13. It searches the whole Python path for the script. The main stage goes to Bun through standard input, not a file in /tmp.

We are the first to report pystr-codec. We are not the first to report this worm.

What the records showed

On 31 October 2026 at 23:13 UTC, OSV had no record for pystr-codec. Nothing runs during pip install, so an install-time sandbox sees a clean package. The payload sits under three layers of encoding and encryption, so a text search of the package finds no address and no secret path.

Who is behind it

The package metadata names no author, no home page, no licence and no project links. Both versions went up on the same day, 25 minutes apart. We did not identify the PyPI account behind it.

If you ran this

This applies to anyone who installed pystr-codec 0.1.1 and then started Python.

  1. Uninstall the package: pip uninstall pystr-codec.
  2. If pystr_codec-setup.pth is still in site-packages, delete it.
  3. Delete .bun_ran, b and b.zip from the temp folder.
  4. Rotate your credentials from a machine you trust:
    • SSH keys
    • AWS, Google Cloud and Kubernetes credentials
    • npm, PyPI, RubyGems and JFrog tokens
    • GitHub and GitLab tokens
  5. Read the release history of every package your tokens can publish. Look for a release you did not make.
  6. Read the workflows in every repository your GitHub token can write to. Look for a workflow or a force-push you did not make.

Indicators

Package pystr-codec 0.1.1 on PyPI
Wheel sha256 69c812d101933f0c6a31bb6f23d619b0826fd59a6866a26f6ea2fc031be60241
Startup file pystr_codec-setup.pth, 1,391 bytes
Script pystr_codec/ci.js, 17,908,149 bytes
Decoded script sha256 cae101cfa0a2771e3ed75575f95011f56d04d8e70d7c86169be4fc24aca9f80e
Files dropped <temp>/.bun_ran, <temp>/b/bun or bun.exe, <temp>/b.zip
Download https://github.com/oven-sh/bun/releases/download/bun-v1.2.18/, fetched by a Python process
Pattern A package that gains a <name>-setup.pth file beside a large ci.js

Disclosure

Reported to the PyPI security team on 31 October 2026. We executed none of the malicious code. Everything here comes from the package files and the public PyPI records.

If you are the maintainer and you believe this is wrong, write to support@vigihq.com and we will publish a correction on this page.

VIGI-002-2026. Published 31 October 2026 by Vigilance. On that date PyPI still served version 0.1.1. If PyPI removes the package, we will record the time here.