Home › Security › Log4Shell JNDI RCE — Patch Log4j and Scan Dependencies
Advanced 5 min · September 23, 2026

Log4Shell JNDI RCE — Patch Log4j and Scan Dependencies

Log4Shell turned log messages into remote code execution via JNDI lookups.

N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Notes here come from systems that actually shipped.

Follow
✓ Production
production tested
September 26, 2026
last updated
2,085
articles · all by Naren
Before you start⏱ 19 min
  • ✓Java services (or any stack) with third-party dependencies
  • ✓Access to build pipelines and artifact storage
  • ✓Basic familiarity with vulnerability scanners and WAFs
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • Log4Shell (CVE-2021-44228) let text written to logs trigger JNDI lookups in vulnerable Log4j 2 versions, turning user input into server-side requests
  • Patch first: upgrade Log4j 2 to a fixed release in the 2.17 line or later, and treat every copy — transitive and shaded — as in scope
  • Use WAF rules that block the lookup indicator only as a stopgap while you patch; filters don't fix the library
  • Keep a software bill of materials (SBOM) and scan dependencies on every build so the next logging-library flaw finds you fast
  • Hunt historically: search archived logs for the lookup indicator to scope whether anyone probed you before the patch landed
✦ Definition~90s read
What is Log4Shell?

Log4Shell (CVE-2021-44228) is a remote-code-execution vulnerability in Apache Log4j 2, the dominant Java logging framework. Vulnerable versions performed JNDI (Java Naming and Directory Interface) lookups while rendering log messages: if message text contained a lookup expression, the library resolved it — including lookups that connected to remote servers and loaded executable code.

★
Imagine a librarian who files every note handed to her — but whenever a note contains the phrase 'please fetch,' she leaves the building to fetch whatever's named.

Since applications routinely log attacker-controlled strings (usernames, search terms, HTTP headers, chat messages), the barrier to reaching the flaw was near zero.

Versions 2.0-beta9 through 2.14.1 were affected; fixes landed in 2.15.0 with hardening in 2.16.0 and the 2.17 line. Practically: any Log4j 2 older than the fixed releases needs upgrading, including transitive copies and shaded (renamed, bundled) copies inside other jars.

The first response many teams reached for was WAF filtering — blocking requests containing the lookup indicator string. That bought time but fixed nothing: filters miss encoded variants and encrypted or internal traffic. The durable controls are upgrading the library and knowing where it lives.

An SBOM (software bill of materials) — a machine-readable inventory of every dependency in every service — plus continuous scanning turns 'find every Log4j' from a weeks-long hunt into a query you run before lunch.

The lasting lesson isn't about one library: any component interpreting untrusted text as instructions carries the same risk. Patch discipline, dependency visibility, and stopgap honesty (knowing what your WAF rule does and doesn't cover) are the habits that survive long after this CVE fades from dashboards.

Plain-English First

Imagine a librarian who files every note handed to her — but whenever a note contains the phrase 'please fetch,' she leaves the building to fetch whatever's named. Attackers started handing in notes that said exactly that, naming addresses they controlled. That's Log4Shell: a logging library that treated message text as instructions and performed lookups it never should have. The fix: file notes without reading them as orders — and check every branch got the memo.

In December 2021, a flaw in one of the world's most-used Java logging libraries redrew everyone's weekend plans. Log4Shell meant that a string — typed into a chat box, a username field, a user-agent header, anything an application logged — could make a vulnerable server perform a network lookup and load remote code. Logging, the thing you do to understand incidents, had become the incident.

The vulnerability sat in Log4j 2's message substitution: when log text contained a lookup expression, the library resolved it, including directory lookups that reached across the network. Because applications log untrusted input constantly — usernames, URLs, form fields, headers — the attack surface was essentially 'anything your app writes down.' Security teams spent weeks finding every Java service they owned, because the library hides inside frameworks, shaded jars, and vendor products.

This guide stays firmly on the defensive side: how the mechanism worked (so you recognize its shape), which versions were affected and the patch discipline that ends it, why WAF rules are only a stopgap, and the SBOM-plus-scanning habit that turns the next library emergency from a forensic dig into a database query. No exploit details — just the response playbook.

