Log Injection Defenses: Sanitize User Fields in Structured JSON Loggers

Log Injection Defenses: Sanitize User Fields in Structured JSON Loggers
13 views
This guide focuses on Log Injection Defenses for production systems, with practical trade-offs for reliability, security, and cost. Classic log injection smuggles newlines into plaintext logs to forge entries. Structured JSON reduces that risk—but only if you actually serialize objects and scrub control characters. User agents, search queries, and ticket titles still break SIEM parsers or plant fake password=... lines when teams string-concatenate.
⚡ TL;DR: Always log via JSON serializers (pino/winston/structlog); never interpolate user strings into format templates as raw lines. Strip/escape \n, \r, \u0000, and ANSI. Cap field lengths. Pair with Secret-Aware Context Filters, CloudWatch Logs Anomaly Detection, and Lambda Log Buffering.

Log Injection Defenses: production guidance

text
User sets display_name = "alice\nERROR auth ok user=admin"
Plaintext logger:
  INFO user=alice
  ERROR auth ok user=admin   ← forged
JSON logger with raw concat can still break if you print strings by hand

Node: pino with redaction and scrubbing

typescript
import pino from "pino";

function scrub(value: unknown): unknown {
  if (typeof value !== "string") return value;
  return value
    .replace(/[\u0000-\u001f\u007f]/g, " ")
    .slice(0, 512);
}

const log = pino({
  level: "info",
  redact: {
    paths: ["req.headers.authorization", "password", "token", "*.secret"],
    censor: "[REDACTED]",
  },
  serializers: {
    user(u) {
      return { id: u.id, name: scrub(u.name) };
    },
  },
});

// GOOD
log.info({ user: { id, name: req.body.name } }, "profile_updated");

// BAD
console.log(`profile_updated name=${req.body.name}`);

Python: structlog / stdlib JSON

python
import json
import logging
import re

_CTRL = re.compile(r"[\x00-\x1f\x7f]")

def scrub(s: str, limit: int = 512) -> str:
    return _CTRL.sub(" ", s)[:limit]

class JsonFormatter(logging.Formatter):
    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "level": record.levelname,
            "msg": scrub(str(record.getMessage())),
            "correlation_id": scrub(getattr(record, "correlation_id", "")),
        }
        if hasattr(record, "user_name"):
            payload["user_name"] = scrub(str(record.user_name))
        return json.dumps(payload, ensure_ascii=False)

# GOOD: extra fields as structured keys
logger.info("profile_updated", extra={"user_name": scrub(name)})

# BAD
logger.info("profile_updated name=%s" % name)

SIEM and CloudWatch considerations

Even with JSON, unbounded user fields inflate ingestion cost and confuse anomaly detectors—cap lengths and drop high-cardinality free text from indices when possible (Lambda Log Buffering, CloudWatch Logs Anomaly Detection). Never log secrets; treat tokens like Secret-Aware Context Filters.

Closing checklist

  • [ ] All services use structured JSON loggers (no raw console concat)
  • [ ] User-controlled strings scrubbed of control chars and length-capped
  • [ ] Redaction paths cover auth headers, passwords, tokens
  • [ ] CI lint bans console.log(\…${user}) patterns where feasible
  • [ ] SIEM parsers tested against adversarial newline payloads
  • [ ] Correlation IDs remain trusted server-side values

Related reading

Last updated on September 11, 2026 Related: Structured Outputs for Codegen: JSON Schemas That Actually Compile; Structured Clone vs JSON: Faster Worker Message Passing in Node; Code Generation with LLMs: Structured Output, AST Manipulation, and Avoiding Hallucinated APIs.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.