The Hidden Dependencies Problem
You declare Flask as a dependency. Flask brings Jinja2, which brings MarkupSafe, which brings... you're not sure anymore. Most developers can name their direct dependencies. Most can't name half their transitive dependencies. That creates a blind spot: a vulnerability in MarkupSafe affects your application, but you didn't know MarkupSafe was installed. Transitive dependencies are critical for security because they're often unmaintained, poorly scrutinized, and frequently exploited. The 2021 Codecov breach used a transitive dependency attack. Understanding your dependency tree is essential for knowing what's actually running in your application.
A transitive dependency is a library that your code doesn't directly use, but gets pulled in because one of your libraries depends on it. You ask for library A, A needs B, B needs C, and suddenly you have three libraries installed but only declared one.
Your code depends on Flask, which is a direct dependency that you explicitly requested. Flask in turn depends on Jinja2 (a transitive dependency that Flask needs), which depends on MarkupSafe (another transitive dependency that Jinja2 needs). Flask also depends on Werkzeug, Click, itsdangerous, and Blinker—all transitive dependencies. In total, you declared 1 dependency but actually installed 7 packages.
Why This Matters for Security
The Numbers Are Staggering
When you install common frameworks, you get far more dependencies than you explicitly requested:
What You Ask For | Direct Deps | Total Installed | Transitive % |
|---|---|---|---|
| 1 | 7 | 86% |
| 1 | 9 | 89% |
| 1 | 62 | 98% |
| 1 | 85+ | 99% |
| 2 | 8 | 75% |
| 1 | 45+ | 98% |
A typical production application has 50–500+ total dependencies, but only 10–20 are direct. The rest are transitive — code you never chose, never reviewed, and probably don't know exists.
The Attack Surface Problem
Every dependency is code running inside your application. Every dependency carries multiple risks: known vulnerabilities (CVEs) with publicly disclosed security flaws, malicious code injected by a compromised maintainer or hijacked account, abandoned maintenance with no one patching security issues, and license conflicts that create legal risks you inherit silently. Because transitive dependencies make up 85–99% of your dependency tree, most of your security risk comes from code you didn't explicitly choose.
Visualizing Dependency Trees
The following diagram shows how a single direct dependency can expand into a large tree of transitive dependencies. Your application directly depends on Flask v3.0.0 (What You Asked For). Flask depends on Jinja2 v3.1.2, Werkzeug v3.0.0, Click v8.1.3, itsdangerous v2.1.2, and Blinker v1.6.2 (Transitive dependencies at Depth 2). Jinja2 depends on MarkupSafe v2.1.1, and Werkzeug also brings MarkupSafe v2.1.1 (Transitive dependencies at Depth 3, DEDUPED). In total: Declared 1, Direct 1, Total Installed 7, Transitive 6 (86%).
Notice that MarkupSafe appears twice — both Jinja2 and Werkzeug depend on it. The package manager resolves this to a single version that satisfies both constraints.
Python Example
$ pip install flask$ pipdeptree --packages flask # Output shows Flask 3.0.0 as the root with the following structure:# - Direct dependencies: blinker, click, itsdangerous, Jinja2, and Werkzeug# - Jinja2 (depth 2) brings in MarkupSafe 2.0 as a transitive dependency# - Werkzeug (depth 2) also brings in MarkupSafe 2.1.1# Note: MarkupSafe appears twice, but the package manager resolves to a single versionNode.js Example
$ npm install express$ npm ls --all # Express 4.18.2 brings in accepts 1.3.8, which depends on mime-types 2.1.35# and mime-db 1.52.0, plus negotiator 0.6.3. Body-parser 1.20.1 is another# direct dependency with its own chain: it requires bytes, content-type, debug# (which brings ms 2.0.0), depd, destroy, http-errors, iconv-lite, on-finished,# qs, raw-body, and type-is. Many of these have their own transitive dependencies.# Side-channel brings in call-bind, get-intrinsic, and object-inspect.# Plus 50+ more entries not shown here.One npm install express → 62 packages. Each package is maintained by a different person or team.
Java Example
$ mvn dependency:tree # The myapp project declares a single direct dependency on spring-boot-starter-web 3.2.0.# Spring Boot Starter Web in turn depends on spring-boot-starter 3.2.0, which brings# spring-boot 3.2.0 and spring-core 6.1.1 (with spring-jcl 6.1.1), plus# spring-boot-autoconfigure, jakarta.annotation-api, snakeyaml, and more.# Additionally, spring-boot-starter-json 3.2.0 includes Jackson libraries:# jackson-databind (with jackson-annotations and jackson-core) plus 15 more Jackson modules.# Spring-boot-starter-tomcat 3.2.0 brings embedded Tomcat: tomcat-embed-core,# tomcat-embed-el, tomcat-embed-websocket, and 30+ more dependencies.# In total: 1 declared direct dependency, 85+ transitive packages installed.One Spring Boot starter → 85+ JARs.
The Depth Problem
Transitive dependencies aren't just one level deep. They chain recursively. Your application directly depends on Library A at depth 1. Library A depends on Library B at depth 2. Library B depends on Library C at depth 3. Library C depends on Library D at depth 4. Library D depends on Library E at depth 5. Library E depends on Library F at depth 6. This chain demonstrates how transitive dependencies can reach significant depths in modern applications.
In the Node.js ecosystem, dependency trees routinely reach depth 10+. A vulnerability at depth 6 is just as dangerous as one at depth 1 — it still runs in your process with the same permissions.
The event-stream Incident (2018)
A real-world supply chain attack that exploited transitive depth occurred when the Copay Bitcoin wallet application declared event-stream as a direct dependency (a popular stream utility library). Event-stream, in turn, depended on flatmap-stream as a transitive dependency. However, flatmap-stream was injected by an attacker and contained obfuscated malicious code specifically targeting Bitcoin wallet keys for theft.
The attacker offered to maintain the abandoned event-stream library, added flatmap-stream as a new transitive dependency, and flatmap-stream contained obfuscated code targeting the Copay Bitcoin wallet. The Copay developers never reviewed flatmap-stream. Why would they? It was a transitive dependency of a transitive dependency.
Common Transitive Dependency Problems
1. Version Conflicts (Diamond Dependencies)
When two of your dependencies need the same transitive dependency but at incompatible versions, a conflict arises. Your application depends on both Library A and Library B. Library A requires OpenSSL >= 3.0, while Library B requires OpenSSL < 3.0. This creates a conflict because both constraints cannot be satisfied simultaneously—the same library cannot be both less than 3.0 and greater than or equal to 3.0 at the same time.
Package managers handle this differently: Python (pip): Fails with a resolver error — you must fix it manually. Node.js (npm): Installs BOTH versions in nested node_modules — now you have two copies of openssl. Java (Maven): Uses "nearest wins" — the version closest to your project in the tree. Go: Uses Minimum Version Selection — picks the minimum version that satisfies all constraints.
2. Phantom Dependencies
Code that imports a transitive dependency directly without declaring it represents a fragile pattern. Your requirements.txt only lists flask==3.0.0, but your code does from markupsafe import Markup — this works because Flask installed MarkupSafe. The danger is that if Flask ever drops MarkupSafe, your code breaks with no warning.
3. Dependency Confusion
An attacker publishes a malicious package with the same name as your internal package to a public registry. When your package manager resolves dependencies, it downloads the public (malicious) version instead of your internal one.
Your company's internal registry: @company/auth-utils v1.2.0 Public npm registry (attacker): @company/auth-utils v99.0.0 ← Higher version number wins npm install → downloads the attacker's v99.0.0How to Inspect Your Dependency Tree
Commands by Ecosystem
# Pythonpip install pipdeptreepipdeptree # Full treepipdeptree --reverse markupsafe # "Who depends on markupsafe?" # Node.jsnpm ls # Full treenpm ls --all # Including dev dependenciesnpm explain lodash # "Why is lodash installed?" # Java (Maven)mvn dependency:treemvn dependency:tree -Dincludes=log4j # "Where is log4j in my tree?" # Gogo mod graph # Full dependency graphgo mod why golang.org/x/crypto # "Why do I need this module?" # Rustcargo treecargo tree --invert regex # "Who depends on regex?"Counting Your Dependencies
# Python: count all installed packagespip list | wc -l # Node.js: count packages in node_modulesls node_modules | wc -l # Java: count JARsmvn dependency:list | grep -c ":jar:" # Go: count modulesgo list -m all | wc -lHow CleanStart Addresses Transitive Dependencies
1. Source-Level Dependency Tracking
CleanStart doesn't just scan the final image — it tracks every dependency from source code through compilation. When compiling Python 3.12 from source, several types of dependencies are required. Build dependencies like gcc 13.2 and make 4.4 are needed during compilation but are NOT included in the final runtime image. Runtime dependencies like glibc 2.38 and libssl3 3.1 ARE included in the final image and are tracked with attestation records. Optional dependencies like readline 8.2 are included only if explicitly configured during the build process. CleanStart distinguishes between these categories so you know exactly what's actually in your final image.
2. Dead Dependency Elimination
Traditional images include every dependency a package could use. CleanStart analyzes actual usage. Traditional Python images typically include every library a package could potentially use: libxml2 for XML parsing, libxslt for XSLT transforms, tcl/tk GUI toolkit, plus 40+ more unused system libraries. These bloat the image and increase your attack surface. CleanStart Python images include only the libraries actually linked by your specific application. Each included library is verified, attested, and documented in an SBOM. The result is 80-90% fewer transitive dependencies compared to traditional images—you get a lean, focused image with a minimal attack surface.
3. 281M+ Dependency Graph
CleanStart Source Intelligence Core maintains a dependency graph spanning 7 ecosystems (Go: 238M, Crates: 16.6M, npm: 12.5M, Maven: 7.2M, PyPI: 3.4M, RubyGems: 3.1M, C++: 18.7K). When a new CVE is disclosed, CleanStart can instantly determine which of your images contain the affected library (even transitively), whether the vulnerable code path is actually reachable in your usage, and what the remediation path looks like (which direct dependency needs updating).
In Practice
- You control ~15% of your dependencies. The other 85% are transitive — chosen by library authors, not you.
- Depth doesn't reduce risk. A vulnerability at depth 6 in the tree is just as exploitable as one in your direct dependency.
- Lock files are essential. Without them, your dependency tree can change between builds without any code change on your part.
- Scanning the final image isn't enough. You need to understand the full dependency graph — what's included, why, and whether it's actually used.
- Source-built images are fundamentally safer. When you compile from source, you know exactly which transitive dependencies are linked. When you download pre-built binaries, you're trusting someone else's entire dependency tree.
What to Read Next
What is a Software Library? — Foundational concepts on libraries and dependencies. What is a Package Manager? — How dependencies are installed and resolved. What is an SBOM? — The complete inventory that lists every dependency. Transitive Dependency Removal — How CleanStart eliminates the explosion. Dependency Intelligence — 281M+ dependency tracking across 7 ecosystems. What is Supply Chain Security? — The broader threat landscape.