How a Log Line Became Code Execution

Logging libraries sit in a uniquely trusted spot: every layer of your app hands them untrusted text — usernames, URLs, error details, headers — and they handle it millions of times a day. Log4j 2 added a feature called message substitution, where special expressions in log text were resolved at render time: dates formatted, environment values inserted, directory service names looked up. That last capability, JNDI lookups, is where routine logging crossed into remote code execution.

JNDI is Java's directory-access API: given a name, it can consult remote servers to resolve it. When a vulnerable Log4j rendered a message containing a lookup expression naming an attacker-controlled server, it connected out and loaded what it was given. The attacker never needed an account, a special endpoint, or even a request the app considered important — anything the app wrote to its logs was a delivery channel, including fields like usernames that get logged on every failed login.

The defensive takeaway is architectural, not anecdotal. Any component that interprets data as instructions — log renderers, template engines, expression languages, deserializers — must default to treating untrusted input as inert text. When you evaluate libraries, ask what their rendering path can trigger: network calls, class loading, and command execution have no business in a log statement. The safest log pipeline is a dumb one: format strings, append, ship.

📊 Production Insight
After Log4Shell, one team audited every other library that rendered untrusted text and found a template engine evaluating expressions in email subjects. Same shape, different logo. Audit the pattern, not just the CVE.
🎯 Key Takeaway
Log4j resolved lookup expressions in untrusted log text, reaching remote servers. Treat any text-interpreting component as suspect until its render path is proven inert.

Affected Versions and the Patch Discipline That Ends It

The vulnerable range ran from Log4j 2.0-beta9 through 2.14.1, with the initial fix in 2.15.0 and further hardening in 2.16.0 and the 2.17 line — each follow-up closing bypasses and disabling the dangerous capability by default. The practical rule for your fleet is blunt: anything on the Log4j 2 line older than the fixed releases gets upgraded, and you verify the upgrade by inspecting what's actually deployed, not what the ticket says.

Version discipline fails in three predictable places. Transitive dependencies pull old copies through frameworks you didn't choose directly — your pom looks clean while the dependency tree quietly includes the vulnerable jar. Shaded jars bundle renamed copies inside vendor SDKs where manifest scanners can't see them. And running processes keep serving the old classes until restarted, so file replacement without restart validation is theater. Your patch runbook must cover all three: tree, contents, and restarts.

Make the fix durable with dependency governance. Pin logging versions in a managed BOM (bill of materials) pom, fail builds that resolve banned versions, and regenerate SBOMs on every build so the next emergency starts with a query instead of a treasure hunt. The snippet below shows the Maven-side habit: enforce the floor version so no transitive downgrade can sneak the vulnerable line back in.

BASH
1
2
3
4
5
6
7
8
9
# Show the resolved Log4j version through every transitive path
mvn dependency:list -Dincludes=org.apache.logging.log4j:log4j-core \
  -Dmdep.outputFile=/tmp/log4j-tree.txt && cat /tmp/log4j-tree.txt

# Fail the build if any module resolves a pre-fix 2.x line
mvn -q dependency:list -Dincludes=org.apache.logging.log4j:log4j-core \
  | grep -E 'log4j-core:jar:2\.([0-9]|1[0-4])' && {
  echo 'BLOCKED: vulnerable Log4j 2 line detected'; exit 1; }
echo 'OK: no pre-fix Log4j 2.x in the resolved tree'
📊 Production Insight
A team patched direct dependencies in hours but left a test-scope module resolving the old line. It shipped inside an admin tool nobody inventoried. Enforce the version floor on every scope, not just production code.
🎯 Key Takeaway
Upgrade every copy — direct, transitive, and shaded — to the fixed line, verify restarts loaded it, and enforce a version floor so it can't regress.

WAF Rules as a Stopgap, Never the Fix

When Log4Shell broke, WAF rules matching the lookup indicator were the fastest relief available: deploy once at the edge, block the obvious probes, buy the weekend to patch properly. That role — time buyer — is the only honest way to describe them. A filter that recognizes one spelling of malicious input can't remediate a library that executes whatever it receives.

