Skip to content

CWE-117: Log Injection / Log Forging - Java

Overview

Log Injection occurs when untrusted data is written to log files without encoding, allowing attackers to forge log entries or inject malicious content.

Primary Defence: Use SLF4J/Logback or Log4j2 with JSON/ECS output (logstash-logback-encoder or JsonLayout) so control characters are encoded within fields and cannot forge log entries. Parameterized logging alone is insufficient - you must configure JSON/ECS output or manually encode control characters (recommended: convert \n to \\n, \r to \\r) to preserve forensic evidence, or remove them entirely (loses attack visibility) to prevent log forging. Handle Unicode line separators (\u2028, \u2029) in addition to ASCII CR/LF.

Common Vulnerable Patterns

String Concatenation Allows Log Forgery

// VULNERABLE - String concatenation allows log forgery
logger.info("User " + username + " logged in");

// Attack example:
// username = "admin\r\nINFO: User hacker logged in as admin"
// Result: Creates fake log entry that appears legitimate

Why this is vulnerable:

  • User input is concatenated directly into the log message.
  • CR/LF characters (\r, \n) can split log lines and forge entries.
  • Attackers can create fake security events or hide real activity.
  • Text-based layouts write the injected lines as separate records.

String Formatting Without Sanitization

// VULNERABLE - String.format() with unencoded input
logger.warn(String.format("Failed login for %s from %s", username, ip));

// Attack example:
// username = "test\nINFO: Security audit disabled by administrator"
// Result: Injects fake administrative log message

Why this is vulnerable:

  • String.format() does not encode control characters.
  • Newline sequences (\n, \r) create additional log records.
  • Forged entries can mimic admin/system messages.
  • Log analysis tools may treat injected lines as legitimate events.

HTTP Headers in Logs

// VULNERABLE - Logging HTTP headers without encoding
String userAgent = request.getHeader("User-Agent");
logger.info("Request from: " + userAgent);

// Attack example:
// User-Agent: Mozilla/5.0\r\nINFO: ADMIN_ACCESS granted\r\nINFO: IP: 127.0.0.1
// Log output:
// Request from: Mozilla/5.0
// INFO: ADMIN_ACCESS granted
// INFO: IP: 127.0.0.1
// Result: Multiple fake log lines with forged admin access

Why this is vulnerable:

  • HTTP headers are attacker-controlled inputs.
  • CR/LF sequences can create multiple forged log lines.
  • Attackers can inject fake access grants or audit events.
  • Incident response and monitoring tools can be misled by forged records.

Secure Patterns

Use Parameterized Logging with SLF4J (Pair with JSON/ECS output)

// SECURE - SLF4J parameterized logging
public class UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserService.class);

    public void logLogin(String username, String ipAddress) {
        logger.info("Login successful. User: {}, IP: {}", username, ipAddress);
    }

    public void logFailedAttempt(String username, String reason) {
        logger.warn("Login failed. User: {}, Reason: {}", username, reason);
    }
}

Why this works:

  • SLF4J keeps the template and parameters separate during formatting.
  • It avoids string concatenation that can mix control characters into the template.
  • JSON layouts escape newline characters as \\n within a single log record.
  • Without JSON encoding, CR/LF still render in text layouts and must be encoded.
  • Use JSON output or explicit encoding to fully block log forging.

Use Manual Encoding

This is recommended over sanitization - all relevant characters are escaped to preserve forensic evidence

// Helper method; place inside your logging utility or service class
private String EncodeForSingleLineTextLog(String input) {
    if (input == null) return "";
    StringBuilder out = new StringBuilder(input.length());
    for (int i = 0; i < input.length(); i++) {
        char ch = input.charAt(i);
        // Encode full ASCII control range + DEL + C1 controls
        if (ch <= 0x1F || ch == 0x7F || (ch >= 0x80 && ch <= 0x9F)) {
            switch (ch) {
                case '\\': out.append("\\\\"); break;
                case '\r': out.append("\\r"); break;
                case '\n': out.append("\\n"); break;
                case '\t': out.append("\\t"); break;
                default: out.append(String.format("\\u%04x", (int) ch)); break;
            }
            continue;
        }
        // Encode Unicode line separators
        if (ch == '\u0085') { out.append("\\u0085"); continue; }
        if (ch == '\u2028') { out.append("\\u2028"); continue; }
        if (ch == '\u2029') { out.append("\\u2029"); continue; }
        out.append(ch);
    }
    return out.toString();
    // Shows "admin\\r\\nFAKE" - preserves evidence of injection attempt
}
Usage
logger.info("User {} performed {}", EncodeForSingleLineTextLog(username), EncodeForSingleLineTextLog(action));

