What log forgery is and why it matters

Log forgery happens when an attacker manipulates log output so it appears to contain events that never occurred. Common examples include:

  • Injecting newline characters to create fake entries
  • Spoofing log prefixes such as ERROR, WARN, or timestamps
  • Breaking log parsers by inserting control characters
  • Hiding malicious input inside multiline messages

A classic vulnerable pattern looks like this:

println!("INFO: user login failed: {}", username);

If username contains alice\nERROR: admin authenticated, the resulting log may look like two separate events. Even if the application behaves correctly, the logs no longer tell the truth.

The goal is not to “hide” user input. It is to ensure untrusted data is represented safely and unambiguously.

Prefer structured logging over formatted strings

The strongest defense against log forgery is structured logging. Instead of embedding user input into a free-form line, emit fields with explicit keys.

Why structured logs help

Structured logs make it harder for attackers to alter the meaning of a record because:

  • Each field has a defined purpose
  • Parsers do not rely on line breaks or human-readable formatting
  • Security tools can query fields directly
  • Sensitive input can be escaped or encoded consistently

In Rust, crates such as tracing and serde_json are commonly used for structured output.

Example with tracing

use tracing::{info, warn};

fn log_login_failure(username: &str, ip: &str) {
    warn!(
        target: "auth",
        event = "login_failed",
        user = %username,
        client_ip = %ip,
        "authentication failed"
    );
}

fn main() {
    tracing_subscriber::fmt()
        .json()
        .init();

    log_login_failure("alice\nERROR: admin authenticated", "203.0.113.10");
}

With JSON output, the username remains a field value rather than becoming part of the log syntax. That makes newline injection far less effective.

Sanitize untrusted text before logging it

Even with structured logs, you should still sanitize fields that may contain control characters or extremely long values. The safest approach depends on your logging format and operational requirements.

Common sanitization rules

For user-controlled strings, consider:

  • Replacing \r and \n with visible escape sequences
  • Removing ASCII control characters except tab if needed
  • Truncating overly long values
  • Normalizing Unicode if your downstream tooling requires it

A simple sanitizer might look like this:

fn sanitize_log_value(input: &str) -> String {
    input
        .chars()
        .map(|c| match c {
            '\n' => "\\n".chars().collect::<Vec<_>>(),
            '\r' => "\\r".chars().collect::<Vec<_>>(),
            c if c.is_control() => "\\u{FFFD}".chars().collect::<Vec<_>>(),
            c => vec![c],
        })
        .flatten()
        .collect()
}

fn main() {
    let raw = "bob\nWARN: payment approved";
    let safe = sanitize_log_value(raw);
    println!("user={}", safe);
}

This example is intentionally simple. In production code, prefer a reusable utility that also enforces length limits and consistent encoding.

Avoid multiline log messages for untrusted input

Multiline logs are convenient for stack traces and diagnostics, but they are risky when they include attacker-controlled data. A newline can visually split one event into many, especially in plain-text logs.

Better patterns

Use one of these approaches:

  • Log untrusted values as single-line escaped fields
  • Store detailed context in structured metadata
  • Keep stack traces separate from user input
  • Encode payloads when raw text must be preserved

Example: safe single-line formatting

fn log_request_error(path: &str, reason: &str) {
    let path = sanitize_log_value(path);
    let reason = sanitize_log_value(reason);

    println!("level=ERROR event=request_failed path={} reason={}", path, reason);
}

This keeps each record visually stable and easier to parse by log collectors.

Use a logging format that is easy to parse safely

Not all log formats are equally resistant to forgery. Plain text is readable, but it is also easier to manipulate. JSON and other structured formats are generally better for security-sensitive systems.

FormatStrengthsWeaknessesForgery resistance
Plain textHuman-readable, simpleEasy to inject separators and fake prefixesLow
JSONStructured, machine-friendlyRequires escaping and consistent schemaHigh
Key-valueCompact, readableParsing can be ambiguous if values are not escapedMedium
Binary logsEfficient, rigid schemaHarder to inspect manuallyHigh

If you use plain text, be strict about escaping and delimiters. If you use JSON, ensure your logger serializes values rather than concatenating strings manually.

Treat log levels as application-controlled, not user-controlled

Never allow user input to determine the severity of a log entry. An attacker should not be able to make a harmless event appear as ERROR, nor should they be able to suppress important events by forcing a lower level.

Unsafe pattern

fn log_event(level: &str, message: &str) {
    println!("{}: {}", level, message);
}

If level comes from the request, the attacker can forge the severity.

Safer pattern

enum EventLevel {
    Info,
    Warn,
    Error,
}

