Supply-Chain Attacks: Auditing Your Dependencies
Photo: Unsplash
Run npm install on a fresh project and watch a thousand packages land in node_modules. You wrote almost none of them. You probably can't name a tenth of their authors. And every single one of them runs with the same privileges as your own code. Modern applications are mostly other people's software, glued together by a manifest file — which means your security perimeter now includes thousands of strangers.
That's the uncomfortable premise of supply-chain security. An attacker doesn't need to find a bug in your code if they can get malicious code into a package you depend on. It's a force multiplier: compromise one popular library and you reach everyone who installs it. This is why supply-chain risk earned its own slot in the OWASP Top 10 as "Vulnerable and Outdated Components," and why auditing dependencies is no longer optional.
How the Attacks Actually Work
The attacks aren't usually sophisticated zero-days. They exploit trust and automation. A few recurring patterns:
- Typosquatting. A malicious package named one keystroke away from a popular one —
reqeusts,lodahs— waiting for a typo in someone's install command. - Dependency confusion. An attacker publishes a public package with the same name as your private internal one and a higher version number, and your build tool helpfully grabs the public, malicious one.
- Account / maintainer takeover. A legitimate, widely-used package gets a malicious update because a maintainer's credentials were phished, or because they handed the project to a "helpful" new contributor who turned out to be hostile.
- Build-time vs. runtime payloads. Malicious code often hides in install scripts that run automatically during
npm install, before your app code ever executes.
# Install scripts run automatically and with your privileges.
# This is a common payload-delivery point for malicious packages.
npm install --ignore-scripts # skip lifecycle scripts when you can
# Audit what would actually run on install for a package
npm view some-package scriptsA dependency runs with the full privileges of your application — it can read your environment variables, your filesystem, and your network. There is no sandbox between "code you wrote" and "code you installed." Treat adding a dependency as granting it that access, because that's exactly what you're doing.
Step One: Know What You Actually Ship
You can't audit what you can't see. The transitive dependency tree is where the risk hides — the package you chose pulled in forty you didn't. Start by making the full set visible and pinned.
# See the real tree, not just your direct deps
npm ls --all
# Lockfiles pin exact versions AND their integrity hashes.
# Commit them. Build from them with a clean, reproducible install.
npm ci # installs strictly from package-lock.json, fails on driftThe lockfile is your first real control. It pins exact versions and records integrity hashes, so a dependency can't silently shift under you between installs. npm ci (as opposed to npm install) builds strictly from the lockfile and fails if the manifest and lock disagree — that determinism is what makes your builds auditable.
Step Two: Scan for Known Vulnerabilities
Most supply-chain incidents you'll face aren't novel attacks — they're known-vulnerable versions you simply haven't updated. Automated scanning catches these cheaply.
# Built-in advisory scanning
npm audit # report known vulns in your tree
npm audit --omit=dev # focus on what ships to production
npm audit fix # apply non-breaking patched versionsnpm audit cross-references your tree against advisory databases. It's noisy and it over-reports dev-only and unreachable issues, so don't treat every line as a fire — but a high-severity advisory on a production, network-reachable dependency deserves same-day attention. The OWASP Vulnerable Dependency Management Cheat Sheet lays out how to triage these by exploitability rather than chasing the count to zero.
Step Three: Vet Before You Add
The cheapest dependency to audit is the one you never install. Before adding a package, spend two minutes on due diligence: Is it actively maintained? How many maintainers, and is the project a one-person hobby that could be abandoned or handed off? How much transitive weight does it drag in? Could you write the twenty lines yourself instead of taking on a tree of forty packages?
# Quick health signals before adding a dependency
npm view some-package # maintainers, last publish, dependencies
# Then ask: do I need this, or can I inline a small helper?This is judgment, not a tool. A left-pad-sized utility is rarely worth a new supply-chain entry. A mature, multi-maintainer cryptography library almost certainly is — you should not be writing that yourself. The skill is knowing which is which.
Step Four: Automate the Boring Parts
Manual audits don't scale and don't happen consistently, so wire the checks into the pipeline where they run on every change.
# In CI: fail the build on high-severity production vulnerabilities
npm audit --omit=dev --audit-level=high- Run audits in CI so a newly-disclosed vulnerability in an existing dependency surfaces on the next build, not in an incident.
- Use automated update PRs (Dependabot, Renovate) so patches arrive as small, reviewable changes instead of a terrifying once-a-year bulk upgrade.
- Generate an SBOM. A Software Bill of Materials is an inventory of everything you ship. When the next widely-publicized vulnerability drops, an SBOM answers "are we affected?" in seconds instead of a frantic afternoon of grepping. PortSwigger's web security material is a good companion for understanding how a compromised component becomes a real exploit path in a running app.
The Balance to Strike
Don't overcorrect into paralysis. The goal isn't zero dependencies — reinventing well-audited libraries is its own security risk, since your hand-rolled crypto or parser will likely be worse than the maintained one. The goal is intentional dependencies: a tree you can see, versions you pin, vulnerabilities you scan for, updates you apply steadily, and a default skepticism toward each new entry. You're not trying to trust nobody. You're trying to know who you're trusting and to notice quickly when that trust is misplaced.
Takeaways
- Your application is mostly code you didn't write, and every dependency runs with your app's full privileges — there's no sandbox between your code and theirs.
- Supply-chain attacks exploit trust and automation: typosquatting, dependency confusion, maintainer takeover, and malicious install scripts.
- Commit lockfiles and build with
npm cifor deterministic, auditable installs; consider--ignore-scriptsto avoid unvetted install-time code. - Scan with
npm auditbut triage by exploitability and production reachability, not by raw count. - Vet packages before adding them — maintenance, maintainer count, and transitive weight — and prefer inlining trivial utilities over inheriting a tree.
- Automate audits in CI, use update bots for steady small upgrades, and keep an SBOM so you can answer "are we affected?" instantly.