Know the blind spots by name so nobody mistakes coverage for closure. Encoded and obfuscated variants slip past literal string matches. Encrypted traffic (TLS to your backends, internal mTLS) is opaque to edge inspection. Internal paths — queue consumers, batch jobs, service-to-service calls carrying logged user content — never cross the WAF at all. And legitimate-looking traffic that the filter waves through still reaches the vulnerable code with full effect.

Use stopgaps with stopgap hygiene: document what each rule covers and what it can't see, alert on rule hits as threat intelligence (probe volume tells you you're being targeted), set an expiry review so temporary rules don't fossilize into false confidence, and report them separately from remediation metrics. The rule count going up is not the vulnerable-host count going down. Patching is the fix; everything else is weatherproofing while the roofers drive over.

📊 Production Insight
One dashboard added WAF blocks to the 'remediated hosts' metric. Leadership saw green while 60 hosts stayed vulnerable behind the filter. Separate stopgap metrics from remediation metrics or the incident will close itself prematurely.
🎯 Key Takeaway
Deploy indicator filters fast, label them temporary with known blind spots, and never count a blocked probe as a patched host.

SBOM and Dependency Scanning as a Daily Habit

The teams that answered Log4Shell fastest weren't the best patchers — they were the ones who already knew where every library lived. An SBOM (software bill of materials) is that knowledge in machine-readable form: every dependency, every version, every service, regenerated on every build. When the next logging-library CVE lands, the first question ('are we affected and where?') becomes a query instead of a multi-week dig through repos, containers, and vendor bundles.

Build the habit in CI, not in a wiki. Generate the SBOM at build time so it reflects what's actually shipped, scan it against vulnerability feeds on every pipeline run, and fail builds on critical issues in reachable code. Scan contents, not just manifests: match on classes and file hashes so shaded and vendored copies can't hide. Store SBOMs per release so incident response can ask historical questions ('which customers got the build containing version X?') without rebuilding the past.

Extend the inventory past your own code. Vendor SDKs, base images, build plugins, and infrastructure agents all ship libraries — require SBOMs from suppliers where you can, and scan what they deliver where you can't. The pipeline snippet below shows the shape: generate, scan, gate. Ten lines that would have saved the 19-day shaded-jar embarrassment in the incident above.

.github/workflows/supply-chain-gate.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
name: supply-chain-gate
on: [push, pull_request]
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Generate SBOM from the built image
        run: syft packages dir:. -o cyclonedx-json > sbom.json
      - name: Scan SBOM for known CVEs
        run: grype sbom:sbom.json --fail-on critical
      - name: Keep the SBOM with the release
        uses: actions/upload-artifact@v4
        with:
          name: sbom-${{ github.sha }}
          path: sbom.json
📊 Production Insight
A team generated SBOMs but stored them on a share nobody indexed. During Log4Shell they re-scanned everything from scratch anyway. SBOMs only pay off when they're queryable per release — attach them to builds, not folders.
🎯 Key Takeaway
Generate SBOMs at build time, scan contents on every run, gate on criticals, and keep them per release so incidents start with answers.

Hunting the Lookup Indicator in Your Logs

After patching, you still owe yourself an answer: did anyone exploit this before the fix? The lookup indicator — the distinctive lookup-expression marker the vulnerable feature resolved — is your detection anchor. Search current and archived logs for that marker in attacker-controlled fields: usernames, user-agent strings, search boxes, form inputs, header values. A hit means someone knocked; a hit followed by suspicious outbound connections means they may have entered.

Search defensively and completely. Cover archived and cold storage, not just the last 7 days — Log4Shell was probed within hours of disclosure and mass-scanned for months. Include encoded shapes of the marker, since scanners obfuscated it to dodge the very WAF rules everyone deployed. Correlate ruthlessly: a lone probe with no follow-on traffic is reconnaissance, while a probe adjacent to an unknown egress connection escalates the host to isolation and forensic review.

Treat the hunt as a scheduled operation, not a one-off. Keep the detection search running for at least 30 days post-patch to catch slow-burn exploitation and straggler systems (un-rebuilt containers, restored backups, forgotten regions). Document negative results too — 'searched 6TB across these sources, found only blocked probes' is what lets you responsibly close the incident instead of anxiously assuming.

