In 2016, removing an 11-line npm package called left-pad broke thousands of projects worldwide. Nine years later, attackers compromised packages with 2.6 billion weekly downloads using phishing and self-propagating malware.
The Problem: A Decade of Escalating Supply Chain Attacks
Timeline
March 2016: Left-pad incident - removing an 11-line dependency broke thousands of projects including Babel and React.
October 2021: ua-parser-js compromise - library with 7M+ weekly downloads hijacked multiple times, injecting cryptocurrency miners and password stealers.
January 2022: colors.js/faker.js sabotage - maintainer intentionally broke packages with 20M+ weekly downloads.
September 2025: Shai-Hulud attack - 20+ packages including chalk, debug, ansi-styles, and strip-ansi with 2.6B+ weekly downloads compromised through maintainer phishing.
Attack Vectors
- Typosquatting: Malicious packages with names similar to popular libraries
- Dependency Confusion: Public packages mimicking private internal packages with higher version numbers
- Maintainer Account Compromise: Targeting legitimate maintainer credentials
- Subdependency Poisoning: Compromising lesser-known dependencies deep in the tree
Minimal Dependencies in Practice
Dependency management is about making informed choices. npm’s micro-package culture creates vast dependency trees, while languages like Go emphasize standard libraries and focused dependencies.
This post isn’t about Node.js/npm versus Go. These principles apply to any language ecosystem—the key is understanding your dependency choices and their security implications regardless of the platform you’re using.
Real-World Examples of “Less is More”
For the libraries I maintain (eg: czds and extsort), minimal dependencies are a core principle. Sometimes I’ll implement something like minimal JWT parsing myself rather than pulling in a full cryptographic library with dozens of dependencies.
The github.com/miekg/dns
Go library demonstrates this perfectly: it’s feature-complete for DNS operations but relies only on Go’s standard library and a few golang.org/x/
packages. Compare this to typical npm packages that can easily pull in 100+ transitive dependencies for similar functionality.
Each dependency is a potential attack vector. Libraries with many dependencies spread trust across countless organizations and individuals.
Dependency Decision Framework
Every dependency addition is a decision point. The difference between my JWT implementation and a typical npm approach: 20 lines of focused code versus potentially hundreds of transitive dependencies.
Risk Assessment
Before adding any dependency:
- Scope: Is this dependency doing more than I need?
- Maintenance: How many maintainers does it have? When was it last updated?
- Trust Surface: How many transitive dependencies does it bring?
- Value Ratio: What’s the complexity vs. security impact ratio?
Implementation vs. Import
Implement yourself when:
- Simple, well-defined functionality (like string padding or basic parsing)
- Security-critical code where you need full control
- Stable requirements unlikely to change
- The “wheel” being reinvented is actually a simple function
Import dependencies for:
- Complex algorithms you’re unlikely to implement correctly
- Cryptographic functions requiring specialized expertise
- Well-established protocols with extensive edge cases
- Libraries with strong track records and minimal dependencies
My JWT parsing handles the specific decoding I need without the complexity of a full cryptographic library. The tradeoff: I handle basic token validation, but avoid 50+ dependencies that come with full-featured JWT libraries.
Ecosystem Considerations
The npm ecosystem’s micro-package culture creates different risks than Go’s standard-library approach:
- npm: 100+ transitive dependencies for basic functionality is common
- Go: Standard library covers most needs, focused external packages
- Python: Mix of comprehensive standard library and ecosystem packages
- Risk: Each ecosystem’s culture shapes your attack surface
Core Principles
- Every dependency is a trust decision - you’re trusting not just the library, but its entire dependency tree
- Evaluate the value-to-risk ratio - sometimes 10 lines of custom code beats 50 transitive dependencies
- Audit what you actually need - many libraries provide far more functionality than you’ll use
- Factor in long-term maintenance - consider the burden of keeping dependencies updated vs. maintaining focused implementations
Defensive Strategies
Dependency Auditing: Use npm ls
or go mod graph
to visualize your complete dependency chain. Look for unexpected depth or breadth.
Version Pinning: Pin exact versions rather than using ranges. This prevents automatic malicious updates but requires active management for security patches.
Vulnerability Scanning: Integrate tools like npm audit
, go mod tidy
, or language-specific security scanners into your CI/CD pipeline.
Standard Library First: Default to language standard libraries when possible. They’re maintained by core language teams and have fewer dependencies.
Regular Cleanup: Periodically audit and remove unused dependencies. Dead code dependencies still represent attack surface without value.
Supply chain attacks have evolved from accidental disruption to targeted campaigns. Every dependency extends trust to its maintainers and their entire dependency tree. Minimal dependencies are essential security practice.