Why this works:

  • Encoding approach (RECOMMENDED): Converts control chars to visible escape sequences, preserving complete forensic evidence.
  • StringUtilities.stripControls() removes ASCII control characters (0x00-0x1F, 0x7F) but sacrifices forensic data.
  • Unicode line separators (\u2028, \u2029) must be handled separately.
  • Sanitization works even with text-based log layouts.
  • Encoding is strongly preferred because security teams can see exactly what attackers attempted.
  • It reduces risk when JSON/ECS logging is not available.

Use Apache Commons Text for Encoding

// SECURE - Use Apache Commons Text to encode for logs
import org.apache.commons.text.StringEscapeUtils;

public void logUserAction(String username, String action) {
    String safeUsername = StringEscapeUtils.escapeJava(username);
    String safeAction = StringEscapeUtils.escapeJava(action);
    logger.info("User {} performed {}", safeUsername, safeAction);
}

// Attack example:
// username = "admin\r\nINFO: FAKE LOG ENTRY"
// Log output: User admin\\r\\nINFO: FAKE LOG ENTRY performed login
// Result: Single log line with visible escape sequences

Why this works:

  • StringEscapeUtils.escapeJava() converts control characters to escape sequences (\n becomes \\n, \r becomes \\r).
  • Encodes ASCII control characters (0x00-0x1F) including \t, \b, \f, and DEL (0x7F).
  • Neutralizes ANSI color/escape codes by escaping ESC character (0x1B\\u001B).
  • Newlines and carriage returns are encoded, preventing log forging.
  • Attack attempts are preserved as visible evidence in logs.
  • Limitation: Does NOT encode Unicode line separators (\u2028, \u2029, \u0085) - use the manual encoding method if these are a concern.
  • Works with any logging framework and text-based layouts.
  • Requires Apache Commons Text dependency

Validate Input Before Logging

// SECURE - Reject invalid input before logging
public void logEvent(String eventData) {
    String decoded = java.net.URLDecoder.decode(eventData, java.nio.charset.StandardCharsets.UTF_8);
    if (decoded.contains("\r") || decoded.contains("\n")
            || decoded.contains("\u2028") || decoded.contains("\u2029")) {
        logger.warn("Attempted log injection detected");
        return;
    }

    logger.info("Event: {}", decoded);
}

Why this works:

  • It checks for CR/LF and Unicode line separators after decoding input.
  • Suspicious input is replaced with a safe, generic warning.
  • This creates an audit trail without logging attacker-controlled data.
  • It serves as a defense-in-depth layer against log forging.
  • It should be paired with encoding or JSON output for full coverage.

Use Structured Logging with JSON/ECS Output

// SECURE - Structured logging with JSON
import net.logstash.logback.argument.StructuredArguments;

logger.info("User login", 
    StructuredArguments.keyValue("username", username),
    StructuredArguments.keyValue("ipAddress", ipAddress),
    StructuredArguments.keyValue("timestamp", System.currentTimeMillis())
);

// Outputs as JSON, preventing log injection:
// {"@timestamp":"2026-01-04T10:30:00.000Z","message":"User login","username":"admin\\nFAKE","ipAddress":"192.168.1.1"}

Why this works:

  • JSON output writes each event as a single structured record.
  • Newlines are JSON-escaped (e.g., \n becomes \\n) inside field values.
  • Injected data cannot create extra log lines in the output stream.
  • Structured fields make forged content easier to detect in log analysis.
  • This is the most robust default for production logging.

Verification

After implementing the recommended secure patterns, verify the fix through multiple approaches:

  • Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
  • Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
  • Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
  • Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
  • Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
  • Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
  • Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
  • Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced

Logback Configuration (Additional Security)

Use JSON layouts/encoders so user input is escaped inside a single log record.

Logback (logstash-logback-encoder):

<configuration>
  <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
  </appender>
  <root level="INFO">
    <appender-ref ref="JSON"/>
  </root>
</configuration>

Log4j2 (JsonLayout):

<Configuration>
  <Appenders>
    <Console name="JsonConsole">
      <JsonLayout compact="true" eventEol="true"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="info">
      <AppenderRef ref="JsonConsole"/>
    </Root>
  </Loggers>
</Configuration>

Additional Resources