The only complete log4j remediation is upgrading Log4j 2 to 2.17.1 or later (2.12.4 for Java 7, 2.3.2 for Java 6); everything else is a temporary mitigation. If you patched during the December 2021 fire drill and stopped at 2.15.0 or 2.16.0, you are still carrying known-vulnerable code. This guide covers what to upgrade to, how to find every copy of the library, and what to do when an upgrade is genuinely blocked.
Why 2.15.0 and 2.16.0 are not enough
Log4Shell was not a single fix. Apache shipped four separate advisories in the span of a month, and each early "fix" turned out to be incomplete:
- CVE-2021-44228 (the original Log4Shell, CVSS 10.0) was partially addressed in 2.15.0 on December 6, 2021.
- CVE-2021-45046 followed because 2.15.0 still allowed JNDI lookups in some non-default configurations. 2.16.0 (December 13) disabled message lookups and removed JNDI by default.
- CVE-2021-45105 was a denial-of-service via uncontrolled recursion in self-referential lookups, fixed in 2.17.0 (December 17).
- CVE-2021-44832 was a lower-severity remote code execution path through a JDBC Appender with an attacker-controlled configuration, fixed in 2.17.1 on January 4, 2022.
So a machine sitting on 2.16.0 is exposed to CVE-2021-45105, and 2.17.0 is exposed to CVE-2021-44832. The clean target is 2.17.1 on the 2.x line. Backport lines exist for older Java: 2.12.4 for Java 7 and 2.3.2 for Java 6.
Step one: find every copy of log4j
Remediation fails most often because a team patches the log4j they declared and misses the one pulled in transitively by a framework, an agent JAR, or a shaded fat JAR. Use several detection methods, not one.
For Maven, print the dependency tree and filter:
mvn dependency:tree -Dincludes=org.apache.logging.log4j
For Gradle:
./gradlew dependencies --configuration runtimeClasspath | grep log4j
These catch declared and transitive dependencies but not classes that have been shaded or repackaged inside another artifact. For that, scan the actual bytecode on disk. The JndiLookup class is the specific component that carries the exploit path:
find / -name '*.jar' -print0 | \
xargs -0 -I{} sh -c 'unzip -l "{}" 2>/dev/null | \
grep -q JndiLookup && echo "{}"'
A software composition analysis (SCA) tool such as Safeguard can flag log4j transitively across a whole fleet and tell you which service actually loads the vulnerable version, which is the part that manual find misses at scale. The point is coverage: the version you ship is not always the version you declared.
Step two: upgrade cleanly
Pin the version explicitly rather than trusting a transitive bump. In Maven, use dependencyManagement so every module resolves the same release:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>2.17.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
In Gradle, a resolution strategy forces the version even when a transitive dependency asks for an older one:
configurations.all {
resolutionStrategy.eachDependency {
if (requested.group == 'org.apache.logging.log4j') {
useVersion('2.17.1')
}
}
}
After upgrading, rebuild and rescan. Do not assume the build picked up the change; confirm it in the resolved dependency tree.
Step three: interim mitigations when you cannot upgrade
Sometimes an upgrade is blocked by a vendor appliance or a release freeze. The best-supported interim mitigation is to physically remove the JndiLookup class from the classpath:
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
This is Apache's own recommended workaround and it neutralizes the primary RCE path. Note what it does not cover: the older formatMsgNoLookups system property (-Dlog4j2.formatMsgNoLookups=true) is insufficient on its own because CVE-2021-45046 showed lookups reachable through other inputs. Treat class removal as a bridge, not a destination, and schedule the real upgrade.
Also confirm you are dealing with Log4j 2, not Log4j 1.x. Log4j 1.x is end of life and carries its own separate issue, CVE-2021-4104, in its JMSAppender. The Log4Shell patches do not apply to 1.x; the remediation there is migration off the abandoned 1.x line entirely.
Step four: prevent the recurrence
The reason Log4Shell was so painful was the lack of a live inventory. A few habits keep the next one cheap:
- Generate and store a software bill of materials (SBOM) per build so you can answer "where is library X" in seconds, not days.
- Wire dependency scanning into CI so a vulnerable transitive pull fails the pipeline before it ships. Our SCA product page covers how gating on new advisories works in practice.
- Watch for lookalikes. The same JNDI-injection pattern shows up in other Java deserialization and lookup issues; the Jackson databind security guide walks through a related transitive-risk class.
Verifying the fix held
Do not close the ticket on a version bump alone. Confirm the runtime actually loads 2.17.1:
java -cp log4j-core-2.17.1.jar org.apache.logging.log4j.core.util.Constants 2>/dev/null; \
unzip -p log4j-core-*.jar META-INF/MANIFEST.MF | grep 'Implementation-Version'
Then rescan with your SCA tool and confirm the advisory count for org.apache.logging.log4j dropped to zero across every deployable artifact, including containers and any bundled agents.
FAQ
Is upgrading to Log4j 2.15.0 enough to fix Log4Shell?
No. 2.15.0 only partially fixed the original vulnerability and remains exposed to CVE-2021-45046. The complete fix on the 2.x line is 2.17.1, which also closes CVE-2021-45105 and CVE-2021-44832.
What is the fastest log4j remediation if I cannot upgrade right now?
Remove the JndiLookup class from log4j-core using zip -q -d. This is Apache's recommended workaround and neutralizes the main remote-code-execution path, but you should still schedule a real upgrade to 2.17.1.
Does the formatMsgNoLookups flag fully protect me?
No. Setting log4j2.formatMsgNoLookups=true was an early mitigation, but CVE-2021-45046 demonstrated lookups reachable through other inputs, so it is not a complete defense on its own.
How do I find log4j if it is not in my declared dependencies?
Scan the actual JAR bytecode for the JndiLookup.class file and use an SCA tool that resolves transitive and shaded dependencies. The version you declared is often not the only version on your classpath.