Security
Dependency Security
Modern applications depend on hundreds or thousands of third-party packages. Each is a potential vulnerability vector. A single compromised or poorly maintained package can compromise your entire application. This guide covers securing your supply chain.
The Supply Chain Risk
Third-party packages are code you don't control. You trust the maintainers to keep it secure. But what if:
- A maintainer's account is hacked and a malicious version is published?
- A popular package has a security vulnerability?
- A maintainer includes code to steal data?
- A dependency of a dependency (transitive dependency) is compromised?
Real examples: Log4Shell (Java logging library with a critical vulnerability), event-stream (Node.js package that was compromised and used to steal cryptocurrency), and countless packages removed after security issues were discovered. These aren't theoretical—they've compromised production systems.
Known Vulnerabilities: npm audit and yarn audit
npm and yarn include audit commands that check your dependencies against a database of known vulnerabilities. Run them regularly:
npm audit — Shows vulnerabilities and suggests fixes (often by updating the package).
npm audit fix — Automatically updates vulnerable packages to patched versions.
This catches known vulnerabilities, but only after they've been reported. It doesn't prevent zero-day vulnerabilities or compromise.
Automated Vulnerability Scanning: Dependabot and Snyk
Tools like Dependabot (GitHub) and Snyk automate vulnerability scanning and create pull requests to fix issues:
Dependabot: Built into GitHub. Periodically scans dependencies against a vulnerability database and creates PRs to update vulnerable packages. Free for public repositories; available for private repositories on GitHub Enterprise.
Snyk: Standalone service. Scans dependencies, creates PRs, and provides detailed vulnerability information. Free tier for public projects; paid for private projects. Integrates with most CI/CD systems.
Both tools scan transitive dependencies (dependencies of dependencies), catching vulnerabilities you might not know about. Enable them and review the PRs they create. Don't auto-merge without testing—even security updates can break things.
CVE Databases
CVE (Common Vulnerabilities and Exposures) is a public database of known vulnerabilities. Each CVE has an ID (CVE-2021-44228 for Log4Shell), description, affected versions, and severity.
Audit tools query CVE databases to determine if your dependencies have known vulnerabilities. Understanding CVE information helps you prioritize fixes. A critical CVE affecting many versions requires urgent patching; a low-severity CVE in an optional feature might be deferred.
When you see a CVE in the news affecting Node.js packages or your tech stack, check your dependencies. Your audit tools might not have caught it yet (databases update periodically), so proactive checking is valuable.
Supply Chain Attacks: Compromised Packages
A supply chain attack is when a malicious package is published to npm, PyPI, or other registries. An attacker gains access to a popular package's repository and publishes a malicious version, or they publish a package with a similar name and hope for typos (typosquatting).
Real example: The npm package "left-pad" (a trivial utility) was removed by its author. An attacker published a malicious version with the same name. Thousands of builds pulled the malicious package before it was caught.
While audit tools catch known vulnerabilities, they won't catch a newly published malicious package. The best defense is careful package evaluation.
Evaluating Packages Before Adding Them
Before adding a dependency, evaluate it:
Downloads: How many downloads per week? A package with millions of weekly downloads is likely safer—it's widely used and more likely to be scrutinized.
Maintainers: Who maintains it? Is it an individual or a company? Are there multiple maintainers? A single-person project is higher risk if that person disappears.
Last published: When was it last updated? A package not updated in years is likely unmaintained. If a vulnerability is found, there's no one to fix it.
Repository: Is there a public repository? Can you see the code? Packages with public repositories are more transparent.
Issues and PRs: How are issues handled? Are PRs merged promptly? A responsive maintainer is a good sign.
Scope: Does the package do what you need and nothing extra? A package with a single, focused purpose is lower risk than one that does everything.
Small, focused, well-maintained packages are preferable to large monoliths with many features. When possible, use established packages from trusted sources (major companies, long-running projects with community backing).
Lock Files: package-lock.json and yarn.lock
Lock files record the exact versions of all dependencies installed. Commit them to version control. When another developer (or CI/CD) runs install, they get the exact same versions, ensuring reproducible builds.
Without lock files: Your package.json says lodash: "^4.17.0". You have 4.17.20 installed. A teammate runs install and gets 4.18.0 (a newer version allowed by the caret). A bug in 4.18.0 breaks their build, but works on your machine.
With lock files: Both developers get exactly 4.17.20. Builds are reproducible. When you want to update lodash, you intentionally run npm update, which updates the lock file and is reviewed in a PR.
Keeping Dependencies Updated
Outdated dependencies accumulate technical debt and security risk. The longer you go without updating, the bigger the jump when you finally do. Best practice: update regularly.
Automated PRs: Use Dependabot or Snyk to create PRs for updates. Review and merge them regularly (weekly or monthly). This keeps you current with minimal manual effort.
Testing: Don't auto-merge without testing. Run your test suite on update PRs. For major version updates, manual testing might be needed.
Breaking changes: Semantic versioning says major versions can break. Review major updates manually. Check the changelog and migration guides.
Removing Unused Dependencies
Over time, dependencies accumulate. You might have installed a package, used it briefly, and forgotten to remove it. Every unused dependency is attack surface.
Periodically audit your dependencies. Are all packages still in use? Tools like depcheck (for Node.js) identify unused packages. Remove them. This reduces dependencies you need to monitor and update.
Node.js Package Security Best Practices
1. Use a lockfile: Commit package-lock.json or yarn.lock.
2. Run npm audit: Regularly check for known vulnerabilities.
3. Enable automated scanning: Use Dependabot or Snyk.
4. Update regularly: Don't let dependencies get too old.
5. Evaluate before adding: Don't add every package you see. Think before depending.
6. Remove unused dependencies: Clean up unused packages.
7. Prefer small, focused packages: Over large monoliths with many features.
8. Monitor the security community: Follow security news. When a major vulnerability is found, check if you're affected.
The Balance: Convenience vs Security
Dependencies are a trade-off. Each dependency adds risk but saves development time. A small, focused, well-maintained package is a good trade. A large, abandoned, poorly-maintained package is a bad trade.
Build only what you need in-house. For everything else, use a well-maintained, widely-used package. Automate vulnerability scanning, update regularly, and periodically audit your dependency tree. Supply chain security requires vigilance, but it's essential to protecting your application.