Toxic Dependencies (Vulnerable and Malicious Components)
Also known as: Vulnerable and outdated components, Software supply chain risk, Dependency confusion and typosquatting, Third-party package risk
A risk in which an application ships third-party packages that carry known vulnerabilities, are abandoned, or are outright malicious, and is controlled by software composition analysis, pinned and verified dependencies, and an SBOM.
How it works
A toxic dependency is a third-party package your application pulls in that puts you at risk. Three shapes are common: a component with a publicly known vulnerability that was never updated, a component that is abandoned so no fix will ever arrive, and a component that is malicious, for example a look-alike package name or a legitimate package whose release was tampered with after its publisher account was compromised.
The risk is amplified by how dependencies are declared. A loose version range such as an open-ended minimum lets a build silently pick up a new release the team never reviewed. Transitive dependencies, the packages your packages depend on, are rarely read by anyone, yet they run with the same privileges as your own code. Install-time scripts can execute before any test or review has run.
The root cause is trust without verification. Teams accept whatever the registry resolves today. The fix is to make that trust explicit and checkable: scan every build against advisory data, pin exact versions in a committed lockfile with integrity hashes, restrict where packages may come from, and keep a software bill of materials so you can answer within minutes whether a newly announced flaw affects you.
Defence is layered. Software composition analysis finds known-vulnerable components, pinning and integrity checks stop silent substitution, a private registry or allow-list limits what can enter, and a patch service-level agreement keeps the backlog from growing. Reducing the number of dependencies is the cheapest control of all.
Walk through it
- 1Read the manifest and the advisory
- 2Name the detection control
- 3Choose the pinning fix
- Inventory the tree with an SBOM
- Fix, gate and verify
A pre-release review of the storefront finds its dependency scan failing. Read the manifest and the advisory output as a reviewer and note how the dependencies are declared and what the scanner reports.
1{2 "name": "fabrikam-storefront",3 "dependencies": {4 "acme-markdown": "^1.0.0",5 "fast-invoice-pdf": "*",6 "colour-utils": "latest"7 },8 "scripts": { "postinstall": "node scripts/setup.js" }9}10 11// scan output (advisory data, fictional ids)12// acme-markdown 1.0.3 GHSA-xxxx-0001 HIGH fixed in 1.4.213// fast-invoice-pdf last release 2021-03, no maintainer response14// colour-utils new maintainer added last week, version 9.9.9Spot it
- Software composition analysis reports advisories (high or critical) against components in a build.
- A dependency receives a new maintainer, a sudden major version jump, or a release soon after a long dormant period.
- A new install-time script (preinstall or postinstall) appears in a package that did not previously have one.
- A package name that is one character away from a popular name, or an internal package name appearing on the public registry.
- The resolved dependency tree differs between builds of the same commit, or no lockfile is committed.
CI dependency scan
ts=2026-10-11T08:14:02Z scan=sca pkg=acme-markdown ver=1.0.3 advisory=GHSA-xxxx-0001 severity=high fixed_in=1.4.2
ts=2026-10-11T08:14:03Z scan=sca pkg=fast-invoice-pdf status=unmaintained last_release=2021-03Package registry audit feed
ts=2026-10-10T22:40:19Z pkg=colour-utils event=owner_added actor=new-maintainer-77 version=9.9.9
ts=2026-10-10T22:41:05Z pkg=colour-utils event=publish version=9.9.9 install_script=addedgrepgrep: loose or unpinned version declarations in manifests
grep -rnE '"(\^|~|\*|latest|>=)' --include=package.json . | grep -v node_modulesTreat every hit as a candidate for an exact pin; confirm a lockfile is committed beside each manifest.
kqlKQL: builds that resolved a new version of a package without a source change
CIBuildLog
| where Step == "install"
| summarize versions=dcount(ResolvedVersion) by Package, CommitSha
| where versions > 1The same commit should always resolve the same tree. Investigate any package that did not.
Stop it
Run software composition analysis in CI and fail builds on high-severity findings
Scan every build and every pull request against advisory data, cover transitive dependencies, and set a policy that blocks release on high or critical issues with a fix available. Re-scan deployed artifacts on a schedule, because new advisories appear after you ship.
Pin exact versions, commit lockfiles and verify integrity hashes
Declare exact versions, commit the lockfile, and install with a mode that refuses to change it. Integrity hashes make a substituted or tampered package fail the install instead of running.
Generate an SBOM, set a patch SLA, and shrink the dependency surface
Keep a software bill of materials per release so you can answer which systems use an affected component. Set patch deadlines by severity, verify publishers before adopting a package, restrict sources through a private registry or allow-list, and remove dependencies you do not need.
Declare exact versions instead of ranges
Vulnerable
"dependencies": {
"acme-markdown": "^1.0.0",
"fast-invoice-pdf": "*",
"colour-utils": "latest"
}Hardened
"dependencies": {
"acme-markdown": "1.4.2",
"colour-utils": "9.9.8"
}The abandoned package was removed and replaced; the others are fixed to reviewed versions and recorded in a committed lockfile.
CI gate: reproducible install and dependency scan
Hardened
steps:
, run: npm ci --ignore-scripts
, run: npm audit --audit-level=high
, run: sca-scan --fail-on high --sbom out/sbom.jsonnpm ci installs strictly from the lockfile and fails on mismatch. Skipping install scripts removes a common execution path; allow them only for reviewed packages.
- Run software composition analysis on every pull request and on a schedule against deployed releases.
- Commit lockfiles, install with a lockfile-strict command, and verify integrity hashes in CI.
- Pin exact versions for direct dependencies; update through reviewed, automated pull requests rather than open ranges.
- Resolve packages through a private registry or proxy with an allow-list, and reserve internal package names so they cannot be claimed publicly.
- Generate and store an SBOM for every release; map advisories to it to find affected systems quickly.
- Review new or changed maintainers, install scripts and transitive additions before adopting an update.
- Define patch SLAs by severity and remove unused or abandoned dependencies.
If it already happened
Freeze deployments that include the affected package, block the bad version at the registry proxy, and isolate any environment that already installed it.
Upgrade or replace the component, rebuild from a clean lockfile, rotate any secrets present on build or runtime hosts that ran it, and use the SBOM to find every other system affected.
Redeploy from verified artifacts, confirm the dependency tree matches the reviewed lockfile, and monitor for unexpected outbound connections or install-time activity.
Add the scan gate, pinning and integrity checks that were missing, set patch SLAs, and review how new dependencies are approved.
Check yourself
1. Which control detects dependencies with publicly known vulnerabilities on every build?
2. Why do loose version ranges and 'latest' create supply chain risk?
3. What is the main value of an SBOM during a newly announced component vulnerability?