Supply Chain Attacks in Open Source: What npm audit Can't Tell You

TL;DR: Software supply chain attacks exploit the trust we place in open-source dependencies. High-profile incidents like event-stream, ua-parser-js, and colors.js demonstrate that npm audit and automated scanners only catch known vulnerabilities — they're blind to malicious code injected by compromised maintainers, typosquatting, and dependency confusion. Real defense requires lockfiles, integrity checks, provenance verification with tools like Sigstore, and a healthy dose of skepticism about what you install.

Table of Contents


The Trust Problem Nobody Talks About

The average Node.js application has somewhere between 200 and 1,500 dependencies when you count the full transitive tree. Each one of those packages was written by someone you've never met, reviewed by a process you know nothing about, and published to a registry where anyone can upload code.

When you run npm install, you're executing arbitrary code from hundreds of strangers on your machine. The postinstall scripts alone can do anything your user account can do — read files, make network requests, install binaries. And most developers do this dozens of times a day without a second thought.

This isn't theoretical paranoia. The software supply chain has become one of the most actively exploited attack surfaces in the industry. The 2021 "State of the Software Supply Chain" report found a 650% year-over-year increase in supply chain attacks. And the incidents keep getting worse.


Real Incidents That Changed Everything

The event-stream Incident (2018)

This one was a wake-up call for the entire JavaScript ecosystem. event-stream was a popular npm package with about 2 million weekly downloads. Its original maintainer, Dominic Tarr, had moved on from the project and openly said he wasn't interested in maintaining it anymore.

A new contributor named "right9ctrl" offered to take over maintenance. Tarr handed over publishing rights. Over the next few months, right9ctrl:

  1. Published several normal, benign updates to build trust
  2. Added a new dependency called flatmap-stream
  3. flatmap-stream contained encrypted malicious code in its minified bundle
  4. The malicious code specifically targeted the Copay Bitcoin wallet application
  5. It stole wallet credentials and private keys, sending them to an external server

The attack was sophisticated — the malicious payload was encrypted and only decrypted when it detected it was running inside the Copay application. Automated scanners saw nothing wrong. The code passed npm audit. It looked like a normal dependency update.

The compromise was only discovered when a developer noticed the unusual dependency addition and dug into the minified source code.

ua-parser-js Hijack (2021)

ua-parser-js has over 7 million weekly downloads and is used by major companies including Facebook, Amazon, and Google. In October 2021, an attacker compromised the maintainer's npm account and published three malicious versions (0.7.29, 0.8.0, 1.0.0).

The compromised versions installed cryptominers and password-stealing trojans on Linux and Windows machines. The attack window was only about four hours, but given the download volume, thousands of CI/CD pipelines and developer machines were affected.

This wasn't a social engineering attack on the project — it was a direct account compromise. The maintainer's npm credentials were stolen.

colors.js and faker.js (2022)

This one was different because it wasn't an external attacker — it was the maintainer himself. Marak Squires, the creator of colors.js (20M+ weekly downloads) and faker.js (2.5M+ weekly downloads), deliberately sabotaged his own packages.

He pushed updates that introduced infinite loops printing "LIBERTY LIBERTY LIBERTY" along with ASCII art. His stated motivation was frustration with large corporations profiting from his free work without compensation.

colors.js v1.4.1 broke thousands of projects that depended on it, including Amazon's AWS CDK. This incident highlighted a different kind of supply chain risk: what happens when a trusted maintainer goes rogue?


Attack Vectors: How They Get In

Typosquatting

Attackers publish packages with names similar to popular packages, hoping developers make typos:

Legitimate Package Typosquat Examples
lodash lodashs, lodash-utils, 1odash
express expresss, express-framework, expres
react reactjs, react-dom-core, reakt
chalk chalkk, cha1k

In 2017, a developer published crossenv (no hyphen) as a malicious clone of the popular cross-env package. It stole environment variables — which often contain API keys and database credentials — and sent them to an external server. It received over 600 downloads before being caught.

Maintainer Account Compromise

If an attacker gains access to a maintainer's npm, PyPI, or RubyGems account, they can publish a new "patch" version that gets automatically pulled by anyone using version ranges like ^1.2.3 or ~1.2.3. Two-factor authentication adoption among npm maintainers is still surprisingly low.

Dependency Confusion

First documented by Alex Birsan in 2021, dependency confusion exploits how package managers resolve names. Many companies have internal private packages with names like company-auth or internal-utils. If an attacker publishes a package with the same name on the public npm registry with a higher version number, some package managers will prefer the public version.

Birsan used this technique to execute code inside the networks of Apple, Microsoft, PayPal, Tesla, and dozens of other companies. He earned over $130,000 in bug bounties.

Compromised Build Infrastructure

Attackers can also target the build pipeline itself. If they compromise a CI/CD system, they can inject malicious code during the build process. The SolarWinds attack (2020), while not an npm attack, demonstrated this vector devastatingly — attackers compromised SolarWinds' build system to inject a backdoor into the Orion software update, which was then distributed to 18,000 organizations including US government agencies.


What npm audit Actually Does (And Doesn't Do)

npm audit checks your dependency tree against the GitHub Advisory Database for known vulnerabilities. It's essentially a CVE lookup tool. Here's what it catches and what it misses:

npm audit CAN detect npm audit CANNOT detect
Known CVEs in published packages Zero-day malicious code
Outdated packages with security patches Typosquatted packages
Vulnerabilities with assigned advisories Compromised maintainer accounts
Packages that have been unpublished for security Dependency confusion attacks
Known vulnerable version ranges Obfuscated malicious payloads
Protestware / intentional sabotage
Malicious postinstall scripts

Running npm audit gives developers a false sense of security. The event-stream attack? npm audit showed no vulnerabilities. The ua-parser-js compromise? Clean audit until the advisory was published hours later. By that time, the damage was done.

This isn't a knock on npm audit — it does what it's designed to do. The problem is treating it as a comprehensive supply chain security tool when it's really just a vulnerability database lookup.


Building Real Defenses

Lockfiles: Your First Line of Defense

Lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) pin exact versions and include integrity hashes for every dependency. This means:

