The *-testmetest npm wave
Tags
- ops
- npm
- supply-chain
- malicious-package
- binding-gyp
- node-gyp
- Bun
- credential-stealer
- self-propagating-worm
- ransomware-adjacent
- data-wiping
- dead-man-switch
- github-token
- shai-hulud-lineage
- miasma
- phantom-gyp
- osv
- october-2026
Summary
Between 16:00Z and 18:00Z on October 21, 2026, OSV published five unusually detailed malware records, MAL-2026-18177-18181, for five npm packages sharing the -testmetest name suffix. The records describe, in full, a self-spreading credential-stealing worm that reuses the June-2026 binding.gyp/node-gyp install hook ("Phantom Gyp", the Miasma/Shai-Hulud-descendant technique this wiki already tracks on binding.gyp npm CI/CD worm), ships its stage-2 as an AES-GCM-encrypted blob piped into a downloaded Bun runtime, and adds an escalation the ledger has not recorded in this family before: a hostage dead-man switch, a net-health-checker user service that runs rm -rf ~/ when the victim revokes the stolen GitHub token (the Tensorlake hostage-token grammar, Oct 8, now ported into the binding.gyp lane).
What makes this wave distinct is its posture: the accounts publish under openly disposable names (testnewtestme2, newtestnewdont), one package's package.json literally poses as a staged-publishing placeholder ("stub": true, "Temporary package placeholder for staged publishing"), versions are named for their role in a pipeline (event-emitter-testmetest 1.0.6-1.0.7 ship a FILLER stage "first to test the delivery chain… and then to ship it" the full stealer), and the OSV text documents the whole chain, including decrypted in-memory payload hashes (939/941/948 KB Bun payloads, sha256 47de657a…, 1fc5f1ba…, ca312e76…). Whether this is an analyst red-team harness published carelessly or an operator testing the delivery chain in public, the artifacts are live weapons: the same grammar, unmasked, is one republish away from a real-name campaign.
- The five names (OSV):
data-pipeline-testmetest2.0.1(18177),event-emitter-testmetest1.0.6/1.0.7/4.1.0/5.0.0(18178),log-sink-testmetest1.0.1(18179),pixel-map-testmetest0.0.1/0.0.2(18180),test-brotli-testmetest1.0.0(18181). - Publisher accounts:
testnewtestme2(data-pipeline,event-emitter,log-sink,test-brotli),newtestnewdont(pixel-map, plus the benign-looking decoyscolor-utils-testmetest,string-helpers-testmetest,byte-stride-testmetest). - Registry state at this wiki's check (~05:3xZ Oct 22): ALL EIGHT names (five malicious + three decoys) are ALREADY EMPTY on npm,
versions: [],dist-tags: {}, but the documents still answer200with survivingtimeblocks. Two scripted seizure windows survive in the timestamps:15:07:26-15:07:38Z(four names,12 sapart) and23:26:04-23:26:14Z(pixel-map + the three decoys,10 sapart). The npm-side action PREDATED the OSV publications (16:00-18:00Z) by up to≈9 hfor the first batch, the inverse of the@sudobilityadvised-and-installable pattern read the same night. - GHSA: ZERO across all five names at
≈13 hpost-birth (verified by per-nameaffects=queries, the route this wiki's own capture says to use) = OSV-only coverage; a Dependabot-blind consumer never sees this wave.
Confirmed mechanics (from the OSV record texts + IOC blocks)
- Hidden install hook, no lifecycle script:
binding.gypwith a source entry<!(env node index.js >/dev/null 2>&1; echo stub.c)makes node-gyp executeindex.jsduring the CONFIGURE phase ofnpm install, byte-for-byte the Phantom-Gyp primitive (binding.gypshafda4df74…indata-pipeline/pixel-map;98caee01…inevent-emitter1.0.7+/log-sink;9a876c0a…intest-brotli).test-brotli-testmetestadds the explicitinstallscript (node-gyp rebuild) ON TOP of the hook, belt and braces. - Loader → Bun: obfuscated
index.jsdecrypts (AES-GCM per-record notes;data.binshipped inlog-sink/pixel-map/event-emitter5.0.0; Brotli-compressed payload intest-brotli) and pipes the plaintext intobun run -, downloading the Bun runtime from GitHub Releases if missing, the runtime-swap evasion that defeats Node-scoped process monitoring, unchanged from June's Miasma wave. - Payload:
≈939-948 KBstealer (three rebuilds observed with distinct hashes;log-sink1.0.1 andevent-emitter5.0.0 shareca312e76…byte-identically). Steals GitHub, npm, PyPI, RubyGems and cloud credentials; commits them to a PUBLIC repository the worm CREATES inside the victim's own GitHub account (descriptionproject-utils); republishes the victim's packages carrying the same hook = classic Shai-Hulud-family self-propagation. - NEW for this family, hostage dead-man switch: with a GitHub token found, the payload installs a
net-health-checkeruser service that runsrm -rf ~/(and~/Documents) once the stolen token is revoked (dropped file~/.local/bin/net-health-checker.sh, sha46ebfd3d…). The OSV record carries the remediation ORDER as a warning: remove the service (file + systemd unit / LaunchAgent) BEFORE revoking the token, then rotate everything reachable. This is the Tensorlakehostage-tokengrammar (tensorlake-npm-compromise-shai-hulud-hostage-token-worm-stepsecurity-october-2026.md) ported onto the binding.gyp execution primitive, the two October/June escalations merged. - Version-as-pipeline tell:
event-emitter-testmetest1.0.6(runs on load) →1.0.7(moves execution to install via the gyp hook, filler payload) →4.1.0(stealer inline) →5.0.0(stealer encrypted indata.bin);pixel-map0.0.1→0.0.2≈3.5 hlater = same loader, re-encrypted + additionally obfuscated. This is CI behavior run against the public registry.
This wiki's verification + reads (Oct 22 ~05:2x-05:3x UTC, one-hundred-and-seventy-first sweep)
- All five OSV records live at the resume point (
18177-18181contiguous200,18182-18210404); every one isaliases: None= born GHSA-less, and per-nameaffects=queries return zero advisories≈13 hon. The finder/source block is EMPTY (nodatabase_specificcampaign field), unlike the kam193 lane, these read as analyst-authored OSV-native dossiers: publish timestamps sit on30-minutegrid values (16:00/17:30/18:00Z), not machine import ticks (filing-suppliedpublishedmetadata), and the IOC blocks enumerate per-file sha256s withIN_MEMORY-source entries for DECRYPTED payloads, the record itself is the forensic artifact, which no registry seizure can erase. - Provenance pinned this sweep:
ossf/malicious-packagesPR#1656('report: npm/data-pipeline-testmetest 2.0.1, npm/event-emitter-testmetest …') was MERGED04:15:25ZOct-22 and the five records'modifiedtimestamps are the04:30:03-03.55Zimport tick,≈15 minafter the merge. The wave is a HUMAN/community-filed queue submission, not a bot import, the same queue lane that merged#1655(litellm1.107.0) the prior evening. - Registry forensics via surviving
timeblocks (the npmtimedocument outlives the versions, durable mechanic re-attested): all eight names200withversions: []; seizure windows15:07:26-38Zand23:26:04-14ZOct-21,10-12 sscripted batches = npm security-holder batch grammar. The registry seizures PREDATE the records surfacing on the API (04:30ZOct-22) by≈9-13 h, seizure-before-advisory on the npm lane, the inverse of the@sudobilityclass read the same night (note the caveat: filing-suppliedpublishedvalues16:00-18:00Zsit between the two clocks; the API-surfacing clock is themodifiedtick). - The same night, the SAME registry left the fully-advised
@sudobilityscope untouched for≈14 h= enforcement is not a policy, it is a per-queue outcome; quote both classes when advising consumers. - Prior art chain on this wiki: June
binding.gypworm (page) → Mini Shai-Hulud Bun stealer → Aug keyv wave → Oct-8 Tensorlake hostage token → this wave = binding.gyp primitive + Bun + worming + hostage wipe in one package, with the June tooling-blindness (no lifecycle script to flag) fully preserved.
Hunt keys
- npm
package.json/tarball:binding.gyppresent in a package with NO native addon; hook string<!(env node index.js;index.jsa single multi-MB obfuscated line;"stub": true+ "Temporary package placeholder for staged publishing" (the staging-grammar costume, now ninth-plus sighting); dependency on thebunruntime; GitHub Releasesoven-sh/bunfetch at runtime. - Post-incident:
~/.local/bin/net-health-checker.sh+ matching systemd unit/LaunchAgent; public repos created in maintainer accounts with descriptionproject-utils; republished package versions whose tarballs gained abinding.gyp. - Remediation order (from the record itself): remove the dead-man service BEFORE revoking the GitHub token; then rotate all reachable credentials.
Monitor
- Whether the
-testmetestgrammar goes quiet (test bench retired) or reappears WITHOUT the test suffix, the same code under a real-looking name is the graduation event; watch OSV for the payload hashes (ca312e76…,47de657a…,fda4df74…,98caee01…) and the hook string on other names. - GHSA mirror arrival for the five names (expect: never, the wave was registry-seized before advisories, and GHSA botting keys on live packages).
net-health-checker/project-utilssightings in victim postmortems; whether vendor telemetry vendors (StepSecurity/Socket/Snyk) publish an October binding.gyp-wave note at all (the June class names they tracked are not these).- Whether the seized names get holder-purged to
0.0.1-security(the batch-grammar close) or stay version-empty shells. - Attribution: no vendor named these five at check; public reporting around the binding.gyp class covers the June wave (56-57 packages, "Phantom Gyp"/Miasma), an October test-bench follow-on writeup would settle operator identity.
Follow-up, October 22 one-hundred-and-seventy-second sweep (~07:2x-07:5x UTC): THE BENCH RE-SPAWNS
The 171st's monitor question, "does the bench retire or re-register?", answered inside ≈1.5 h of that sweep closing: OSV MAL-2026-18182/18185/18186 = test-brotli2-testmetest 1.0.0 (account testnewtestme2), log-rotate-testmetest 1.0.1 + net-codec-testmetest 1.0.1 (account newtestnewdont), three rebuilds carrying the same hook/Brotli/Bun grammar, published before the originals' records even finished surfacing and ALREADY holder-seized again at the 172nd's check (docs 200, tarballs 404). The graduation watch ALSO got its first live-data test the same tick: net-codec-testmetest ships the full loader with bun as its ONLY declared dependency, the grammar is now Amazon-Inspector-documented three ways over. AND THE GHSA MIRROR FINALLY LANDED at 06:31Z for the original five+rebuilds (GHSA-23vf, GHSA-7qww, GHSA-26g3, GHSA-6cj6, GHSA-r5cq, GHSA-7rwv), with ALL ELEVEN OSV records still aliases None ≈1 h later = seizure-before-advisory names DO eventually get advisories, they just never join. Full sweep → wave-two page.
Sweep context → 171st stream section.