A typical Node.js application has between 500 and 2,000 dependencies when you count the full dependency tree. Each one of those packages is code that runs in your production environment with the same permissions as your own code. If any one of them is compromised, your application is compromised.
Software supply chain attacks are not theoretical. The event-stream incident in 2018 affected millions of downloads. The ua-parser-js hijack in 2021 injected crypto miners and credential stealers into a package with 7 million weekly downloads. The colors and faker sabotage in 2022 broke thousands of applications. These are not edge cases. They are the new normal, and reviewing what you actually pull in is now a standard part of an application security review.
Understanding your dependency surface
Run npm ls --all in your project and look at the output. Most teams are shocked by how many packages they depend on. A single import of Express brings in 30 dependencies. Adding a test framework adds another 100. Your project might have 50 direct dependencies but 1,500 transitive ones.
The first step is understanding what you actually depend on. Use npm audit to identify known vulnerabilities. Use tools like Socket.dev or Snyk to monitor for suspicious package behavior. Set up automated alerts so you know when a dependency publishes a new version with unexpected changes.
Lock your dependencies
Always commit your package-lock.json file. This ensures that every developer and every CI build uses exactly the same versions of every package. Without a lockfile, running npm install on two different machines can produce different dependency trees, which makes debugging incredibly difficult and creates security inconsistencies.
Consider using npm ci instead of npm install in CI environments. It installs exactly what is in the lockfile without modifying it. If there is a mismatch between package.json and package-lock.json, it fails rather than silently resolving the difference.
Pin versions and review updates
Do not use caret (^) or tilde (~) ranges for dependencies that handle sensitive operations like authentication, encryption, or payment processing. Pin these to exact versions and review every update manually. A compromised patch release to your authentication library is an immediate production compromise.
Use Renovate or Dependabot to automate dependency update pull requests. Configure them to separate critical security updates (which should be merged quickly) from routine version bumps (which can be batched and reviewed weekly).
Verify package integrity
npm supports package integrity checking through SHA-512 hashes stored in the lockfile. Make sure your CI pipeline verifies these hashes. If a package has been modified after publication (which should not happen but has happened), the hash check will catch it.
Consider setting up a private npm registry using Artifactory, Verdaccio, or GitHub Packages. A private registry acts as a proxy that caches packages locally. If a package is yanked or compromised on the public registry, your builds continue to work with the cached version while you assess the situation.
Restrict install scripts
npm packages can run arbitrary scripts during installation via the preinstall, install, and postinstall hooks. These scripts run with the permissions of the user executing npm install. A malicious package can use install scripts to exfiltrate environment variables, install backdoors, or modify other packages.
Use --ignore-scripts for packages that do not need install scripts, or use a tool like Socket to detect packages with suspicious install scripts before adding them to your project. Review the scripts of any new dependency before adding it.
Need help with application security?
traztech helps startups audit their dependency chains, implement supply chain security controls, and build secure development pipelines.
Book a free strategy callThe attack patterns that actually land
Most teams picture supply chain compromise as a maintainer going rogue. In the incidents we have worked, the entry point is usually duller than that. Maintainer account takeover is the most common: a package author reuses a password, has no MFA on their npm account, and an attacker publishes a patch release under their name. The package looks normal on the registry page because the tarball contains the real code plus twenty lines appended to a build file.
Dependency confusion is the second pattern and it hits companies that run an internal registry badly. If your build resolves @acme/billing-client against a proxy that falls back to the public registry, anyone can publish a package with that name publicly at version 99.0.0 and win the resolution. The fix is to scope every internal package and configure the scope to resolve only against your private registry, then verify that the fallback is actually disabled rather than assuming your Artifactory defaults are safe. Test it by publishing a harmless decoy internally and watching what your CI resolves.
Typosquatting and starjacking target the moment a developer adds a dependency. A package named one character away from a popular library, with a README copied verbatim and a repository field pointing at the legitimate GitHub project, looks entirely credible in a pull request diff. The reviewer sees a plausible name and approves. This is why dependency additions deserve a different review standard than dependency upgrades, and why a policy of "one new direct dependency per PR, named in the PR title" catches more than any scanner.
Expired infrastructure is the pattern nobody plans for. A maintainer lets the domain behind their npm account email lapse, someone re-registers it, does a password reset, and now owns publish rights to a package with millions of weekly downloads. There is nothing you can do about this upstream. What you can do is make sure a bad version cannot reach production without a human seeing it.
Your CI pipeline is the real blast radius
A malicious postinstall script running on a developer laptop is bad. The same script running in CI is usually worse, because CI holds the credentials that matter: cloud deployment roles, registry publish tokens, signing keys, and often a GitHub token with write access to the repository. If you only harden one place, harden the build.
Practical controls that hold up under review: run installs in a container with no cloud credentials mounted, and only inject deployment credentials in a later job that does not run npm install. Use short-lived OIDC federation to your cloud provider instead of long-lived access keys, so a stolen environment variable expires in minutes. Pin your GitHub Actions to full commit SHAs rather than tags, because a tag on a third-party action can be moved silently. Restrict the default token permissions to read-only at the repository level and grant write scopes per job.
If you publish npm packages yourself, turn on trusted publishing and provenance so consumers can verify that a tarball was built by your CI from a specific commit. Then treat your own publish token like a production secret: rotate it, scope it to a single package where possible, and make sure it does not exist on any laptop.
What to do with a wall of critical findings
npm audit output is where most dependency programs die. A mid-sized Node application will report dozens of high and critical advisories, the majority of them in build tooling that never ships, or in a code path your application does not call. Teams either fix everything and burn a week, or fix nothing and stop looking. Both outcomes are bad, and the second one is what a buyer discovers when they ask for your vulnerability management evidence.
The workable middle is a written triage rule that you apply consistently and can show to an auditor. Separate dependencies from devDependencies and treat them as different risk classes with different timelines. For each remaining finding, record whether the vulnerable function is reachable from your code, whether the vulnerable path requires attacker-controlled input, and what the compensating control is if you are not patching this week. Reachability tooling helps here but a two-line note from an engineer who read the advisory is enough. What matters is that every open finding has a decision attached to it and a date.
This is also the artifact that answers the SOC 2 and ISO 27001 questions. Auditors are not checking that your dependency count is zero. They are checking that you have defined severity, defined a remediation window per severity, and can produce a record showing findings moving through that window. A ticket queue with dates in it satisfies that. A screenshot of a clean scanner dashboard on one particular Tuesday does not.
The runbook for a compromised package
Assume an advisory drops naming a package you depend on, with the malicious versions published four days ago. The first question is not "do we use it" but "did we install it". Those are different. Search your lockfiles across every repository, including the ones nobody has touched in a year, and search your CI logs for the resolved version, because a caret range may have pulled the bad version into one build and not another.
If a malicious version was installed anywhere, treat every secret that was present in that environment as disclosed. That means CI environment variables, cloud credentials, npm tokens, and anything in a .env file on the affected developer machines. Rotate them before you do the forensic work, because rotation is cheap and analysis is slow. Then pin the last known good version, rebuild, and redeploy from a clean runner rather than a cached one.
The part teams miss is the artifact registry. If a poisoned build produced a container image that is now sitting in your registry and referenced by a deployment manifest, removing the npm package does not remove the image. Trace forward from the build to every artifact it produced and every environment those artifacts reached. Write this down now, while nothing is on fire, because doing it under time pressure at 11pm is how environments get missed. If you would rather have someone on the phone who has run this before, that is what an incident response retainer covers.
When you should not hire anyone for this
If you are a five-person team with one Node service and no compliance requirement yet, you do not need a supply chain consulting engagement. You need an afternoon. Commit the lockfile, switch CI to npm ci, turn on Dependabot with grouped updates, enable MFA on every npm account that has publish rights, and remove cloud credentials from the install job. That is most of the risk reduction available to you and it costs nothing but the afternoon.
You also should not buy a software composition analysis platform because a questionnaire asked whether you have one. The question behind the question is whether you know what you ship and whether you fix things on a timeline. A free scanner plus a documented triage rule answers that honestly. Paid tooling earns its license when you have enough repositories that nobody can hold the inventory in their head, or when you need reachability analysis to keep the remediation queue survivable.
Where outside help genuinely pays is the build pipeline review, because that is the part where mistakes are expensive and quiet, and the part most teams have never had a second pair of eyes on. If that is where you are, our security work and the fixed-scope options on pricing are the place to start.
Want this handled? Tell us what your buyer is asking for and we will tell you what the work involves, what it costs, and what you can do yourself.
Talk to usOr talk about a retainer