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.
Attack shape
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
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
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
- Secret-Aware Context Filters: Stop AI Editors From Shipping Keys
- CloudWatch Logs Anomaly Detection: Ignore Deploy Noise, Catch Novel Errors
- Lambda Log Buffering: Cut CloudWatch Ingestion Without Losing Correlation
- Prompt Injection in Trackers: Sanitize Jira Before Agents Read Issues
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