fn log_event(level: EventLevel, message: &str) {
    let message = sanitize_log_value(message);

    match level {
        EventLevel::Info => println!("INFO: {}", message),
        EventLevel::Warn => println!("WARN: {}", message),
        EventLevel::Error => println!("ERROR: {}", message),
    }
}

Application code should decide the level based on behavior, not on external input.

Protect downstream log consumers

Log forgery is not only a problem for the application that writes logs. It also affects SIEM systems, alerting pipelines, dashboards, and incident response tools. If those systems parse logs incorrectly, an attacker can hide in plain sight.

Best practices for consumers

  • Parse structured logs with a strict schema
  • Reject malformed records instead of guessing
  • Preserve original raw events for forensic review
  • Separate application logs from audit logs
  • Avoid regex-based parsing when a schema is available

If your logs are consumed by multiple systems, test them with malicious payloads. For example, verify that a newline in a username does not create a second event in your dashboard.

Build an audit-friendly logging strategy

Security-sensitive applications often need two kinds of logs:

  1. Operational logs for debugging and monitoring
  2. Audit logs for security events and compliance

Audit logs should be especially resistant to forgery because they may be used to reconstruct user actions or prove that a control was enforced.

Recommended audit log properties

  • One event per record
  • Immutable or append-only storage
  • Timestamp generated by the server
  • Actor identity recorded separately from message text
  • Clear event identifiers
  • Minimal free-form text

Example audit event

use serde::Serialize;

#[derive(Serialize)]
struct AuditEvent<'a> {
    event_id: &'a str,
    action: &'a str,
    actor: &'a str,
    target: &'a str,
    outcome: &'a str,
}

fn write_audit_event(event: &AuditEvent<'_>) {
    let json = serde_json::to_string(event).unwrap();
    println!("{}", json);
}

By serializing a typed structure, you reduce ambiguity and make it easier to validate the shape of each record.

Handle secrets carefully in logs

A related risk is accidental secret disclosure. Even if logs are not forged, they can still become a security liability if they contain passwords, tokens, session IDs, or API keys.

Never log these values directly

  • Passwords
  • Access tokens
  • Refresh tokens
  • Private keys
  • Session cookies
  • One-time recovery codes

If you need to correlate events, log a truncated or hashed identifier instead.

Example: log a token fingerprint, not the token

use sha2::{Digest, Sha256};

fn token_fingerprint(token: &str) -> String {
    let digest = Sha256::digest(token.as_bytes());
    hex::encode(&digest[..8])
}

fn log_auth_event(token: &str) {
    let fp = token_fingerprint(token);
    println!("auth_event token_fp={}", fp);
}

This preserves observability without exposing the secret itself.

Validate logging behavior with malicious test cases

Security controls are only useful if they are tested. Add tests that simulate hostile input and verify that logs remain well-formed.

What to test

  • Newline injection
  • Carriage return injection
  • Very long values
  • Unicode control characters
  • Empty and null-like inputs
  • Values that resemble log prefixes

Example test idea

#[test]
fn log_values_are_sanitized() {
    let input = "mallory\nERROR: access granted";
    let output = sanitize_log_value(input);

    assert!(!output.contains('\n'));
    assert!(!output.contains('\r'));
    assert!(output.contains("\\n"));
}

If you use structured logging, also verify that the serialized output remains valid JSON and that fields round-trip correctly.

Operational recommendations for production systems

A secure logging design is not just about code. It also depends on deployment and operations.

Practical guidance

  • Centralize logs in append-only storage where possible
  • Restrict who can modify log configuration
  • Rotate logs regularly and preserve integrity metadata
  • Time-sync hosts with a trusted source
  • Separate application logs from security audit logs
  • Monitor for malformed or unusually large log entries

If an attacker can change log formats, disable escaping, or redirect logs to an untrusted sink, forgery defenses become much weaker.

A concise implementation checklist

Use this checklist when reviewing Rust code that writes logs:

  • Prefer structured logs over string concatenation
  • Escape or sanitize all untrusted text
  • Keep each record on one line unless multiline output is explicitly required
  • Do not let user input control severity levels
  • Avoid logging secrets or full credentials
  • Test with newline and control-character payloads
  • Validate downstream parsers and dashboards
  • Use typed audit events for security-sensitive actions

Conclusion

Preventing log forgery in Rust is mostly about removing ambiguity. When logs are structured, sanitized, and generated from trusted application state, attackers have far fewer opportunities to disguise their actions or fabricate events. The result is better monitoring, stronger incident response, and more reliable evidence when something goes wrong.

Rust’s type system and ecosystem make it straightforward to build safer logging pipelines, but the key decisions are architectural: choose structured formats, treat all external input as hostile, and design logs to remain truthful even under attack.

Learn more with useful resources