Safeguard
Open Source Security

Direct vs Transitive Dependencies

Most known vulnerabilities live in transitive dependencies, not the ones in your manifest. Here's how to tell them apart and prioritize what's exploitable.

James
Principal Security Architect
7 min read

A project's manifest file is not its dependency tree. When a team runs npm install express, package.json gains one line, but express pulls in accepts, body-parser, cookie, debug, and dozens of other packages that ship code without a single explicit approval. Those are transitive dependencies: packages your dependencies depend on, several hops removed from anything a developer typed into a terminal. The distinction matters because most vulnerability scanners, and most engineering teams, still reason about risk as if package.json were the whole attack surface. It isn't. The March 2024 xz-utils backdoor (CVE-2024-3094) was buried three layers deep in the build chain of OpenSSH on affected Linux distributions, invisible to anyone auditing only their direct dependency list. This post breaks down what separates direct from transitive dependencies, how large that gap typically is, and how security teams should actually prioritize the vulnerabilities buried inside it.

What is the difference between a direct and a transitive dependency?

A direct dependency is a package your project declares explicitly in its manifest (package.json, requirements.txt, pom.xml, go.mod); a transitive dependency is anything pulled in because one of those declared packages needs it to run. If your Node.js app lists express in package.json, that's direct. Express itself depends on body-parser, which depends on bytes and iconv-lite, which depend on still more packages — none of which appear anywhere in your manifest, yet all of which execute inside your process. The relationship is recursive: a transitive dependency of your transitive dependency is still your transitive dependency, however many hops away it sits. Package managers resolve this entire graph automatically (npm's node_modules tree, Maven's dependency tree, pip's resolved wheel set), which is precisely why most engineers have never manually inspected more than a fraction of the code actually running in their own production systems.

How many transitive dependencies does a typical project actually have?

Most projects carry five to ten times more transitive dependencies than direct ones. Snyk's 2020 State of Open Source Security report found that a typical JavaScript project declares around 10 direct dependencies but resolves roughly 80 total packages once transitive dependencies are counted — and that 86% of the vulnerabilities Snyk detected across JavaScript projects that year were located in transitive, not direct, dependencies. The ratio isn't unique to Node.js. A Maven project with 15 direct entries in pom.xml commonly resolves 100-150 total JARs once transitive dependencies are pulled in, because Java libraries frequently bundle their own logging, HTTP client, and serialization dependencies. A minimal Python service with 8 lines in requirements.txt can easily resolve 60 or more packages once transitive requirements are pinned by pip's resolver. The pattern holds across ecosystems: the manifest file developers actually read represents a small fraction of the code that ships.

Why did the xz-utils backdoor hide inside a transitive dependency chain?

The xz-utils backdoor (CVE-2024-3094) worked precisely because almost nothing depends on xz-utils directly. On affected Debian and Fedora builds, sshd doesn't link against liblzma in a vanilla configuration — the attack path ran through libsystemd, which links against liblzma to compress journal data, and some distributions patch OpenSSH to link against libsystemd for notify-socket support. That put the backdoored liblzma code (shipped in versions 5.6.0, released February 24, 2024, and 5.6.1, released March 9, 2024) inside the memory space of sshd on systems three dependency hops removed from anything resembling a deliberate, auditable choice. Andres Freund, a PostgreSQL developer at Microsoft, discovered it on March 29, 2024, only because he noticed sshd logins were consuming roughly 500 milliseconds more CPU time than expected and traced the anomaly back through profiling — not through a dependency scanner, since no direct-dependency scan would ever have flagged liblzma as relevant to OpenSSH in the first place.

Can a vulnerability in a transitive dependency actually be exploited?

Only if the vulnerable code path is reachable from code your application actually executes, which is why raw CVE counts on transitive dependencies overstate real risk. Log4Shell (CVE-2021-44228, disclosed December 9, 2021) is the clearest example: thousands of Java applications included log4j-core 2.14.1 as a transitive dependency — often pulled in through Spring Boot starters, Elasticsearch clients, or other logging facades — without ever calling a logger configuration that invoked the vulnerable JNDI lookup path. Teams that patched every application containing log4j-core regardless of usage spent weeks on emergency changes; teams that could trace whether the vulnerable JndiLookup class was reachable from attacker-controlled input, such as an HTTP header logged verbatim, correctly separated urgent fixes from theoretical ones. Reachability analysis — tracing call graphs from your application's entry points down into the vulnerable function inside a transitive package — is what turns "this CVE exists somewhere in my tree" into "this CVE is exploitable in my running application."

How do SBOMs capture transitive dependencies differently across formats?

CycloneDX and SPDX both model transitive relationships, but CycloneDX has historically made the full dependency graph easier to query. CycloneDX's dependencies array explicitly maps each component to the components it dependsOn, so a scanner can walk the graph from your direct dependencies down to any transitive package in a handful of steps. SPDX, prior to SPDX 3.0 (released April 2024), relied on flatter DEPENDS_ON relationship statements that were frequently omitted by generator tools, producing SBOMs that listed every package present without always encoding which packages depended on which. That gap matters operationally: an SBOM that lists liblzma as present tells you almost nothing; an SBOM that shows liblzma sitting three hops beneath sshd via libsystemd tells you exactly how exposure travels through your stack. When evaluating an SBOM generation tool, the question isn't "does it list transitive dependencies" — nearly all do — it's "does it preserve the edges connecting them."

How should security teams prioritize vulnerabilities in transitive dependencies?

Prioritize by exploitability and depth, not by CVSS score alone, because a critical CVSS score in a transitive dependency three hops deep and never called is a lower real-world risk than a medium-severity CVE in a directly invoked function. The 2018 event-stream incident illustrates the failure mode from the opposite direction: a new maintainer added a malicious package, flatmap-stream, as a dependency of the popular event-stream library, and it shipped silently into every project that depended on event-stream — including Copay's Bitcoin wallet app, where the payload specifically targeted wallet credentials. No CVE existed to flag it; it was a supply-chain injection, not a known vulnerability, which is exactly the class of risk that transitive-dependency monitoring, not just CVE matching, is built to catch. A workable prioritization order looks like: first, is the vulnerable function reachable from application code; second, is the transitive package pulled in by more than one direct dependency, which increases blast radius; third, does a fix exist that doesn't require forking a package three levels removed from your control; and fourth, has the package had a suspicious maintainer or release-cadence change recently — the same signal that would have flagged event-stream before flatmap-stream ever shipped.

How Safeguard Helps

Safeguard maps your full dependency graph — direct and transitive — and runs reachability analysis to confirm which vulnerable functions in third-party code, however many hops deep, are actually callable from your application's entry points, so teams stop triaging CVEs that can never execute. Griffin AI, Safeguard's detection engine, correlates that reachability data with real-world exploit activity and maintainer or release-pattern anomalies — the same signal class that would have caught event-stream's flatmap-stream injection before it shipped — to separate urgent transitive-dependency risk from noise. Safeguard generates CycloneDX and SPDX SBOMs that preserve the actual dependency edges rather than a flat component list, and ingests SBOMs from build pipelines or vendors to reconstruct that same graph for third-party and vendored code. When a fix exists, Safeguard opens an auto-fix pull request that bumps the vulnerable transitive dependency, or the nearest direct dependency that controls its version, without requiring a manual fork three layers removed from your team's own code.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.