To defend against Log4Shell well, it helps to understand how it worked at a conceptual level: not to reproduce it, but to see exactly where the chain can be broken. This article walks through the mechanism of CVE-2021-44228 as a sequence of links, explaining what each step did and which defensive control severs it. It deliberately stays at the conceptual level and provides no working exploit, no server setup, and no weaponization steps. The goal is understanding that translates directly into better detection and remediation.
If you want to act while you read, the Log4Shell scanner shows you which of your systems still have the vulnerable behaviour.
The Foundation: Message Lookups
Log4j included a convenience feature called message lookups. When it formatted a log message, it scanned the text for tokens written as ${...} and substituted them with dynamic values such as an environment variable or a system property. This expansion happened on the message content itself, which is the crucial design decision. If any part of that message came from untrusted input, the expansion logic would run over attacker-influenced text. This is the crucial difference between a logging library that merely records text and one that interprets it: interpretation is precisely the capability an attacker wants to reach, and logging gave them a path to it through perfectly ordinary application behaviour. For background on the overall flaw, see what is Log4Shell.
The Dangerous Capability: JNDI
One of the lookup mechanisms Log4j supported was JNDI, the Java Naming and Directory Interface. JNDI is a general-purpose API for resolving names against directory services such as LDAP and RMI. Critically, JNDI in older Java configurations could do more than return data: it could resolve a name to a remote object reference and, under permissive defaults, cause the application to load and instantiate a class identified by that remote service. This object-loading capability is the link that turned a lookup into code execution. It is worth stressing that this behaviour depended on permissive defaults in older Java environments; the same capability is exactly why later Log4j releases removed JNDI support by default and why hardened runtimes restrict remote object loading.
The Chain, Step by Step
Putting the pieces together, the conceptual chain looked like this. Each step is described so you can see where it breaks, not so it can be reproduced:
- Untrusted input arrives. A request carries text containing a JNDI lookup token in a field the application logs, such as a header or a username.
- The application logs it. The vulnerable Log4j version writes that field to a log, passing the attacker-influenced text through its formatter.
- Lookups expand. Log4j's message-lookup logic encounters the
${...}token and processes it, including any nested obfuscation that reconstructs the keyword. - JNDI resolves a remote name. The expanded token directs JNDI to contact a remote service named in the token.
- A remote object is loaded. Under permissive defaults, the resolution returns a reference that causes the JVM to fetch and instantiate a remote class, resulting in code execution on the server.
The important defensive insight is that this is a chain, and breaking any single link prevents the outcome.
Where Each Link Breaks
Mapping defences onto the chain turns understanding into action. Each control below severs a specific link:
- Remove the JNDI lookup code (link 3 and 4). Deleting JndiLookup.class or upgrading removes the path that turns a lookup into a JNDI resolution. This is the most decisive break and the basis of both patching and the strongest stopgap in mitigation without upgrading.
- Disable message lookups (link 3). Stopping the expansion of
${...}tokens prevents the token from ever being processed, which is what the formatMsgNoLookups setting does on certain versions. - Block the outbound callback (link 4 and 5). Egress filtering that blocks outbound LDAP, RMI, and unusual DNS stops the server reaching the remote service even if a lookup fires.
- Filter inbound payloads (link 1). WAF rules can reduce the volume of tokens that arrive, though obfuscation means this is only a speed bump, as explained in obfuscated JNDI payloads.
Why Upgrading Is the Cleanest Break
Among all these, removing the vulnerable code, by upgrading to 2.17.1 or the appropriate backport, is the cleanest because it severs the chain at its core regardless of configuration, encoding, or obfuscation. The newer versions disabled message lookups and removed JNDI support by default, so the dangerous links simply no longer exist. The other controls are valuable defence in depth, but they each protect one link and can be bypassed if that link is reachable another way. The full upgrade procedure is in how to patch Log4j.
Why the Same Pattern Recurs
Understanding the chain also helps you recognise its relatives. The root pattern, untrusted data flowing into a powerful evaluation feature, recurs across software in the form of template injection, expression-language injection, and unsafe deserialization. JNDI-based loading in particular had been a known risky pattern before Log4Shell brought it to mass attention. For defenders, the durable lesson is to be suspicious whenever attacker-controlled text can reach a feature that resolves names, evaluates expressions, or loads code. That instinct generalises far beyond this one library.
Turning the Mechanism Into Detection
Knowing the chain sharpens detection too. Because the malicious data flows through logging, evidence of attempts lands in logs, and because exploitation needs an outbound callback, success leaves a network footprint. That is exactly why the methodology in detecting Log4Shell in logs pairs a log search for JNDI tokens with a hunt for unexpected outbound connections. Understanding the mechanism is what makes those two signals meaningful rather than arbitrary.
From Concept to Confirmation
Once you understand which links exist on a given host, confirm the reality with tooling rather than assumption. Use the version checker to establish whether the Log4j version still contains the vulnerable code path, and the scanner to confirm whether the live service still exhibits the lookup behaviour. Together they tell you whether the chain is intact or already broken on each system, which is the practical question all this conceptual understanding is meant to answer.
Defence in Depth Around the Chain
Because the attack is a chain, a layered defence is far stronger than any single control. Removing the vulnerable code is the decisive break, but pairing it with egress filtering, disabled lookups where applicable, and inbound filtering means that even an unexpected gap in one layer does not automatically lead to compromise. This defence-in-depth mindset is valuable precisely because real estates are messy: a forgotten bundled JAR, a misconfigured property, or an encoding trick can defeat any one measure. By severing multiple links at once, you ensure that the failure of a single control is not the failure of your whole defence. The same layering logic applies to the broader family of injection and deserialization flaws, which is why building these habits around Log4Shell pays off well beyond this specific library.
Conclusion
Log4Shell worked because untrusted text reached Log4j's message-lookup feature, which could invoke JNDI to resolve a remote name and load a remote object, executing code. It is a chain of links, and breaking any one prevents the outcome, with removing the vulnerable code being the cleanest break. Map which of your systems still have the chain intact with the Log4Shell scanner at log4shell.tools.