NewApp Monitoring is liveSign up free
AgentbeamBeta
Course outline

Learn / Level 3 / lesson 9

Log4Shell and Spring4Shell

Two famous flaws in libraries most victims never chose: a logger that interpreted what it logged, and a framework that bound request fields too freely. What they teach about dependencies and how to respond.

10 min read · Yash Thakker · updated 2026-10-24

Log4Shell · Spring4Shell · dependencies · CVE-2021-44228

A stack of library blocks inside an application with one deep block highlighted as vulnerable

What you will learn

  • Explain how a logging library could be made to fetch and run remote code
  • Explain how data binding exposed internal objects in Spring
  • Describe why transitive dependencies make exposure hard to see
  • Plan a first-day response to a library vulnerability

Before this: File upload flaws, Software, versions and updates

Every earlier lesson in this level assumed the vulnerable code was yours. This lesson is about code you never wrote and might not know you ran. In December 2021 and April 2022, two flaws in widely used Java libraries gave the world a lesson in what that costs. We will take them apart using only what the public records say, then ask what we can do before the next one.

Libraries you did not choose

Modern software is mostly other people's code. A project lists the libraries it needs, called its dependencies. Those libraries have dependencies of their own, and so on. Libraries you did not list directly, but which arrive because something you chose needs them, are called .

A dependency tree: an application at the top, libraries below, and one deep library marked as vulnerable even though the application never named it

The tree can be deep. A flaw in a library at the bottom is a flaw in every program above it, whether or not its authors ever heard of that library. Keep that picture in mind for both cases.

Log4Shell: a logger that interpreted its input

Almost every program writes a log, a running record of what happened, as you saw in reading logs. Many Java programs used Apache Log4j2 to do it. Programs typically log text they receive: the name someone typed, the browser's User-Agent header, a failed username.

Log4j2 had a feature that let a log message contain a special expression which the library would evaluate while writing the line, a "lookup", substituting in something fetched from elsewhere. Some of those lookups used a naming service interface called JNDI, which can reach out over the network, including to servers named in the expression.

Put those together, and you have the pattern of this whole level. Per the record for , these features "do not protect against attacker controlled LDAP and other JNDI related endpoints", and an attacker who can control log messages or their parameters can execute arbitrary code loaded from an LDAP server when message lookup substitution is enabled. The library treated the text it was asked to record as instructions to carry out.

Step by step: text containing a lookup expression is logged, the logger evaluates it, the lookup reaches out to a remote server, and code that comes back runs inside the application

Why was the reach so large? Because logging is everywhere and logs record outside input as a matter of routine. Any field a visitor could influence, in any request, that reached a logger was a way in. The trust boundary was in a place nobody looked: the logging call.

Two details from the record deserve your attention. First, the fix is not "filter the input". The vendor first turned the feature off by default and then removed it, which is the strongest kind of fix: the dangerous capability no longer exists. Second, the record describes two steps: disabled by default in 2.15.0, removed in 2.16.0. Fixing a library flaw can be a sequence of releases, so read the advisory for the final fixed version, not just the first.

Spring4Shell: binding too freely

The Spring Framework is a very common base for Java web applications. One of its conveniences is : it takes the fields in a request and copies them into the properties of an object automatically, so developers do not write the copying by hand.

The risk of binding is exposure. A request can try to set any property the object offers, and objects can contain other objects, reachable through a chain of property names. If some of those internal objects were never meant to be set from outside, the framework must stop it. The record for says a Spring MVC or WebFlux application running on JDK 9 or later may be vulnerable to remote code execution via data binding, that the specific exploit requires Tomcat as a WAR deployment, and that an application deployed as the default Spring Boot executable jar was not vulnerable to that exploit. It adds that the nature of the vulnerability is more general.

Step by step: request fields are bound to an application object, one field reaches through a chain of properties into an internal object, and the changed internals lead to code execution

Notice how conditional it was. It needed a Java version, a server, and a packaging style to line up. That is typical of real vulnerabilities and it matters for response: "are we affected?" is a question about your configuration, and you can only answer it if you know your versions and deployment. Still, the record cautions that the flaw was more general than one exploit. Waiting to prove exploitability is a bet; patching is cheaper.

What the two have in common

QuestionLog4ShellSpring4Shell
Where did untrusted input go?Into a log messageInto request fields bound to objects
What interpreted it?A lookup feature in the loggerA binding feature in the framework
Who wrote the vulnerable code?A library the victim usually did not choose directlyA framework the victim chose, but not the dangerous part
What did it hand over?Code execution on the serverCode execution on the server

