Tracking malware threats and cybersecurity news.

Vulnerabilities

Log4Shell Five Years Later: How to Check If Your Java Apps Are Still Exposed

September 21, 2026

Log4Shell Five Years Later: How to Check If Your Java Apps Are Still Exposed

In December 2021, most teams found the obvious Log4Shell exposure within days: the log4j-core JAR buried in a Spring Boot fat JAR, a version string in a Maven POM, an alert from a scanner. Tickets got closed, sprints moved on, and everyone assumed the problem was behind them.

That assumption is usually wrong somewhere. Java dependency trees are long, appliances ship their own JVMs, container images get rebuilt from cached layers, and a patched JAR sitting on disk does nothing for a JVM that has been running since last year's release window. Five years on, the work is less emergency response and more inventory discipline.

Why Log4Shell still turns up in audits

The root cause — a JNDI lookup evaluated inside a log message — was fixed, but the vulnerable code was copied everywhere before that happened. It lives inside shaded dependencies, where a build tool renamed the package to avoid classpath conflicts and hid it from version-based scanning. It lives inside vendor appliances you cannot patch yourself. It lives in the private artifact repository that still serves log4j-core 2.14.1 to anyone who asks for it.

There is also a simple organizational reason. The original ticket often said "upgrade log4j in the payments service," not "find every copy of log4j in the estate." One service got fixed; the shared base image did not.

Start with an inventory, not a patch

Scanning beats guessing. Run file-level searches on hosts and inside container images, then work through archives and dependency manifests.

On hosts and images

A basic filesystem sweep catches the unshaded cases: find / -xdev -name "log4j-core*.jar" 2>/dev/null. Add -iname "log4j*.jar" to catch odd naming, and repeat the search inside each container image with a tool that can mount and inspect layers. Include golden VM images, backup appliances and DR environments — those get scanned last and forgotten first.

Inside fat JARs, WARs and EARs

The interesting finds are nested. A single application archive can contain dozens of JARs, and version-based tools that only read filenames will miss classes moved into another namespace. Look for the class itself, not the filename. Tools such as log4j-detector read bytecode inside nested archives and are useful for exactly this. A crude but effective loop also works: for each archive, list its contents and grep for JndiLookup.class or org/apache/logging/log4j.

Dependency-level scanning and SBOMs

File scans tell you what exists. Dependency scanners tell you what a build would pull in next time. Run Trivy, Grype, Syft or OWASP Dependency-Check against your repositories and generate an SBOM you can query later. If you cannot answer "which services depend on log4j-core?" in under ten minutes, that gap is the real finding.

  • Most-missed locations: shaded copies under renamed packages
  • Third-party appliances, virtual images and vendor-managed hosts
  • Private artifact repositories and CI build caches
  • Container registries and long-lived golden images
  • Backup, DR and snapshot environments
  • JVMs that have been running since before the patch

Check what is actually running

A clean scan of the filesystem is not proof of a clean runtime. Ask the JVM directly which classes it has loaded: jcmd <pid> GC.class_histogram | grep -i jndilookup. If that returns a hit, the process is running vulnerable code right now, regardless of what the deployment pipeline says. You can also check open JARs with lsof -p <pid> | grep -i log4j and read the startup arguments with jcmd <pid> VM.command_line.

One caveat: a running JVM can hold a handle to a JAR file that has since been replaced on disk. That is why you verify both the artifact and the process. The remedy is a restart, not another commit.

Know what "fixed" means

Versions matter, and the first fix was not the final one. Log4j 2.15.0 addressed the original report but was incomplete, and 2.16.0 disabled JNDI lookups by default. Treat 2.17.1 as the baseline for the 2.x line, with 2.12.4 and 2.3.2 as the maintained backports for older Java runtimes. Anything in the 2.0-beta9 through 2.14.1 range is vulnerable.

If you cannot upgrade yet, mitigations reduce risk but do not eliminate it:

  • Delete JndiLookup.class from the offending JAR. It is crude, and it works.
  • Set log4j2.formatMsgNoLookups=true where the version supports it. Helpful, never complete.
  • Filter outbound LDAP, RMI and unusual DNS traffic from application servers. This is the control that keeps paying off.
  • Keep Java patched, since newer JNDI trustURLCodebase defaults block some, but not all, delivery paths.

And retire Log4j 1.x rather than patching it. It has been end of life for years, and it carries its own deserialization and appender problems.

Prioritize in this order

  1. Internet-facing services that log unauthenticated user input.
  2. Anything running a vulnerable version and permitted outbound LDAP or RMI.
  3. Internal services with broad network reach or access to sensitive data.
  4. Vendor appliances, where your leverage is a support contract and a deadline.
  5. Build caches and repositories, so you stop reintroducing the old version.

Verify, then verify again

Remediation is a loop, not a milestone. Rebuild images instead of patching them in place, purge the artifact cache, restart every JVM you touched, and re-run the scan that found the problem. Ask vendors for component versions rather than reassurances; "we are not affected" is not evidence. Then write the finding down, because the next audit will ask the same question.

A quarterly check that takes an hour

You do not need a project to stay ahead of this. Once a quarter: run the file search across hosts and images, run a dependency scan that produces an SBOM, confirm no running JVM has JndiLookup loaded, and confirm your build pipeline cannot resolve a vulnerable version. Blocking outbound LDAP and RMI from application servers should be permanent configuration, not a temporary measure.

Log4Shell is no longer a crisis. It is a housekeeping item — and housekeeping is exactly the kind of work that quietly stops happening until something forces it.

Photo: Brett Sayles / Pexels