Your application is mostly code you did not write. A typical JavaScript project has hundreds of transitive dependencies. A typical Python project has dozens. Each one is code running with the full trust and permissions of your application. This lesson covers how to manage that risk.
The supply chain threat
Supply chain attacks target the software you depend on rather than the software you write. The attacker does not need to find a vulnerability in your code — they inject one through a package you trust.
Notable examples:
- event-stream (2018) — a popular npm package was handed to a new maintainer who added a targeted cryptocurrency theft payload
- ua-parser-js (2021) — a package with 8 million weekly downloads was compromised to mine cryptocurrency and steal passwords
- colors/faker (2022) — the maintainer deliberately broke the packages, affecting thousands of downstream projects
- xz-utils (2024) — a sophisticated, multi-year social engineering campaign to backdoor a core Linux compression library
These are not theoretical risks. They happen regularly.
Dependency management fundamentals
Use lock files
Lock files pin the exact version of every dependency (including transitive dependencies) to a specific version and integrity hash:
package-lock.json(npm) oryarn.lock(Yarn) orpnpm-lock.yaml(pnpm)Pipfile.lock(Pipenv) orpoetry.lock(Poetry)Gemfile.lock(Bundler)go.sum(Go modules)
Without a lock file, npm install might resolve to a different version each time — including a newly published malicious version.
Always commit lock files to version control. Run npm ci (not npm install) in CI to install exactly what is in the lock file.
Minimise dependencies
Every dependency is a liability. Before adding one, ask:
- Can I write this in 20 lines of code?
- Is this a well-maintained package with a strong track record?
- How many transitive dependencies does it pull in?
- What is the bus factor? (Is one unpaid maintainer the only contributor?)
Tools like npm ls and pipdeptree show the full dependency tree, including transitive dependencies.
Audit regularly
# npm
npm audit
# pip (with pip-audit)
pip-audit
# Ruby
bundle audit
# Go
govulncheck ./...
Run these in CI on every pull request and on a schedule. Fix critical and high-severity vulnerabilities promptly. For lower severities, triage and plan — not every advisory requires immediate action.
SBOMs (Software Bill of Materials)
An SBOM is a machine-readable inventory of every component in your software, including versions and licenses. Think of it as an ingredients list.
Why SBOMs matter
- Vulnerability response. When a new CVE is announced, can you quickly determine which of your applications use the affected library? With an SBOM, you can query instantly. Without one, you are searching through repositories manually.
- Regulatory compliance. Several regulations (US Executive Order 14028, EU Cyber Resilience Act) require or encourage SBOMs for software used in government or critical infrastructure.
- License compliance. SBOMs track which licenses your dependencies use, helping you catch GPL or AGPL dependencies that may have implications for your distribution model.
Generating SBOMs
# CycloneDX for npm
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
# Syft (works for containers, filesystems, and most package managers)
syft . -o cyclonedx-json > sbom.json
Standard formats: CycloneDX and SPDX are the two dominant formats. Both are supported by major security tools.
Integrating SBOMs into your workflow
Generate an SBOM as part of each build. Store it alongside the release artefact. Use tools like Grype or Dependency-Track to continuously scan SBOMs against vulnerability databases.
Dependency update strategies
Automated updates
Tools like Dependabot (GitHub), Renovate, and Snyk automatically open pull requests when new versions of your dependencies are available.
Configure them to:
- Group minor and patch updates to reduce PR noise
- Auto-merge patch updates for trusted packages (with passing tests)
- Flag major version updates for manual review
- Prioritise security updates
Version pinning strategies
| Strategy | Lock file | Manifest (package.json) | Trade-off |
|---|---|---|---|
| Pin exact versions | Yes | "lodash": "4.17.21" | Maximum reproducibility; requires manual updates |
| Use ranges | Yes | "lodash": "^4.17.0" | New patches install via lock file update; common in JS |
| Lock file only | Yes | Any range | Lock file ensures reproducibility; manifest allows flexibility |
Recommendation: Use ranges in the manifest and rely on the lock file for exact version pinning. Update the lock file regularly (via Dependabot/Renovate) and review changes.
Package registry security
Use a private registry
For internal packages, use a private registry (npm Enterprise, Artifactory, GitHub Packages). This prevents:
- Dependency confusion attacks (a public package with the same name as your internal package)
- Internal code leaking to the public registry
Namespace protection
Register your organisation's scope/namespace on public registries even if you do not publish publicly. This prevents attackers from claiming it.
@yourcompany/internal-lib ← you own the @yourcompany scope
Dependency confusion defence
Dependency confusion attacks exploit package managers that check the public registry before the private one. An attacker publishes a higher-versioned package with the same name as your internal package on the public registry.
Defence:
- Use scoped packages (
@company/package-name) instead of unscoped names - Configure your package manager to resolve specific scopes from your private registry only
- Use tools that detect dependency confusion risks
Evaluating new dependencies
Before adding a dependency, check:
- Maintenance: When was the last commit? Are issues being addressed?
- Popularity vs. quality: Download counts do not equal security. A popular package can still be compromised.
- Contributors: Is it maintained by one person or a team? What is the governance model?
- Known vulnerabilities: Check
npm audit, Snyk, or OSV for existing advisories. - Permissions: What does the install script do? Does the package need native binaries? Does it make network calls during install?
- License: Is the license compatible with your use case?
Tools like Socket.dev and Snyk Advisor provide automated analysis of these factors.
Summary
Your dependencies are your code — you are responsible for their security. Use lock files and audit them regularly. Generate SBOMs so you can respond quickly to new vulnerabilities. Automate dependency updates but review security-relevant changes. Use private registries for internal packages and scope names to prevent dependency confusion. Minimise what you depend on, and evaluate the maintenance, security, and governance of every package you add.