Both are the same mistake you have seen all level, input read as instructions, living one layer down, in code that developers trusted by default.

What to do about it

You cannot review every line of every dependency. You can prepare.

  1. Keep an inventory. A software bill of materials () lists every component and version in a build, including transitive ones. When an advisory lands, searching it answers "are we affected?" in minutes instead of days.
  2. Know your versions and your deployment, since exposure depends on both. Software, versions and updates covered why.
  3. Patch to the final fixed release, and keep a fast path for emergency updates.
  4. Reduce what a compromise can do. Run services with least privilege, restrict outbound network access (so a fetched remote payload cannot be fetched), and keep secrets out of reach. Outbound filtering would have blunted any flaw whose first step was "reach out to a remote server".
  5. Log and watch. After a disclosure, search logs for the vulnerable pattern, and watch for unexpected outbound connections from application servers.
Why could a company be exposed to Log4Shell without ever choosing Log4j?

Another library or product it used could include Log4j as a dependency, a transitive one. Without an inventory including indirect components, the company would not know the library was present.

Spotting it in a code review

  • Where does outside input reach a logger, and does any logging configuration interpret what it logs?
  • Do binding features copy request fields straight into objects? Is there an allowlist of fields that may be bound?
  • Is the dependency tree reviewed and pinned? Are indirect dependencies visible, and is there a scanner on the build?
  • Which parts of the stack can reach out to the network, and do they need to?

How AI changes this

Attackers. After a disclosure the race is between those who patch and those who scan. With Log4Shell, the record shows listing as known exploited on the day of publication. Today, AI makes the attacker's part of the race shorter: models can read an advisory and a patch diff and produce a scanner or an exploit adaptation for variants, and they can help find which products embed the library. Beam's October 2026 attack roundup describes AI-found bugs being attacked before patches shipped, as reported there. The time between disclosure and exploitation is the number to worry about.

Defenders. AI helps with the work that made these responses slow: mapping an advisory onto your inventory, finding which of a hundred services embeds a version range, drafting the upgrade and tests. Pair it with a real SBOM, so the model reasons over facts, not guesses. Beam's guide to checking dependencies against known CVEs in CI covers one practical setup.

AI agents. Agents are heavy consumers of dependencies. They suggest and install packages, and they run tools whose own dependencies are invisible to the user. An agent's environment is a dependency tree with the added risk that the agent picks the packages. Beam's supply-chain work covers (packages with plausible names a model invents) and malicious packages. Agent frameworks and MCP servers are software with the same classes of flaw as these two: see the Langflow MCP stdio record for a tool in this ecosystem where a configured command reached a shell. The lesson is the same: inventory what the agent runs, patch it, and restrict its outbound access.

Try it

Two short exercises on your own machine, both read-only.

1. See interpretation of logged text, safely. Save as logger_lab.py:

import os

def risky_log(message):
    # a toy logger that "helpfully" expands ${env:NAME} while writing
    out = message
    while "${env:" in out:
        start = out.index("${env:")
        end = out.index("}", start)
        name = out[start + 6:end]
        out = out[:start] + os.environ.get(name, "?") + out[end + 1:]
    print("RISKY:", out)

def plain_log(message):
    print("PLAIN:", message)

text = "user typed: ${env:HOME}"
risky_log(text)
plain_log(text)

Run python3 logger_lab.py. The risky logger replaced part of the user's text with a value from your machine. The text decided what the logger did, which is the Log4Shell shape on a toy scale. The plain logger just records the text.

2. Look at your own dependency tree. If you use Python, run pip list and then pip show requests (or any package you have) and read the Requires: and Required-by: lines. If you use Node, npm ls --all in a project folder prints the tree. Count how many packages you never chose by name.

Check yourself

What is the shared root cause of Log4Shell and Spring4Shell, in one sentence?

Untrusted input was interpreted as instructions by a feature (log lookups, request-to-object binding) in a library that developers trusted by default.

Why does Spring4Shell show that 'are we affected?' depends on configuration?

The record says the known exploit needed JDK 9 or later and a Tomcat WAR deployment, while a default Spring Boot executable jar was not affected by it. You can only answer if you know your Java version, server and packaging.

Name three things you can prepare before the next library vulnerability.

Keep an SBOM that includes transitive dependencies, know your versions and deployment, keep a fast patching path, restrict outbound network access and privileges, and monitor logs and connections after disclosures.

Next

You have now seen the main web bug classes and the single idea that links them. In the next level we go below the application, to how memory works and how bugs in it let programs be taken over.