jason's blogObservations on web standards, browsers, and frontend development

Category: Security

S1ngularity: the Nx package supply-chain attack

A review of the Nx S1ngularity attack and the protections frontend teams can actually apply.

Published 08/26/2025

I run npm install so routinely that I almost never stop to think, 'A collection of strangers' programs is about to run on this machine.' Familiarity has worn down the alarm. What bothered me about S1ngularity was not only that Nx was compromised, but the realization that much of my trust in the supply chain comes down to everyone installing packages this way.

In August 2025, malicious code was published in several versions of Nx packages. A developer performing a normal install could expose credentials and environment data from the machine.

This is a supply-chain attack. Instead of compromising every user separately, an attacker controls a trusted upstream package and lets the malicious code travel through an ordinary update.

A lockfile prevents versions from changing unnoticed, but it cannot fully protect a team that deliberately updates to a malicious version. Practical defenses include narrowing CI token permissions, avoiding long-lived credentials, reviewing important updates, and using tools that detect malicious packages.

Why can a package cause harm before it is imported?

npm packages can execute lifecycle scripts such as postinstall. Legitimate packages use them to download browsers or compile native modules; a malicious release can use the same hook to read environment variables, config files, and local credentials. The application may be compromised during installation before it imports the package once.

Monorepo tooling also has a wide trust boundary. It reads the whole workspace, runs tasks, and commonly operates in CI. That makes Nx a valuable target. This is not because Nx is uniquely insecure; the closer a tool is to the core development workflow, the more it can reach if compromised.

Unexciting practices that help

  • Give CI short-lived, least-privilege credentials instead of production secrets in every job.

  • Commit the lockfile and use a frozen lockfile in CI so dependency resolution cannot drift.

  • Pause before adopting an unexpected release or a new major version and check maintainer announcements.

  • Enable phishing-resistant 2FA for development and package-publishing accounts.

No single measure blocks every supply-chain attack. The useful strategy is a series of small gates, so one compromised dependency cannot immediately inherit the entire company's access.

What a lockfile can and cannot do

A lockfile records exact versions and sources, keeping installs consistent across environments. npm ci or pnpm's frozen lockfile also stops CI from resolving a new dependency tree. This prevents yesterday's install from silently differing from today's, but it does not help when a malicious release is intentionally committed into the lockfile.

Dependency changes still need review. Nobody can manually read thousands of packages, but a changed maintainer, unusual release cadence, new install script, or unfamiliar dependency should raise the risk level. Automated update tools can open pull requests without automatically merging every green build.

Frontend CI often holds npm tokens, cloud keys, and deployment credentials together. Even when malicious code executes, the damage is far smaller if the job cannot access production secrets. Supply-chain security is ultimately about separating privileges, not simply choosing a supposedly safer package manager.

After an incident, the hardest question is often which machine or job installed the affected version during the relevant window. I want CI to retain a dependency tree and build provenance, while local investigations should at least have the lockfile and package-manager cache. Without those records, the safest assumption is that every credential was exposed, forcing an expensive complete rotation.

I also would not make humans approve every line of every dependency update; that quickly becomes a rubber stamp. Risk tiers work better. A patch to a type-only package can remain highly automated, while a new install script, maintainer change, or release-tool update requires stronger evidence.