Compromised Maintainers

Compromised Maintainer Accounts and Worm Propagation

The lab's "attacker" package carries a harmless stand-in for a stealer. It only reports what it could reach, with exactly a real one's access: your user, your directory, your environment.

steal.js: the postinstall script of the lab's malicious packageJavaScript
// A harmless stand-in for a credential stealer: it only reports what it could reach.
const fs = require("fs"), os = require("os"), path = require("path");
const npmrc = path.join(os.homedir(), ".npmrc");
const secrets = Object.keys(process.env).filter((k) => /TOKEN|SECRET|KEY/.test(k));
console.log(`[1.99.0 postinstall] user=${os.userInfo().username} cwd=${process.cwd()}`);
console.log(`[1.99.0 postinstall] ~/.npmrc readable: ${fs.existsSync(npmrc)}; ` +
  `secret-like env vars: ${secrets.length}`);

It found the NPM_TOKEN variable the install command set, as a CI job sets one to publish. Real payloads such as Shai-Hulud and ChainDrop also read ~/.npmrc, ~/.aws, ~/.kube/config and GitHub 29 tokens and query cloud metadata endpoints. What makes them worms is the next step. With a stolen npm 2,036 token the payload lists every package that token may publish, downloads each one's latest tarball, adds itself as a preinstall or postinstall hook, bumps the patch version and publishes it. Everyone whose ^ range accepts the new patch installs the payload on their next install, and any tokens on their machines continue the chain. ChainDrop also pushed branches and GitHub Actions 29 workflows with stolen GitHub tokens, so it spread through source repositories as well as the registry.

Three facts make the loop fast: ranges accept new patches automatically, install scripts run before anyone reviews the code, and long-lived publishing tokens sit in CI secrets and on laptops. The next two subsections break each link: 2FA-gated publishing protects the token, release-age delays slow the upgrade, and blocked install scripts stop the payload.