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.
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 call