BASH
1
2
3
4
5
6
7
8
9
# Hunt archived logs for the lookup-indicator marker (defensive search)
# Searches plain and gzipped logs; review hits for attacker fields.
MARKER='jndi:'
rg -l --no-messages -g '*.log' -g '*.log.gz' -z "$MARKER" /var/log/ \
  | tee /tmp/indicator-hits.txt

echo "Files with possible lookup-indicator probes:"
cat /tmp/indicator-hits.txt
# Next: correlate each hit's timestamp with egress logs before concluding.
⚠ Search for the indicator, don't reproduce the attack
Historical hunting needs only the lookup marker as a search string in your own logs. Never craft or replay functional lookup expressions — detection uses the indicator's name, not a working payload.
📊 Production Insight
A team searched 7 days of hot logs, found nothing, and closed the incident. A later audit of 90-day archives found the first probes predated the patch by weeks. Scope your hunt to disclosure date, not retention convenience.
🎯 Key Takeaway
Search all logs back to disclosure for the indicator marker, correlate hits with egress, and keep the hunt scheduled for 30 days after patching.

A Patch Rollout Playbook for Logging Libraries

Library RCEs compress into hours what normally takes sprints, so run them from a playbook, not from memory. Split the response into parallel tracks from minute one: patching (upgrade direct, transitive, and shaded copies), stopgaps (indicator filters with documented blind spots), detection (historical indicator hunt plus egress correlation), and communications (status updates that distinguish 'blocked at edge' from 'remediated'). Assign an owner to each track so nothing queues behind anything else.

Sequence the patching track for maximum risk reduction per hour. Internet-facing Java services first, then internal consumers of logged user content (queue workers, analytics pipelines), then everything else including admin tools and test environments — attackers don't respect your environment tiers. Validate each wave by confirming running processes loaded the fixed jars, not just that files changed on disk. A deploy without restart validation is a hope, not a fix.

Close the incident with evidence, not exhaustion. Require the content-based scan to return zero across all artifacts, the SBOM query to agree, the 30-day detection watch to be scheduled, and communications to state residual risk honestly (which stragglers remain, what covers them). Then hold the retrospective while memories are fresh: which inventory gap cost the most time, and what CI gate would have closed it? The playbook you improve today is the weekend you get back next time.

BASH
1
2
3
4
5
6
7
8
# Prove the FIXED jar is loaded: service start must be NEWER than jar mtime
JAR=$(ls -t /opt/app/lib/log4j-core-*.jar | head -1)
echo "Newest jar: $JAR"
stat -c 'jar mtime: %y' "$JAR"
systemctl show api.service -p ActiveEnterTimestamp
# Content re-scan across the service tree must return zero vulnerable copies
rg -l --no-messages 'JndiLookup\.class' /opt/app --glob '*.jar' \
  && echo 'FAIL: vulnerable class still present' || echo 'OK: class absent'
📊 Production Insight
The fastest Log4Shell responses pre-assigned a 'shaded-jar hunter' role on day one. Teams that discovered hidden copies in week three all shared one trait: nobody owned the hunt for copies the scanner couldn't see.
🎯 Key Takeaway
Run four parallel tracks — patch, stopgap, detect, communicate — riskiest services first, and close only on scan-zero evidence plus a scheduled watch.
● Production incidentPOST-MORTEMseverity: high

One Shaded Jar Kept 340 Servers Vulnerable for 19 Days

