A single vulnerability does not exist in isolation. Once published, it travels through your entire stack — from the library maintainer's repository to running production containers. Understanding this propagation path is essential to understanding why single-point defenses fail.
The Vulnerability Journey: A Timeline
Track a real CVE from discovery to production container. This is not a theoretical exercise — this is what happens with every vulnerability in the ecosystem.
Day 0: Vulnerability Discovered
A researcher finds a buffer overflow in OpenSSL. The code has been in production for 18 months. Roughly 2 billion devices are potentially affected.
Layer 1 impact: The vulnerable code is in the OpenSSL repository. If you've cloned this repository or pinned a recent version, the vulnerability is in your source code already.
Day 1: Vulnerability Reported to Maintainers
The researcher sends a responsible disclosure report to the OpenSSL team. They provide proof of concept. The maintainers begin triage.
Layer 1 status: Unchanged. Your source code still contains the vulnerable version.
Days 2-7: Patching Phase
The OpenSSL team identifies the root cause, develops a fix, writes a test case, and commits the patch. They schedule a release window. Security advisories are drafted. Downstream projects are contacted.
Layer 1 status: Unchanged. Your source code is unchanged unless you manually apply the patch (extremely rare).
Day 8: Release
OpenSSL 3.0.13 is released with the fix. The maintainers publish a detailed advisory. Severity is marked as "Critical" (CVSS 9.8).
Layer 1 status: The fix is available, but not in your source code. You have not updated.
Days 9-14: Package Manager Propagation
Package managers consume the OpenSSL release: Linux distributions (Ubuntu, Red Hat, Debian) begin building new packages Homebrew updates the formula Docker library images (official-library/ubuntu, official-library/python) start rebuilding
Layer 1 status: The vulnerable version is still in your source code unless you've updated. Package managers will offer the newer version, but you have to explicitly update.
Days 15-30: Base Image Propagation
Base images published by Docker Library and Linux distribution maintainers incorporate the patched version: ubuntu:22.04 is rebuilt with the new OpenSSL package python:3.11 is rebuilt with the new base OS + new OpenSSL node:18 is rebuilt with the new base OS + new OpenSSL
These new images are pushed to Docker Hub. But this creates a new problem: old base image tags (like ubuntu:22.04) now refer to a different set of packages than they did yesterday. Your CI/CD might have cached the old version.
Layer 2 status: If your Dockerfile uses FROM ubuntu:22.04, the base image you pull might contain the patched version — unless Docker Hub is serving cached layers. This is not guaranteed. Some users will get the patched version; others will get the vulnerable version, depending on caching behavior.
Days 14-45: Your CI/CD Rebuild
Meanwhile, in your organization, a developer is assigned to "update OpenSSL." Your CI/CD pipeline runs. The build pulls dependencies. Your build system uses pip install or npm install, which fetches the latest versions of everything. Some of your dependencies might update automatically. Others are pinned and won't update.
Your application gets rebuilt. The new image contains the patched OpenSSL — but only if your dependency resolution pulls the latest version. If you've pinned openssl==3.0.12 in a requirements file, the rebuild produces an image that still contains the vulnerability.
Layer 2 status: Your build produces a new artifact. The vulnerability may or may not be present, depending on your dependency pinning strategy.
Days 30-60: Deployment Rollout
Your new image is pushed to your registry. A deployment policy kicks in. The rolling update begins.
At Day 45: 10% of your production pods are running the patched version. At Day 60: 90% of your production pods are running the patched version.
Some legacy services that rarely deploy are still running the vulnerable version months later.
Layer 3 status: Your registry contains both vulnerable and patched images. If you're scanning the registry, you see both. Vulnerability management policies are constantly re-evaluating because the same application has multiple versions in the registry, some vulnerable and some patched.
Day 60+: Active Exploitation
By the time you've deployed the patch across your entire fleet, vulnerability exploits are already active in the wild. Zero-day exploits exist before the patch, but they become weaponized when the patch is released (because attackers reverse-engineer the patch to understand the vulnerability).
For systems that didn't get the patch (legacy applications, abandoned services, customers who haven't updated), this vulnerability is now being actively exploited.
Layer 4 status: Some of your clusters are running the vulnerable code under attack. Some have been patched. Some are unaware they run the vulnerable version.
A typical vulnerability propagates through multiple layers over time:
Day 0: Discovery. A vulnerability is discovered and disclosed.
Day 7: Patched by Maintainer. The package maintainer releases a patch fixing the vulnerability.
Day 14: Distros Build. Linux distributions (Alpine, Debian, Ubuntu) begin incorporating the patched versions into their repositories.
Day 30: Base Images. CleanStart and other base image providers include the patched versions in their base images and rebuild.
Day 45: Your CI Rebuilds. Your CI/CD pipeline detects the new base image and automatically rebuilds your applications, incorporating the patch.
Day 60+: Exploited. For systems that haven't been patched (legacy applications, abandoned services, customers who haven't updated), the vulnerability is now being actively exploited in the wild.
During this entire timeline, Layer 1 (Source) impact means your code is vulnerable unless you manually intervene. Layer 2 (Build) impact means your builds produce vulnerable artifacts unless you pull the latest versions. Layer 3 (Registry) impact means your registry contains vulnerable images unless you proactively rescan and redeploy. Layer 4 (Runtime) impact means your clusters run vulnerable code under active exploitation during this 60-day window.
This 60-day window is optimistic. It assumes: The vulnerability is discovered responsibly (not exploited secretly for months first) The maintainer patches quickly Package managers propagate fast Your organization prioritizes the patch Deployment doesn't require manual approval at each stage
Many real vulnerabilities take longer. The xz/liblzma backdoor (CVE-2024-3094) existed undetected for months. The Log4j vulnerability (CVE-2021-44228) had a 10-day gap between discovery and awareness, during which nobody in the world was patching because nobody knew. The Cloudflare vulnerability (CVE-2019-9193) existed for years before detection.
The Compounding Effect: New Vulnerabilities While You're Patching
While you're rolling out a fix for CVE-A, 10 new vulnerabilities are discovered in other components.
On any given week in 2024: 50-100 new CVEs are published 300-500 vulnerabilities are disclosed in open-source projects 5-10 of those are marked "Critical" or "High" severity
Your patch timeline for one vulnerability (60+ days) means that by the time you finish patching CVE-A, CVE-B through CVE-J have already been published and are circulating.
This creates a vulnerability lag: the time between when a vulnerability is published and when your containers are patched. Most enterprises have a lag of 30-90 days. During this window, if an attacker has published an exploit, your systems are under attack.
The lag is not linear. Some vulnerabilities are patched in 7 days. Others in 6 months. It depends on: Severity and exploitability Whether the vulnerable component is a dependency or core code Whether the maintainer is responsive Whether your organization has allocated resources to patch Whether the patch requires testing or coordination across teams
Layer 1 strategy (knowing your dependencies) helps detect this lag. If you know you depend on OpenSSL and you get a CVE alert, you know you're affected. Layer 3 strategy (registry scanning) helps detect vulnerable images that were built before the patch existed. Layer 4 strategy (runtime monitoring) helps detect exploitation while you're still patching.
None of these strategies eliminate the lag. The lag is unavoidable because software is released, distributed, built, and deployed through a pipeline that takes weeks or months. The best you can do is minimize it and detect attacks while you're closing the window.
Transitive Vulnerability Propagation: The Hidden Dependency Chain
Your application doesn't call OpenSSL directly. Your application calls an ORM (Object-Relational Mapping library). The ORM calls a database driver. The database driver calls libpq (the PostgreSQL C client library). libpq links to OpenSSL.
The dependency chain runs from application code down to the kernel. Application code calls the ORM framework, which calls the database driver. The database driver calls libpq (the C library). libpq calls OpenSSL, which ultimately interfaces with the kernel.
When OpenSSL has a vulnerability: Application layer: No direct call to OpenSSL ORM layer: No direct call to OpenSSL Driver layer: No direct call to OpenSSL libpq layer: Calls OpenSSL for TLS handshakes OpenSSL layer: Vulnerable code Kernel layer: If exploited, may lead to privilege escalation
Your application code doesn't know it depends on OpenSSL. Your ORM library doesn't know it depends on OpenSSL. The dependency chain is transparent. But the vulnerability is not. When OpenSSL is vulnerable, every component in the chain is affected, even if they don't directly use the vulnerable function. Transitive dependency vulnerability affects enterprise environments significantly: Your application has 47 direct dependencies (packages you explicitly import). Those 47 dependencies have an average of 38 transitive dependencies each. That's roughly 1,786 total packages. If we assume a 1% rate of vulnerability across all packages (slightly conservative), you have ~18 vulnerable packages in your dependency tree that you're not directly aware of. Many of those vulnerabilities are not exploitable in your specific context. But you don't know which ones without deep analysis. So you track all of them. SCA (Software Composition Analysis) tools help by building a dependency tree and flagging vulnerabilities, but they struggle with the scale. A typical Node.js application has 500+ transitive dependencies. A typical Python application has 100+. A typical Java application using Spring Framework has 200+. The vulnerability is real even if it's in a transitive dependency. When a CVE is published for a package three layers deep in your dependency tree, it affects you. You can't ignore it, but you also can't fix it without waiting for each layer of maintainers to patch and propagate the fix upward. ## Why Scanners Disagree: Different Databases, Different Detection You run the same container image through three vulnerability scanners:Trivy: 347 vulnerabilities found Grype: 412 vulnerabilities found Snyk: 289 vulnerabilities found
Which number is correct? None of them are fully correct, and all of them are partially right. Trivy uses the National Vulnerability Database (NVD) as its primary source. NVD is authoritative but lags behind vendor disclosures. When a vendor publishes a CVE, NVD takes 3-7 days to ingest and process it. Trivy's database is updated weekly. Grype uses multiple sources: NVD, Red Hat, Ubuntu, Debian package advisories, GitHub Security Database, and others. Grype's database is updated daily. It catches vulnerabilities faster than Trivy because it consumes advisories directly from Linux distributions. Snyk maintains its own database, combining NVD, vendor advisories, security research, and data from Snyk's customer scanning activity. Snyk's database is proprietary and updated in real-time. The differences aren't just about timing. They're also about what counts as a vulnerability: - **Snyk flags** an old vulnerability in a transitive dependency because the dependency hasn't been updated in 3 years, indicating potential abandonment- **Trivy flags** only vulnerabilities with a CVE identifier in NVD- **Grype flags** vulnerabilities that distributions have patched, even if NVD hasn't assigned a CVE ID yet These different scoring systems mean the same image produces different risk profiles depending on which tool you use. An image that "passes" Trivy (severity threshold set to Critical and High) might fail Snyk (threshold set to High and Medium) because Snyk has identified additional vulnerabilities. This disagreement creates policy confusion: What severity threshold is actually secure? If Trivy says 47 vulnerabilities and Snyk says 412, what do you enforce in CI/CD? If Grype says it's worse than Trivy, does that mean Grype is more accurate or more cautious? The answer is: they're using different data. Each scanner is capturing a different slice of vulnerability information. None of them are complete. The NVD vulnerability count is incomplete because vendors publish without CVE IDs. Snyk's count is high because it's conservative. Trivy's count is low because it only uses NVD. To actually understand vulnerability risk, you need multiple scanners. A vulnerability that appears in all three scanners is likely critical and widespread. A vulnerability that appears in only one scanner might be a false positive or a very specific case. ## The Visibility Gap: What Scanners Cannot See A scanner looks at a container image. It sees packages and versions. It matches them against CVE databases. It reports findings. But this process is fundamentally limited. **Scanners cannot determine code reachability.** An image contains 5,000 packages. One package has a critical CVE in a function that handles image encoding. Your application never processes images. The vulnerable code is unreachable. A scanner can't tell this. It flags the vulnerability anyway. This is why VEX (Vulnerability Exploitability eXchange) was created — to provide manual attestations of whether a vulnerability is actually reachable in your specific context. But VEX requires manual work for thousands of vulnerabilities, and most organizations don't maintain it. **Scanners cannot see how the code is called.** A vulnerability might exist only when specific conditions are met:- User input of a specific type triggers it- A race condition must occur- Specific compiler flags enable the vulnerability- A feature must be enabled in configuration A scanner sees the vulnerable package version. It doesn't see whether the conditions that trigger the vulnerability are present in your execution context. **Scanners cannot see supply chain attacks.** If a package is compromised at the source (maintainer account takeover, build system compromise, etc.), the package version is correct, but the code is malicious. Scanners won't detect this unless the malicious code has a known signature (after the attack is publicly exposed). **Scanners cannot see runtime behavior.** The vulnerability might exist in code that your container never executes. Your application loads a library for backward compatibility, but never calls it. A scanner sees the library and its CVEs. It can't tell whether the application calls it. **Scanners cannot see kernel interaction.** A vulnerability in a user-space library might be exploitable only if the container has certain capabilities, runs as root, or has access to specific device files. A scanner sees the vulnerable library. It doesn't see the container's runtime configuration. ## The False Negative Problem: What Scanners Miss Entirely Beyond things scanners can't determine, there are entire categories of vulnerabilities scanners don't even look for: - **Configuration vulnerabilities**: The container runs as root, but this isn't a CVE. It's a security misconfiguration.- **Architecture vulnerabilities**: The container stores secrets in environment variables instead of mounted secrets. Not a CVE.- **Process vulnerabilities**: The container includes a shell for post-exploitation, but it's not a CVE by itself.- **Behavioral vulnerabilities**: The container makes unexpected network connections or file access patterns that indicate compromise. Not a CVE. These vulnerabilities exist at Layers 2 and 4 primarily. Scanners focus on Layer 3 (package vulnerabilities). They miss the execution context entirely. This is why a container can "pass" a scan (no high-severity CVEs) and still be exploitable (runs as root, contains shells, has writable filesystems). ## The Timeline Problem: Scanning Arrives Too Late Scanning is a detection mechanism, not a prevention mechanism. By the time a vulnerability is detected at Layer 3 (registry), the code has already been written, tested, merged, built, and pushed. **Timeline of Container Security** **Days 1-7**: Developer writes code, adds dependency, commits to main- Status: Vulnerable code in repository- Detection: None (code hasn't been scanned yet) **Days 7-14**: Code review, automated tests pass, PR merged- Status: Vulnerable code in main branch- Detection: SCA scanner may flag during PR (if enabled) **Days 14-21**: CI/CD builds image, pushes to registry- Status: Vulnerable image in registry- Detection: Registry scanner flags immediately **Days 21-30**: Image waits for deployment approval or sits in registry- Status: Vulnerable image deployed or waiting to deploy- Detection: Vulnerability flagged, policy blocks deployment (maybe) **Days 30-60**: Deployment happens or image is overridden- Status: Vulnerable container runs in production- Detection: Runtime monitoring or manual discovery At each step, you have choices: If the vulnerability is flagged at Layer 1 (during code review), you can reject the code before it's merged. This is the cheapest place to stop it. If it's flagged at Layer 2 (during build), you can block the build. This prevents the image from being created. If it's flagged at Layer 3 (during registry scan), you can prevent deployment. But if the image is already deployed, you have to roll it back or accept the risk. If it's flagged at Layer 4 (at runtime), you've already failed. The code is running and potentially exploitable. Most organizations don't have Layer 1 detection (it requires integrating SCA into the development workflow). So vulnerabilities are caught at Layer 3 after they've already traveled through Layers 1 and 2. ## How Vulnerabilities Escape: The Patch Lag Problem A CVE is published on Monday. Your organization becomes aware on Wednesday. A patch is released Thursday. Your CI/CD picks up the patch Friday. A new image is built Saturday. The rollout begins Sunday. During this entire week, your production containers are running vulnerable code. But this timeline assumes optimal conditions. In reality: - **Awareness lag**: You might not see the CVE announcement until Friday or later- **Assessment lag**: Your team needs to assess impact (are we affected? how bad is it?)- **Patch availability lag**: The maintainer might not have patched yet- **Dependency lag**: Your dependency might not pull the latest version automatically- **Build lag**: Your CI/CD might take 6+ hours to complete a full rebuild- **Testing lag**: You might need to test the patched version before deploying- **Deployment lag**: Rolling out across 50+ services takes days- **Awareness of running vulnerable code**: You might not know old pods are still running the vulnerable version The total lag is often 30-90 days. During this time, if an exploit is published, your systems are under attack. ## Patch Propagation Failure Modes In the real world, patches don't always propagate cleanly: **Broken patch**: The maintainer releases a patch that introduces a new bug. You can't deploy it. You wait for a second patch. **Incompatible patch**: The patch requires a new version of a dependency you can't upgrade. You're stuck. **Regression patch**: The patch fixes the CVE but breaks an API your application depends on. You have to choose between security and stability. **Missing patch**: For some old libraries, patches never come. The maintainer abandoned the project. You have to fork the library, apply the patch yourself, and maintain your own version. **Transitive patch failure**: The maintainer patches, but it takes 2 weeks for your dependency to update its sub-dependency. You're waiting on a chain of releases. When patches fail to propagate, you either accept the vulnerability or take matters into your own hands. Accepting means running known-vulnerable code. Taking matters into your own hands means forking the library, patching it yourself, and maintaining that fork indefinitely. ## The Upstream Patch Response Pattern Not all vulnerabilities are created equal. Response varies dramatically: **Critical vulnerabilities** (like Log4j, xz/liblzma) that affect billions of devices:- Patch released within days- All downstream propagate within weeks- Most users patched within months **High vulnerabilities** in widely-used libraries:- Patch released within weeks- Downstream propagate within weeks- Most users patched within 2-3 months **High vulnerabilities** in specialized libraries:- Patch released within weeks- Downstream propagate over months- Most users not patched for 6+ months **Medium vulnerabilities** in any library:- Patch released when maintainer has time- Downstream propagate slowly- Many users never patch **Low vulnerabilities** in maintenance-mode libraries:- Patch might never come- Library users live with the vulnerability- Some fork and maintain patches themselves The pattern is clear: patching is not equally effective across the ecosystem. Libraries that are actively maintained and widely-used get patched fast. Libraries that are mature, stable, and niche get patched slowly or not at all. Yet both are in your dependency tree. ## Vulnerability Interdependencies: Chaining Exploits A single vulnerability is less dangerous than a chain of vulnerabilities that allow escalating privilege or access. Example exploit chain:- **CVE-1** (Layer 2): Non-hermetic build allows base image to be swapped- **CVE-2** (Layer 3): Container image contains unverified binary downloaded from compromised source- **CVE-3** (Layer 4): Container runs as root, allowing privilege escalation- **Result**: Attacker has compromised the build, injected malicious code, and gained root access An attacker doesn't need a single critical vulnerability. They need a sequence of weaknesses:- Write access to source code (supply chain compromise)- Execution context in build (ability to run code during build)- Persistence mechanism in image (ability to survive deployment)- Privilege escalation in runtime (ability to break out or move laterally) When security is strong at Layer 3 (registry scanning) but weak at Layers 1, 2, and 4, an attacker focuses on those weak layers. Most breaches don't exploit the latest CVE. They exploit architectural weaknesses that have existed for years. ## The Only Real Fix: Patch at the Source Vulnerability remediation at Layers 3 and 4 is damage control. The only lasting fix is patching at Layer 1: updating dependencies in your source code. But Layer 1 patching has constraints:- You can only patch what the maintainer has fixed- You have to test patches before deploying- Patches sometimes break backward compatibility- Patches take time to propagate When maintenance is fast (critical libraries, active projects), patching works. When maintenance is slow (niche libraries, unmaintained projects), you're stuck. The only alternatives are:1. **Fork and patch**: Maintain your own patched version2. **Migrate**: Switch to a different library that's maintained3. **Accept and monitor**: Run the vulnerable code and watch for exploitation4. **Remove the feature**: Stop using the vulnerable component entirely Most organizations choose option 3 (accept and monitor) because options 1, 2, and 4 require significant effort. This means vulnerabilities persist in Layer 1, travel through all four layers, and are running in production indefinitely. True security requires treating vulnerabilities as a Layer 1 problem. Layer 3 scanning detects that you have the problem. Layer 4 monitoring detects exploitation. But only Layer 1 action (patching, forking, migrating, or removing) solves it.