{
  "node_modules/lodash": {
    "version": "4.17.21",
    "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
    "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
  }
}

The integrity field is a Subresource Integrity (SRI) hash. If someone publishes a different tarball under the same version number, the hash won't match and installation will fail. Always commit your lockfile to version control. Always use npm ci (or yarn --frozen-lockfile) in CI/CD pipelines instead of npm install, which can modify the lockfile.

Subresource Integrity (SRI)

For client-side dependencies loaded from CDNs, SRI attributes verify that the file hasn't been tampered with:

<script
  src="https://cdn.example.com/[email protected]/dist/lib.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
  crossorigin="anonymous">
</script>

If the CDN is compromised and the file contents change, the browser will refuse to execute the script.

Sigstore: Cryptographic Provenance

Sigstore is an open-source project that brings code signing to the software supply chain without the pain of traditional PKI. It provides:

  • Cosign: Signs and verifies container images and other artifacts
  • Fulcio: A certificate authority that issues short-lived certificates tied to OIDC identities
  • Rekor: A tamper-evident transparency log

npm added support for Sigstore-based provenance attestations in 2023. When a package is published with provenance, you can verify:

npm audit signatures

# Output:
# audited 350 packages in 2s
# 350 packages have verified registry signatures
# 42 packages have verified attestations

This tells you that those 42 packages were built in a verifiable CI/CD pipeline and published directly from source code, not from some random developer's laptop.


The SLSA Framework: Supply Chain Levels for Software Artifacts

SLSA (pronounced "salsa") is a security framework by Google that defines four levels of supply chain integrity:

Level Requirements What It Prevents
SLSA 1 Build process is documented and produces provenance Unintentional tampering
SLSA 2 Build runs on a hosted service, provenance is authenticated Tampering after build
SLSA 3 Source and build platforms meet auditing standards Tampering during build
SLSA 4 Two-person review, hermetic builds, reproducible Insider threats

Most open-source projects today don't meet even SLSA 1. Getting the ecosystem to SLSA 3 would prevent the majority of supply chain attacks we've seen, but it requires significant infrastructure investment.


What Automated Tools Miss

Beyond npm audit, there's a growing ecosystem of supply chain security tools: Snyk, Socket.dev, Semgrep Supply Chain, Dependabot, Renovate. They're all valuable, but they share fundamental limitations:

Obfuscated code. Malicious code can be hidden in minified bundles, encoded strings, or behind layers of indirection. The event-stream payload was encrypted — no static analysis tool would have flagged it without knowing the decryption key.

Behavioral attacks. A package that exfiltrates environment variables via a DNS query during postinstall looks like normal network activity. Tools that focus on code patterns miss runtime behaviors.

The trust boundary problem. How do you distinguish between a package that legitimately needs filesystem access (like a build tool) and one that's stealing SSH keys? The permissions model in npm is effectively all-or-nothing.

Social engineering. No tool can detect that a maintainer's account was phished yesterday or that the friendly new contributor is actually building toward an attack. These are fundamentally human problems.


Practical Steps You Can Take Today

  1. Always commit lockfiles and use npm ci in CI/CD
  2. Enable npm provenance verification: npm config set verify-signatures true
  3. Audit new dependencies before installing: check the package's GitHub repo, download counts, maintainer history, and recent changes
  4. Use npm install --ignore-scripts and selectively enable postinstall scripts only for packages that need them
  5. Pin your dependencies to exact versions in production applications (not libraries)
  6. Set up a private registry (Artifactory, Verdaccio) and configure scoped packages to prevent dependency confusion
  7. Monitor for typosquatting of your own packages using tools like Socket.dev
  8. Review dependency diffs when updating. npm diff lets you see what changed between versions
  9. Minimize your dependency tree. Do you really need is-odd? Every dependency is an attack surface
  10. Enable 2FA on your npm account. If you maintain open-source packages, this is non-negotiable

The reality is that no single tool or practice makes you immune. Supply chain security is a layered defense, and it requires constant vigilance. But understanding the threat landscape — and the limitations of your tools — is the first step toward managing the risk intelligently.


References

  1. Birsan, Alex. "Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies." Medium, February 2021. https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610
  2. GitHub Advisory Database. https://github.com/advisories
  3. Sonatype. "2021 State of the Software Supply Chain Report." https://www.sonatype.com/resources/state-of-the-software-supply-chain-2021
  4. Sigstore Project. https://www.sigstore.dev/
  5. SLSA Framework. "Supply-chain Levels for Software Artifacts." https://slsa.dev/
  6. npm Documentation. "About Registry Signatures and Provenance." https://docs.npmjs.com/about-registry-signatures
  7. Socket.dev. "Detecting Supply Chain Attacks." https://socket.dev/
  8. Tarr, Dominic. "Statement on event-stream compromise." GitHub, November 2018. https://github.com/dominictarr/event-stream/issues/116
  9. Schneier, Bruce. "SolarWinds and the Holiday Bear Campaign." Schneier on Security, January 2021. https://www.schneier.com/blog/archives/2021/01/solarwinds-and-the-holiday-bear-campaign.html