tech, developers, and the code underneath

issue 067· news·

The Nx compromise and the credential-stealing postinstall

A popular build tool's npm packages ship a payload that harvests tokens and pushes them to public repos.

Malicious versions of the nx build tool and several related packages were published to npm with a postinstall script that scanned the developer's machine for credentials and published them to a public GitHub repository created under the victim's own account.

the payload#

The script searched for:

  • npm and GitHub tokens
  • SSH private keys
  • Cloud provider credentials in the usual locations
  • Environment variables matching credential-shaped patterns
  • Cryptocurrency wallet files

It then created a public repository in the victim's GitHub account named with a recognizable prefix and pushed the harvested data to it.

Some variants additionally invoked locally-installed AI coding CLI tools with a prompt asking them to enumerate sensitive files — using the developer's own agent as a discovery mechanism. That detail is new and it is going to be studied.

the entry point#

Compromised publishing credentials. The specifics of how they were obtained matter less than the pattern: a maintainer's token, or a CI workflow with publishing rights, was reachable by an attacker.

This is the dominant supply chain attack shape. Not typosquatting, not dependency confusion, not a malicious contribution. Take over the account of a legitimate maintainer of a package people already depend on.

the controls that would have stopped it#

In order of effectiveness:

1. --ignore-scripts. The payload was in postinstall. An install that does not run lifecycle scripts does not run the payload.

bash
npm ci --ignore-scripts

Set it in .npmrc for your CI and see what breaks. For most projects the answer is: nothing, or one package that needs a native build, which you allowlist.

2. Trusted publishing. OIDC-based publishing from a verified CI workflow rather than a long-lived token. A token that does not exist cannot be stolen. npm supports this now and adoption is the bottleneck.

3. Install cooldown. The malicious versions were live for hours before removal. A policy of not adopting a version until it is 24 to 72 hours old would have avoided this entirely, and avoids most incidents of this shape, because the window between publish and detection is short.

json
{ "minimumReleaseAge": 4320 }

4. Short-lived credentials. The payload harvested long-lived tokens. If your CI uses OIDC federation to get a fifteen-minute cloud credential, there is nothing durable to steal.

if you were affected#

The order matters:

  1. Rotate everything the machine had access to. npm tokens, GitHub PATs, SSH keys, cloud credentials. Assume everything readable was read.
  2. Check for the public repository in your GitHub account and delete it — but capture its contents first for your incident record.
  3. Audit for persistence. Check ~/.bashrc, ~/.zshrc, ~/.profile, shell history, cron, launch agents, and git hooks in your repositories.
  4. Review recent activity on every account whose credentials were on that machine.

Rotation before cleanup. Cleanup on a compromised machine while live credentials are still valid is how you spend an afternoon on remediation and the attacker spends it on access.

the trend#

This is one of several npm incidents this year and they are getting more sophisticated: better targeting, cleaner payloads, and now the use of local AI tooling as an attack primitive.

The ecosystem's structural weakness has not changed. A single maintainer's credentials protect a package that millions of machines install and execute automatically. Every control above mitigates; none of them fix that.

The fix requires the registry to make unattended publishing hard by default, and that is a change to how a lot of people work. It is going to happen anyway, because the alternative is this every few weeks.

Dom, August 29, 2025

get README in your inbox

One dispatch, no noise. Tech and developer news, plus the occasional long piece on the craft.

subscribe →