Transitive Dependency CVE Exposure Across Runtime Stacks
Most CVE risk hides in transitive dependencies that traditional scanners never see.

Most of what makes a software supply chain dangerous was never listed in anyone's manifest file. Most real CVE exposure in modern runtime stacks sits in transitive dependencies, the packages pulled in by the packages a team chose, and it compounds as dependency graphs get deeper and environments drift further from whatever was last scanned.
When one enterprise customer working with Kusari looked past direct dependencies, it found tens of thousands of components in its dependency tree, at a scale no developer had deliberately assembled or reviewed. That gap between what a team thinks it runs and what it actually runs is not an edge case. Research from Endor Labs, cited by Kusari, puts roughly 95% of open-source vulnerabilities in transitive components rather than the packages developers chose directly. That figure describes an order of magnitude, not a precise average every organization should expect to replicate, but it points at where the risk actually concentrates. Kusari's Application Security in Practice report found the same split showing up in team self-assessment: most respondents reported strong awareness of their direct dependencies, and only a minority reported similarly strong insight into the deeper transitive layers underneath them, and that mismatch between the risk's concentration and the scrutiny it receives is structural rather than a matter of effort. It is structural.
Traditional software composition analysis tools scan manifest files and check them against known vulnerability databases, so they can see top-level packages but not the full recursive graph beneath them. Dependencies that never appear in a manifest, whether pulled in indirectly through another package, downloaded manually, embedded as a vendored library, or left over from a legacy build, go undetected by the scan. If a scanner checks package.json or pom.xml against the National Vulnerability Database, it is doing what it was designed to do. It just was not designed to see three or four layers down, where most of the actual risk lives. Teams do not have a scanning problem in the sense of running too few scans or using weak tools. They have a coverage problem: the scans point at the wrong layer of the stack.
How dependency depth multiplies both exposure and resolution time
A vulnerability does not stay equally exposed once you know about it. The deeper a flaw sits in the dependency graph, the worse it gets, because depth itself becomes a variable that extends how long an organization stays exposed. A 2025 study of Maven projects used survival analysis and found that each additional level of transitive dependency is tied to meaningfully longer CVE resolution times, and a linear regression model showed resolution duration climbing steadily as dependency level increases. The deeper a vulnerable component sits, the longer it tends to stay unpatched in the wild, even after the flaw has a name and a fix.
The mechanism behind that pattern is straightforward. Fixing a vulnerability four layers down requires every maintainer in that chain to update in sequence: the deepest library has to patch first, then the package that depends on it has to adopt the new version, and so on up through each intermediate dependency until the fix finally reaches the team that actually deployed the software. But that sequence of handoffs doesn't exist for a direct dependency, where one maintainer update and one version bump close the loop. Every additional layer adds a maintainer who has to act before the next one can.
Reachability complicates this picture, but it complicates it in the other direction. Classport research out of KTH Royal Institute of Technology shows that static software bills of materials overcount risk because they include dependencies that are declared in a build but never actually loaded at runtime, so teams end up spending remediation effort patching components that never execute in production. Put those two findings together: teams under-see which transitive components actually exist, a blind spot, while simultaneously over-counting which of the components they do see actually matter, which produces alert fatigue. Both failures come from the same missing capability, the ability to see what a dependency graph actually does at runtime rather than what it claims to include on paper. Deeper graphs slow down fixes, and slower fixes stretch out exposure windows, and you can see those stretched-out windows when you look at real incidents in detail.
What compounding transitive exposure looks like in production
Three incidents from recent years share the same underlying shape: the vulnerable component sat several layers below anyone's build file, and it stayed invisible until exploitation was already underway.
Log4Shell, tracked as CVE-2021-44228, is the case most people already know by name, so the value here is in the structural factors rather than the retelling. A 2026 longitudinal measurement study used an active network telescope and found that exploitation attempts continued for years after the original disclosure, spanning 2021 through 2025, with attackers reusing infrastructure and continuing to evolve their payload obfuscation techniques long after patches were available. Severity came from three compounding factors stacking on top of each other: dependency chains deep enough to hide the library from casual inspection, patch adoption that varied wildly across industries, and the absence of any centralized governance over how the library got used downstream.
Netty, as it shows up inside Spring Boot end-of-life stacks in June 2026, is the ongoing case, and it deserves the most space here because it illustrates the mechanism most completely. Netty rides transitively inside Elasticsearch clients, inside gRPC, and inside most reactive Java stacks, so a team can carry real Netty exposure without ever declaring Netty as a dependency anywhere in its build files. End-of-life Spring Boot lines freeze their dependency bill-of-materials pins at the framework level, but the upstream projects feeding into that BOM keep moving. CVEs accumulate against a pinned version the team no longer has any practical way to update. Layered on top of that is a separate and more subtle failure: the Neo4j Java Driver 4.4 line ships its own shaded copy of Netty, relocated under its own internal package namespace, and many SCA scanners never see it. A dashboard can report a clean Netty 4.1.135 while a vulnerable copy of the library executes inside a completely different jar. This shaded-dependency blind spot matters because it shows that standard scanning can fail even when a team believes it has full visibility into its own stack.
The xz Utils compromise, tracked as CVE-2024-3094, is the supply chain case, and it shows the same structural invisibility playing out through a trust relationship, not a missed patch. An actor who spent time social-engineering a maintainer role on the project inserted malicious code into a compression library that most developers never interact with directly, but that sat inside the transitive dependency chain of critical Linux distributions through its connection to OpenSSH. The exposure here wasn't a coding flaw in the traditional sense. It was a trust relationship with the maintainer of a deeply nested component, invisible to any team that had never mapped its full dependency graph down to that depth. Across all three cases, the pattern repeats: depth hides the component, and the absence of a complete dependency graph is what turns that invisibility into an incident.
Environment Drift Widens the Transitive CVE Surface Between Scans
The cases above describe flaws that were present in a system while remaining invisible to the teams running it. Environment drift is the process by which that gap keeps widening even after a scan has run. When a developer pins a top-level library to a specific version but leaves its sub-dependencies free to resolve on their own, a remote package update can change what actually runs in production with no intentional code change on the team's part. That is drift, a predictable outcome of how package ecosystems resolve versions rather than a side effect of sloppy practice.
Drift is a security condition, not an inconvenience. Once a system stops executing under the same conditions that were originally validated, its security behavior can change with no code change at all, and the most recent clean scan stops describing what's actually running. The problem compounds across the software development lifecycle because development, CI, staging, and production environments each resolve their own dependencies independently, and each can drift in a different direction from the others. A scan that passes cleanly in CI does not guarantee that production is clean, because the environments may have already diverged by the time that scan even runs.
Static SBOMs decay for the identical reason. A dependency graph was accurate the moment a build was captured, but it goes stale as sub-dependencies keep updating underneath it. Kusari compares a static SBOM process to printing a map of a city where the roads keep changing after the map goes to print. End-of-life frameworks make this worse in a structural way, not just a practical one: once a bill of materials is frozen at the framework level, the team cannot receive upstream security pins even when it wants to, and the Spring Boot end-of-life case shows 18 CVEs accumulating in a single month against a dependency the team has no normal channel to update. Nuxt 3's end-of-life, set for July 31, 2026, shows the same dynamic playing out in a different stack: once that date passes, CVEs disclosed against the 3.x line have no upstream fix, and the bundled transitive dependencies underneath it, including Nitro, h3, and the Vue 3 integration, stop receiving patches alongside the framework itself. Drift is what happens by default when nobody actively holds an environment to a fixed, reproducible state, not a symptom of a careless team.
The AI agent and MCP ecosystem's new tier of transitive dependency risk
Every MCP server an agent can call is a production dependency, and as of mid-2026 the MCP ecosystem has no widely adopted certification or vetting infrastructure, so it is a transitive dependency surface with less visibility than even the worst-governed software stacks.
CVE-2025-6514 makes the risk concrete. The flaw was a critical remote code execution vulnerability in mcp-remote, an OS command injection bug in the OAuth handshake that triggered when mcp-remote connected to a malicious or untrusted MCP server, allowing full OS command execution on Windows and the launch of arbitrary executables with limited parameter control on macOS and Linux. The CVE was published July 9, 2025, and the fix had actually shipped earlier, on June 17, 2025. Remediation required upgrading to that patched version or later, connecting only to trusted MCP servers over HTTPS, and auditing every existing MCP connection. That single CVE shows how directly an MCP server functions as a dependency, with all the same consequences as an unpatched library sitting three layers down in a traditional build.
The ecosystem-level picture is worse. The ClawHub malicious package incident in 2026 found that, at its peak, one in five packages in that ecosystem were malicious, carrying CVEs that included three with public exploit code already circulating, a contamination rate with no real parallel in mature package ecosystems like npm or Maven. Part of what makes MCP riskier than a typical compromised package is its trust model: tool descriptions blend instructions with data, so a change to a server's metadata can redirect an agent's behavior just as effectively as a change to its system prompt. An attacker does not need to compromise the agent. Compromising a tool the agent already trusts is enough. A server can pass a security review on day one and then quietly change its tool definitions, its network destinations, or its own dependency tree afterward. That is why re-approval before every deployment has to be standard practice rather than a one-time gate.
None of this calls for a different governance model from the one already proven on traditional software. It calls for extending the same one: pin server and dependency versions, review lockfiles and package provenance before installing anything, generate an SBOM where the environment warrants it, scan dependencies for known CVEs, and keep checksums or attestations on file for every artifact that's been reviewed. Teams that have not extended that discipline to MCP are exposed the same way they were exposed to Log4Shell: through a dependency they didn't know was there.
Why Scanning Harder Does Not Close the Gap
A scan captures the state of a dependency graph at one instant. In an environment that keeps drifting, that graph keeps changing in between scans, so a clean result only certifies the moment the scan ran, not the state of whatever is actually running an hour, a day, or a week later.
Reachability analysis helps with prioritization, but it does not fix the underlying problem. Knowing which vulnerable functions a system actually calls lets a team triage faster, yet the Classport research shows that without real runtime dependency introspection, static analysis keeps mischaracterizing what's actually loaded, both overstating some risks and missing others that only appear at runtime. The shaded-dependency failure from the Neo4j and Netty case makes the same point from a different angle: even carefully configured SCA tooling can return a clean result while a vulnerable copy of a library runs inside a completely different jar, because the scanner checks the declared artifact and never sees the one embedded inside it.
Better scanning cadence and better tooling both help at the margins, but neither one addresses why the gap exists in the first place: dependency graphs run several layers deeper than any manifest shows, and environments drift away from their last validated state the moment any sub-dependency resolves to a new version on its own. Closing that gap takes environment definitions precise and reproducible enough that what gets scanned is actually what runs, not an approximation of it captured at build time and left to decay afterward.
Sources
- The 95% Problem: Why Transitive Dependencies Are Your Biggest Software Supply Chain Blind Spot in 2026
- HeroDevs Blog | Spring Boot Managed Dependencies Still Get CVEs After EOL: June 2026 Patch Round-Up
- Classport: Designing Runtime Dependency Introspection for Java
- The Lifecycle of Transitive Vulnerabilities in Maven
- The Top Open Source Vulnerabilities in 2026
- When Nuxt 3 Reaches End of Life: Why Some Teams Should Skip Nuxt 4 and Wait
- Out of Sight, Still at Risk: The Lifecycle of Transitive Vulnerabilities in Maven