Symptom
During the December 2021 Log4Shell response, a SaaS company upgraded Log4j across 340 servers in 48 hours and declared the incident closed. Nineteen days later, a routine dependency rescan flagged a vulnerable Log4j copy shaded inside a vendor analytics SDK on the API tier — same flaw, different jar name, invisible to the first scan. Firewall logs showed 1,400 blocked requests carrying the lookup indicator had reached those hosts during the 19-day window. No successful exploitation was confirmed, but the team couldn't prove a negative for the earliest probes, forcing a full credential rotation and a week of forensic log review across 6 terabytes of archives.
Assumption
The team assumed their scanner saw every Log4j because it checked declared Maven versions. Shaded jars — dependencies renamed and bundled inside another library — don't appear in manifests under their own name, so the scanner reported clean. They also assumed the WAF rule blocking the indicator string made remaining copies harmless, forgetting that internal service-to-service paths and encrypted traffic bypassed the WAF entirely.
Root cause
The vendor SDK bundled its own Log4j 2.13 copy under relocated package names. Version-based scanning inspected pom files, not jar contents, and missed it. The WAF stopgap covered only edge HTTP traffic while internal log flows (message queues carrying user content into Java consumers) never crossed the filter. The vulnerable code therefore stayed reachable for 19 days, absorbing over a thousand blocked-at-the-edge probes plus an unknown number of uninspected internal ones.
Fix
The SDK was upgraded to a vendor release with the fixed Log4j line the same day, verified by content-scanning jars for the vulnerable lookup class rather than trusting manifests. WAF rules stayed on as defense in depth but were formally labeled stopgaps with expiry reviews. The lasting fix was process: an SBOM generated at every build, content-based (not manifest-based) scanning in CI, and a rule that any P0 library flaw triggers a manifest-plus-contents hunt — including vendor bundles — before closure.
Key lesson
  • Scan jar contents, not just manifests: shaded and vendored copies hide the same vulnerable class under different names.
  • WAF indicator-blocking is a time buyer with explicit blind spots (encrypted and internal traffic), not a remediation you can close on.
  • Don't declare a library incident closed until the SBOM query for the flaw returns zero across every service, including vendor SDKs.
Production debug guideFive defensive steps, from scoping exposure to proving the flaw is gone.5 entries
Symptom · 01
A critical RCE in a logging library is announced and you run Java services
→
Fix
Assume exposure and start two tracks at once: patch the direct dependency to the fixed release line, and query your SBOM (or generate one per service now) for every copy including transitive and shaded ones. Contain first with the vendor's recommended kill-switch property where published, but track it as temporary — configuration flags get lost in rebuilds, upgraded libraries don't.
Symptom · 02
You need to find every copy of the vulnerable library, including hidden ones
→
Fix
Scan built artifacts for the vulnerable class itself, not version strings: list jar contents across services and flag the lookup class under both its original and any relocated package paths. Check containers, VM images, and vendor SDKs — anything shipping Java. Record each find against its owning service so remediation has an owner, not just a list.
Symptom · 03
You need a stopgap before patches finish rolling out
→
Fix
Deploy WAF rules matching the lookup indicator at the edge, document exactly what they don't cover (encrypted traffic, internal paths, encoded variants), and set an expiry review date. In parallel, strip or disable the dangerous lookup capability via the vendor's supported flag. Measure stopgap coverage daily and report it as 'attacks blocked at edge,' never as 'hosts remediated.'
Symptom · 04
You must determine whether anyone exploited you before the patch
→
Fix
Search current and archived logs for the lookup indicator string and its common encoded shapes, focusing on internet-facing fields (usernames, user-agents, search boxes). Correlate hits with outbound connections to unknown hosts around the same timestamps. Any host with both a probe and a suspicious egress gets isolated and forensically reviewed; when in doubt, rotate its credentials.
Symptom · 05
Patches are deployed and you need to prove the incident is closed
→
Fix
Re-run the content-based scan across all artifacts and require zero hits; verify running processes actually loaded the fixed jars (restart validation, not just file replacement); confirm SBOMs regenerate clean on the next build; and keep the detection search scheduled for 30 days to catch stragglers like un-rebuilt containers or restored backups.
Log4Shell Response Layers at a Glance
Root CauseHow to ConfirmFixPrevention
Vulnerable Log4j 2 in direct dependenciesResolved dependency tree shows a pre-fix 2.x versionUpgrade to the fixed release line and enforce a version floorBOM-managed versions with CI bans on vulnerable lines
Hidden copies in transitive and shaded jarsContent scan finds the lookup class under original or relocated pathsUpgrade the bundling SDK or replace the shaded copyContent-based scanning plus per-build SBOM queries
Probes reaching unpatched hostsEdge and WAF logs show indicator hits; correlate with egressIndicator filters as stopgap; patch as remediationScheduled indicator hunts and egress anomaly alerts
No inventory of where libraries shipIncident response starts with 'we don't know what runs Java'Generate per-release SBOMs and scan them in CISBOM attached to every build; supplier SBOM requirements
⚙ Quick Reference
4 commands from this guide
FileCommand / CodePurpose
mvn dependency:list -Dincludes=org.apache.logging.log4j:log4j-core \Affected Versions and the Patch Discipline That Ends It
.githubworkflowssupply-chain-gate.ymlname: supply-chain-gateSBOM and Dependency Scanning as a Daily Habit
MARKER='jndi:'Hunting the Lookup Indicator in Your Logs
JAR=$(ls -t /opt/app/lib/log4j-core-*.jar | head -1)A Patch Rollout Playbook for Logging Libraries

