Your Python application depends on a library called requests. That library depends on another library called urllib3. That depends on something else. Somewhere deep in the chain, there's a vulnerability in a library you've never heard of. Your application is now vulnerable because of a transitive dependency.
This cascading dependency chain—and how it manifests in container images—is the foundation for understanding container vulnerability management. It also explains why a single CVE in a low-level library can affect thousands of applications.
graph TD App["Application<br/>Your Code"] App -->|Direct| Requests["requests<br/>HTTP library"] Requests -->|Direct| urllib3["urllib3<br/>URL parsing"] urllib3 -->|Direct| ssl["libssl<br/>TLS/Crypto"] App -->|Direct| Django["Django<br/>Web framework"] Django -->|Direct| sqlparse["sqlparse<br/>SQL parsing"] sqlparse -->|Transitive| Lib["libc<br/>C Standard<br/>Library"] ssl -->|Transitive| Lib Lib --> Vuln["CVE in libc<br/>Affects all<br/>dependent code"] style App fill:#fff9c4 style Vuln fill:#ffccccWhat Is a Library?
A library is reusable, compiled code that provides functionality to applications.
Examples include libssl for TLS/SSL encryption (used by curl, Apache, nginx), libz for data compression (used by curl, nginx, browsers), libc as the C standard library used by almost every program, libcrypto for cryptographic functions (used by TLS libraries, databases, etc.), and libpthread for multithreading (used by many servers and applications).
When you use a library in your code, you're linking to it: your application's binary contains references to functions inside the library.
Example C code:
#include <openssl/ssl.h> // Include SSL library header int main() { SSL_CTX *ctx = SSL_CTX_new(TLS_method()); // Call function from library return 0;}When compiled, this creates a binary that calls functions in libssl. If libssl isn't installed on the system, the binary won't run.
Static vs Dynamic Linking: Trade-Offs for Containers
When compiling code that uses a library, you can link statically or dynamically.
Static Linking
Static linking bundles the library code directly into the binary.
gcc -static myapp.c -lssl -o myapp# Result: myapp binary contains both myapp code AND all of libssl codePros of static linking include having a single binary with no external dependencies needed, being portable and working on any system without library version issues, and providing fast execution with no runtime library resolution. Cons of static linking include creating a large binary where instead of having a 1 MB app plus a separate 2 MB libssl, you get a 3+ MB app binary, making it hard to patch since a vulnerability in libssl requires recompiling the entire binary, and causing duplication where multiple apps that statically link the same library have that library duplicated in memory.
In containers: Statically linked binaries are popular because you can use a completely empty base image (scratch). Statically linked binary with no external dependencies works in an empty container (scratch base image), which is extremely secure and efficient.
Dynamic Linking
Dynamic linking puts library code in separate files (.so files on Linux) that are loaded at runtime.
gcc myapp.c -lssl -o myapp# Result: myapp binary is small (~100 KB), contains references to libssl# At runtime: system must provide libssl.soPros of dynamic linking include having a small binary where only application code is compiled in, sharing memory so one copy of a library is loaded and used by multiple apps, and enabling easy patching where you update the library file and all apps using it automatically get the update. Cons of dynamic linking include having external dependencies where the binary won't run without libraries installed, potential library version mismatches where libssl.so has a different API than expected and the binary breaks, and runtime overhead where the linker must resolve library references at startup.
In containers: Dynamic linking is more common since you can include libraries in the container image. The container includes both the binary and the library. When the binary starts, the dynamic linker finds libssl and loads it.
Shared Libraries and the Dynamic Linker
Linux systems use a dynamic linker (ld-linux.so) to load libraries at runtime.
When you execute a binary, the OS doesn't run the binary directly. It runs the dynamic linker first, which reads the binary's dynamic link section (ELF headers), identifies required libraries such as libssl.so.3, libcrypto.so.3, libc.so.6, searches for those libraries in standard locations including /lib, /usr/lib, /lib64, loads them into memory, resolves symbols by replacing wherever the binary says "call SSL_new()" with the address in libssl, and finally hands control to the binary's main() function.
This process is called relocation.
Example: Inspecting a binary's library dependencies:
ldd /usr/bin/curl linux-vdso.so.1 (0x00007ffc8a4c2000) libcurl.so.4 => /usr/lib/libcurl.so.4 (0x00007f1234567000) libssl.so.3 => /usr/lib/libssl.so.3 (0x00007f1234556000) libcrypto.so.3 => /usr/lib/libcrypto.so.3 (0x00007f1234400000) libc.so.6 => /lib64/libc.so.6 (0x00007f1234200000) /lib64/ld-linux-x86-64.so.2 (0x00007ffc8a4d5000)This shows that curl depends on libcurl.so.4, libcurl depends on libssl.so.3, libcrypto.so.3, and libc.so.6, and all are found and loaded at specific memory addresses.
If any of these libraries is missing or the wrong version, the binary won't run.
The Dependency Chain: How Vulnerabilities Cascade
Your application depends on libraries. Those libraries depend on other libraries. This creates a chain where a vulnerability deep in the chain affects everything that depends on it.
Example dependency tree: Your web application depends on the Django Framework. Django in turn depends on Python 3.11, which depends on glibc 2.36, which depends on linux-vdso (the kernel interface). Django also depends on OpenSSL 3.0, which depends on glibc 2.36. Finally, Django depends on zlib 1.2.13, which depends on glibc 2.36. Notice that glibc 2.36 is a shared dependency across multiple components.
A single CVE in glibc 2.36 affects Python 3.11, OpenSSL 3.0, zlib 1.2.13, Django, your application, and literally every other application in a Linux container using that glibc version.
This is why a seemingly "low-level" vulnerability can affect thousands of applications.
Real-world example: A 2023 vulnerability in glibc's DNS resolver (getaddrinfo) affected every application that resolved DNS. That's virtually all networked applications.
Transitive Dependencies
"Transitive dependency" means a dependency of a dependency of a dependency.
Your app → depends on requests → depends on urllib3 → depends on certifi → depends on ... (which libraries?)You directly depend on requests. You have no direct relationship with urllib3, certifi, or anything deeper. But if there's a vulnerability in certifi, your app is vulnerable because of the transitive chain.
Managing transitive dependencies is hard because you don't control their versions (the maintainer of requests chooses which version of urllib3 to use), different versions of your library might choose different versions of their dependencies, and a library upgrade might pull in new transitive dependencies you never expected.
Example: Upgrading Django from 3.2 to 4.0 might pull in 5 new packages you don't know about.
Diamond Dependencies
Sometimes the same library appears multiple places in the dependency tree. For example, your app depends on lib-a (which depends on lib-c version 1.5) and lib-b (which depends on lib-c version 1.8).
Your app needs lib-c, but lib-a wants version 1.5 and lib-b wants version 1.8. This is the "diamond dependency problem."
Package managers solve this by finding a version that satisfies both constraints (for example, 1.8 is newer than 1.5, so they use 1.8). If no compatible version exists, they fail and ask the user to resolve the conflict. They also provide warnings to inform the user which versions were chosen.
In containers, you usually have one Python environment, one Node.js installation, etc. So diamond dependencies must be resolved (one version must be chosen).
Packages Bundle Libraries
A Linux package is not just a single library. It's a collection of binaries and libraries packaged together.
For example, the curl package includes /usr/bin/curl (the command-line tool), /usr/lib/libcurl.so.4 (the library that programs link to), and /usr/share/man/man1/curl.1 (documentation).
When you install the curl package, you get both the binary and the library. If an application wants to use libcurl, it depends on the curl package being installed.
Packages declare dependencies in their metadata:
Package: nginxVersion: 1.24.0-r1Depends: libpcre3, openssl, zlibThis tells the package manager: "To install nginx, you also need libpcre3, openssl, and zlib installed."
When you run apk add nginx, the package manager reads that nginx depends on libpcre3, openssl, and zlib. It checks if these are installed, and if not, adds them to the install list. It then installs everything in dependency order: zlib, then openssl, then libpcre3, then nginx.
How a CVE in a Library Affects Your Application
A vulnerability is discovered in OpenSSL 3.0.7. It affects TLS connection handling.
Here is how the process unfolds. First, the vulnerability is announced: OpenSSL 3.0.7 has CVE-2024-1234 (CVSS 7.5 High). Next, packages depending on OpenSSL become vulnerable, including libcurl, nginx, and your Python TLS stack—all are now vulnerable. Then, distributions release patched packages: OpenSSL 3.0.8 (security update) is released, libcurl 8.1.1 (rebuild with new OpenSSL) is released, and nginx 1.24.0-r2 (rebuild with new OpenSSL) is released. Your application remains vulnerable until you rebuild—the old Dockerfile has openssl=3.0.7 but the new one has openssl=3.0.8. Finally, the new image is built with patched OpenSSL: the container image now contains OpenSSL 3.0.8 and all libraries using OpenSSL are automatically patched.
The key: You don't patch "libcurl" or "nginx" directly. You patch OpenSSL. Anything that depends on OpenSSL automatically gets the fix when you rebuild.
Packages and Shared Libraries: The Linux Model
This is the fundamental design of Linux package systems: multiple applications share the same library in memory. For example, /bin/curl uses libcurl, /usr/bin/wget uses libcurl, and /usr/bin/python uses libssl. All these applications point to shared system libraries: /usr/lib/libcurl.so.4, /usr/lib/libssl.so.3, /usr/lib/libcrypto.so.3, and /lib/libc.so.6.
When the library is patched, you update the .so file at /usr/lib/libcurl.so.4, all applications using it automatically use the patched version, and there's no need to recompile or redeploy applications.
In containers, the model is slightly different: library updates require rebuilding the image (because the image is immutable). But the principle is the same.
Inspecting Library Dependencies in Containers
How do you know what libraries are in your container image?
List Installed Packages
You can list installed packages using apk info on Alpine, dpkg -l on Debian/Ubuntu, or rpm -qa on RHEL/CentOS. These commands show packages, but to see the libraries inside those packages, you need additional commands:
# Alpine: Show files in packageapk info -L nginx # Debian: Show files in packagedpkg -L nginx # Generic: Inspect containerdocker run myimage:latest apk info -L nginxdocker run myimage:latest dpkg -L nginxTrace Library Dependencies
For a specific binary, show what libraries it depends on:
ldd /usr/bin/curlThis is the most direct way to understand what libraries are actually used.
Scan for Vulnerable Libraries
Container vulnerability scanners list all packages, cross-reference against CVE databases, and report vulnerabilities:
trivy image myimage:latestUnder the hood, the scanning process works as follows: first, it extracts the package list using apk info or dpkg -l. Then, for each package, it gets the version. Next, it queries the CVE database asking "Is libssl 3.0.7 vulnerable?" Finally, it reports the findings, for example "CVE-2024-1234 found in openssl 3.0.7".
Version Numbers and Compatibility
Libraries use version numbers to indicate compatibility.
Semantic Versioning
Most libraries use semantic versioning in the format MAJOR.MINOR.PATCH. Version openssl-3.0.7 is structured as follows: 3 is the major version indicating big changes where breaking changes are possible, 0 is the minor version for new features and is usually backward compatible, and 7 is the patch version for bug fixes and security patches. When the MAJOR version changes, code depending on the library might break because the API might change. When the MINOR version changes, it's usually backward compatible so new features are added but old code still works. When the PATCH version changes, it's backward compatible because only bug fixes and security patches are applied.
API Stability
The "API" (Application Programming Interface) is the set of functions a library provides.
Example: OpenSSL provides functions like:
SSL_CTX_new()SSL_CTX_free()SSL_do_handshake()...If OpenSSL 4.0 removes SSL_do_handshake(), code using that function breaks. This is a breaking change (MAJOR version bump).
If OpenSSL 3.1 adds a new function but doesn't remove any, code continues to work (MINOR version bump).
Soname Versioning
On Linux, the actual filename includes the version: /usr/lib/libssl.so.3 is the actual file, /usr/lib/libssl.so.3.0 is a symlink for the minor version 0, and /usr/lib/libssl.so is a generic symlink that is often outdated. The soname is what applications link against. When you compile curl, it links against libssl.so.3. When you run curl, the dynamic linker finds libssl.so.3 and loads it. If OpenSSL releases a PATCH update from 3.0.7 to 3.0.8, the soname stays libssl.so.3, making it a drop-in replacement that is backward compatible. However, if OpenSSL releases a MAJOR update from 3.x to 4.0 with breaking changes, the soname changes to libssl.so.4. Old binaries continue to look for libssl.so.3, which still exists as the old version, so they continue to work. New binaries can use the new libssl.so.4.
This allows multiple versions to coexist on the same system, though it's rare in containers (images usually have only one version of each library).
The Container Difference: Isolated Dependencies
In traditional systems, all applications share the same libraries from the OS package manager. In containers, each image has its own copy of libraries, providing isolation. In a traditional system, all applications share system-wide libraries like /usr/lib/libssl.so.3 and /usr/lib/libcurl.so.4, and all applications link to these same system libraries. In contrast, containers isolate dependencies: Container 1 (Image 1) has /usr/lib/libssl 3.0.7 and myapp v1, Container 2 (Image 2) has /usr/lib/libssl 3.0.8 (patched) and myapp v2, and Container 3 (Image 3) has /usr/lib/libssl 3.1.0 and a different app. Containers allow different images to use different versions of the same library without conflict. This is powerful but also means you must patch each image independently.
If Image 1 has a vulnerable libssl, patching Image 2 doesn't help. You must rebuild Image 1 specifically.
Dependency Management in Container Builds
Package managers in containers work just like in traditional systems, but the workflow is different.
Poetry/pipenv (Python)
FROM python:3.11-slimCOPY pyproject.toml poetry.lock /app/WORKDIR /appRUN pip install poetry && poetry install --no-devCOPY . /app/ENTRYPOINT ["python", "/app/main.py"]Poetry resolves Python dependencies. The poetry.lock file lists exact versions:
[[package]]name = "requests"version = "2.31.0"dependencies = {urllib3 = ">=1.21.1,<3", certifi = ">=2017.4.17"}npm/package-lock.json (Node.js)
FROM node:18-alpineCOPY package.json package-lock.json /app/WORKDIR /appRUN npm ci # Install exact versions from lock fileCOPY . /app/ENTRYPOINT ["node", "/app/index.js"]The lock file pins exact versions of every transitive dependency.
Go modules
FROM golang:1.21 as builderCOPY go.mod go.sum /app/WORKDIR /appRUN go build # Automatically uses versions from go.sumAPK/apt/dnf
FROM alpine:3.18RUN apk add --no-cache \ python3=3.11.6-r0 \ curl=8.1.0-r0 \ nginx=1.24.0-r1Always pin exact versions. Never use apk add python3 (floating version).
Dependency Conflicts and Resolution
Sometimes dependencies conflict when two libraries need incompatible versions of the same dependency. For example, a project depends on both lib-a (which wants lib-c version 1.0.x) and lib-b (which wants lib-c version 2.0.x). Package managers handle this by trying to find a compatible version—though 1.5 might be newer than 1.0.x, it won't work because 2.x has breaking changes. They also provide warnings to tell the user which versions are being used. If no solution exists, they fail and ask the user to resolve the conflict.
In containers, conflicts are usually resolved by accepting one version and hoping it works. Some applications might fail, and you need to update the conflicting dependencies to resolve the issue.
Symbol Conflicts
When two libraries define the same symbol (function or variable), the linker must choose which one to use.
Example: Both lib-a and lib-b define crypto_hash():
// lib-avoid crypto_hash() { ... } // lib-bvoid crypto_hash() { ... } // Your appcrypto_hash(); // Which one?The linker uses symbol resolution rules (typically first one wins, or a specific version if tagged).
This is rare in practice because libraries use namespacing:
liba_crypto_hash()libb_crypto_hash()Explicitly named functions avoid conflicts.
Summary: Dependencies Flow Through Layers
Your application's security depends on every library in the dependency chain. Remember that your application security is only as strong as its weakest dependency. Understanding the chain is critical: it flows from your code to application libraries to system libraries to the OS kernel. A vulnerability at any layer affects everything above it. Patching requires updating the vulnerable library and rebuilding everything that depends on it, and in containers, this means rebuilding the image.
Vulnerability-scanning tools exist because dependency chains are complex with 5–50+ transitive dependencies being common, vulnerabilities are discovered constantly with multiple per day, and manual tracking is impossible.
Automation that scans, detects vulnerabilities, and rebuilds images is not optional for serious container deployments. It's the only way to keep up with the rate of vulnerability discovery.
