Most security thinking focuses on the perimeter: your servers, your APIs, your authentication. But attackers have learned that the easiest way into a hardened target is through the software it trusts — not the front door, but the loading dock.
That’s a supply chain attack.
What is a software supply chain attack?
A software supply chain attack happens when an attacker compromises software before it reaches you — by injecting malicious code into a dependency, a build tool, a CI runner, or a software update mechanism.
The key characteristic is trust. You didn’t write the malicious code. Your security review never saw it. It arrived pre-signed, pre-tested, and pre-trusted — bundled inside something you deliberately chose to use.
This is what makes supply chain attacks so effective: the attack surface is enormous and largely invisible. A typical application has hundreds of transitive dependencies. You control maybe a handful of them. The rest are maintained by people you’ve never met, hosted on infrastructure you don’t own, distributed through registries that are difficult to audit at scale.
How supply chain attacks work
There’s no single attack vector. The common thread is that the attacker inserts themselves somewhere in the chain between “code being written” and “code running in production.”
Compromising an upstream maintainer
The attacker gains access to a legitimate package maintainer’s account — through phishing, credential theft, or social engineering — and publishes a malicious version of a package that already has millions of downloads.
The event-stream incident (2018) is the canonical example. A popular npm package’s maintainer transferred ownership to a new contributor, who published a version that contained code designed to steal bitcoin wallet credentials from a specific application. The package was downloaded 8 million times per week.
Typosquatting and dependency confusion
Attackers register package names that are close to popular packages (lodahs instead of lodash, reqeusts instead of requests) or exploit how package managers resolve internal vs. public packages. When a developer or CI system makes a typo — or when a public package name shadows an internal one — the malicious package is installed instead.
Dependency confusion attacks (first documented by Alex Birsan in 2021) affected Apple, Microsoft, PayPal, and dozens of other companies. Birsan published packages to public registries with the same names as internal packages at those companies. Many of their build systems automatically preferred the public version.
Compromising a maintainer account
Rather than publishing a new package, attackers steal credentials from an existing trusted maintainer and publish a malicious version under that identity. The package already has millions of users. npm has no way to distinguish a malicious publish from a legitimate one.
In March 2026, the lead maintainer of axios had their machine compromised via a targeted social engineering campaign. The attacker used the stolen npm credentials to publish axios@1.14.1 and axios@0.30.4, both of which pulled in a dependency containing a Remote Access Trojan for macOS, Windows, and Linux. The versions were live for approximately three hours before a collaborator flagged the anomaly and npm removed them. One downstream casualty: OpenAI’s macOS app signing pipeline pulled the compromised version and had to rotate its code-signing certificate.
Sustained, multi-target campaigns
Individual account compromises are one thing. The Mini Shai-Hulud campaign — attributed to North Korean threat actors — demonstrated something more alarming: a systematic, credential-driven operation targeting npm maintainers at scale across months.
In May 2026, the campaign hit TanStack: 84 malicious package artifacts were published across 42 packages, including @tanstack/react-router with 12 million weekly downloads. The vector was a GitHub Actions “Pwn Request” cache poisoning attack combined with OIDC token extraction from the runner. In the same weeks, the campaign compromised the @antv ecosystem (639 malicious versions across 323 packages), Intercom’s npm client, Bitwarden CLI, SAP CAP, and several others. npm responded by invalidating all granular access tokens that bypass 2FA.
Why defenses lag behind the threat
Most application security tooling focuses on what you wrote. Static analysis, fuzzing, and penetration testing assume you control the code under test. Supply chain attacks bypass all of that.
There are a few structural reasons this problem is hard:
The dependency graph is enormous. A small Node.js app can have 1,000+ transitive dependencies. Auditing each maintainer’s identity, infrastructure, and commit history is not feasible.
Package registries have weak identity controls. npm and PyPI accounts can be created without verification. Two-factor authentication is not universally enforced. Ownership transfers are opaque.
Trust accumulates silently. Once a package is pinned in a lockfile, it typically stays there. Most teams apply updates reactively (when there’s a known CVE) rather than proactively reviewing what changed.
Build pipelines are complex and under-audited. The systems that turn source code into deployable artifacts often have broad permissions and little logging. They’re infrastructure, not product — and they’re treated accordingly.
What you can actually do
None of these defenses eliminates supply chain risk. The goal is to make an attack expensive enough that it’s not worth the effort, and to detect it quickly if it happens anyway.
Pin your dependencies
Use lockfiles. This is table stakes — package-lock.json, go.sum, Cargo.lock, poetry.lock. Pin the exact version and hash of every dependency, direct and transitive.
Pinning doesn’t prevent a malicious version from being published, but it prevents your CI from automatically picking it up. Upgrades become explicit, reviewable decisions rather than silent behavior.
Verify integrity
Modern package managers support content-addressable hashing. Verify that what you installed matches what was published. Tools like npm’s --audit-signatures flag and Sigstore’s cosign bring code signing to open source packages.
A signed artifact with a verifiable provenance chain — “this binary was produced from this commit by this CI system” — is significantly harder to tamper with than an unsigned tarball on a CDN.
Generate and monitor SBOMs
An SBOM is your inventory. Without it, you don’t know what’s running. With it, you can query: “is anything in this repo affected by CVE-2024-XXXX?” and get an answer in seconds rather than hours.
Generate SBOMs in CI on every commit. Scan them against OSV, NVD, and GitHub Security Advisories automatically. Subscribe to vulnerability feeds for the packages you depend on — not just your direct dependencies.
boring.tools does this automatically: connect a repository, and every commit generates an SBOM, scans it, and surfaces new vulnerabilities as they’re disclosed — including for transitive dependencies you didn’t know you had.
Reduce your dependency surface
Every dependency is a potential attack vector. Before adding a package, ask whether it’s actually necessary. Many npm packages do trivial things (is-odd, left-pad) that belong in your own codebase. Fewer dependencies means a smaller graph to audit.
For existing dependencies, review what they actually do. A package that renders markdown doesn’t need filesystem access. A logging library doesn’t need to make network calls. If a dependency requests capabilities that don’t match its purpose, that’s a signal.
Use private registries and mirrors
Proxy public registries through a private one (Verdaccio, Artifactory, AWS CodeArtifact). This gives you:
- A single place to enforce package allowlists
- Caching that prevents upstream availability issues
- A chokepoint to scan packages before they reach your builds
- Audit logs of what was installed when
Dependency confusion attacks in particular are much harder to execute when your build system resolves all packages through an internal registry.
Enable MFA on publishing accounts
If your organization publishes packages, enforce multi-factor authentication on every account with publish access. This is the first line of defense against account takeover attacks.
npm and PyPI both support TOTP-based 2FA and hardware keys. GitHub’s npm integration supports verified publishing tied to GitHub Actions OIDC tokens, so a release can be tied to a verifiable workflow run rather than a single human’s credentials.
Watch for anomalous behavior
At runtime, a compromised dependency behaves differently from a legitimate one. It might make unexpected outbound connections, read files outside its scope, or spawn subprocesses. Behavioral monitoring in production — network egress filtering, eBPF-based process monitoring, container security policies — provides a last line of defense when static analysis fails.
The honest answer
Software supply chain security doesn’t have a final state you can reach. You’re managing risk across a system you don’t fully control, against adversaries who are patient and technically sophisticated.
What you can do is make your software’s composition explicit and auditable, respond quickly when something changes, and reduce the blast radius when something does go wrong.
SBOMs are the foundation. Not because they prevent attacks — they don’t — but because you can’t defend what you can’t see.
boring.tools generates CycloneDX and SPDX SBOMs from your repositories, scans them for vulnerabilities on every commit, and keeps a versioned history so you always know what was in any release. See how it works →