Key takeaways

1
Log4Shell resolved lookup expressions in log text, reaching remote servers from a ubiquitous library.
2
Upgrade every copy to the fixed line
direct, transitive, and shaded — and verify restarts loaded it.
3
WAF indicator filters buy time with known blind spots; never count blocks as patched hosts.
4
Scan artifact contents for the vulnerable class; manifests alone miss renamed bundled copies.
5
Hunt logs back to disclosure for the indicator and correlate with suspicious egress.
6
Ship per-build SBOMs and scan them in CI so the next library emergency starts with answers.

Common mistakes to avoid

5 patterns
×

Trusting manifest-only scans to find every vulnerable copy

Symptom
Declared versions look clean while shaded jars bundle the same vulnerable classes under relocated names.
Fix
Scan artifact contents for the vulnerable class itself and query per-build SBOMs that include vendored code.
×

Counting WAF blocks as remediated hosts

Symptom
Dashboards turn green from filter hits while unpatched services stay reachable via encrypted and internal paths.
Fix
Report stopgap and remediation metrics separately; close only on scan-zero plus restart validation.
×

Replacing files without verifying running processes reloaded them

Symptom
The fixed jar sits on disk while the live JVM keeps serving the vulnerable classes from memory.
Fix
Validate loaded library versions per process after every deploy wave and restart where needed.
×

Searching only recent logs for pre-patch exploitation

Symptom
Early probes in archived storage go unnoticed and the incident closes on a false negative.
Fix
Hunt back to the disclosure date across hot and cold storage, including encoded marker shapes.
×

Patching production but forgetting admin tools and test environments

Symptom
A forgotten admin console with the old library becomes the attacker's quietest path in.
Fix
Scope the hunt to every Java runtime you own — prod, staging, tooling, backups — before declaring closure.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
In one paragraph, what made Log4Shell so severe?
Q02SENIOR
Why were WAF rules insufficient as a Log4Shell fix?
Q03SENIOR
How do shaded jars complicate vulnerability scanning?
Q04SENIOR
How would you determine whether you were exploited before patching?
Q05SENIOR
Design a dependency program so the next Log4Shell takes hours, not weeks...
Q01 of 05JUNIOR

In one paragraph, what made Log4Shell so severe?

ANSWER
A ubiquitous Java logging library resolved lookup expressions in untrusted log text, reaching remote servers and loading code. Since apps log attacker-controlled input everywhere, exposure was near-universal, and the library hid inside frameworks and vendor bundles that inventories missed.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Which Log4j versions were affected by Log4Shell?
02
Is removing the lookup class from the jar a valid fix?
03
Do non-Java services need to worry about Log4Shell?
04
How long should I keep hunting for exploitation after patching?
05
Should I pay a ransom or negotiate if exploited?
06
What's the single highest-value habit to adopt after Log4Shell?
N
Naren Founder & Principal Engineer

20+ years shipping production backend systems. Notes here come from systems that actually shipped.

Follow
✓ Verified
production tested
September 26, 2026
last updated
2,085
articles · all by Naren
🔥

That's Supply Chain. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Server-Side Request Forgery (SSRF) and Cloud Metadata
1 / 2 · Supply Chain
Next
Dependency Confusion in Package Registries
→