Security logs often contain values that originated outside the trust boundary: usernames, request paths, HTTP headers, device names, search terms, object identifiers, and error details. Those values can be useful during incident response, but they can also become an attack surface when an application inserts them into log records without safe encoding.
A malicious value containing line breaks or terminal control data can distort a text log, create a record that appears to come from the application, or make an analyst misread the sequence of events. The same input can also cause trouble farther downstream when collectors and parsers disagree about record boundaries.
The practical rule is: treat every untrusted log value as data, preserve event boundaries, and let a structured logging system encode fields for its output format.
A log record is a security boundary
Suppose an authentication endpoint writes failed attempts with string concatenation:
log.Printf("login failed user=%s ip=%s", username, remoteIP)For a normal value such as mina, the result is unsurprising:
login failed user=mina ip=192.0.2.40Now consider a submitted username containing a newline:
mina\nadmin action approved user=rootA line-oriented sink can display this as two apparent records:
login failed user=mina
admin action approved user=root ip=192.0.2.40The attacker has not performed the administrative action. The attacker has altered the presentation of evidence. That distinction matters during alert triage, forensic review, compliance checks, and automated parsing.
This class of problem is commonly called log injection or log forging. Its core issue is not limited to newline characters. Any data that changes the interpretation of the logging format can become significant.
Keep data separate from log syntax
A safer design gives each event a fixed message and records external values as structured fields:
logger.Info("login failed",
"user", username,
"ip", remoteIP,
)If the logger emits JSON, a correct encoder represents embedded control characters inside the JSON string rather than allowing them to create a second JSON record. For example, the conceptual output can resemble:
{"event":"login failed","user":"mina\nadmin action approved user=root","ip":"192.0.2.40"}The newline remains part of the user value. It does not become a record separator.
Structured logging is strongest when application code passes native field values to the logger. Building JSON manually with string formatting simply moves the injection problem into another syntax.
Avoid code such as:
line := fmt.Sprintf(`{"event":"login failed","user":"%s"}`, username)
log.Print(line)A proper JSON encoder knows how to escape quotes, backslashes, and control characters according to the format. Hand-built output is easy to get wrong.
Preserve one event from producer to storage
Safe encoding at the application layer is necessary, but the full logging path also matters. A production event may travel through several components:
application
|
v
logging library
|
v
stdout or local socket
|
v
collector or agent
|
v
transport
|
v
central log store
|
v
query and alerting toolsEach stage needs an unambiguous event boundary. If the application emits JSON objects separated by newlines, embedded newlines inside string fields must be encoded. If a transport uses explicit message framing, the receiver should honor that framing rather than reconstructing records from arbitrary text.
Test the complete path with hostile field values. A local logger can behave correctly while a collector later splits, truncates, or transforms the event in an unsafe manner.
Useful test inputs include:
ordinary-user
line1\nline2
carriage\rreturn
tab\tvalue
quote"value
backslash\\valueThe expected result is one stored event for each application event, with each field recoverable as data.
Control characters need deliberate handling
Newline and carriage return are especially important in line-oriented systems, but other control characters can also affect terminals, text viewers, and legacy parsers.
There are two common defensive approaches:
- use a structured encoder that safely represents control data in the selected serialization format; or
- when a downstream format cannot represent such data safely, normalize or reject prohibited control characters before logging.
Do not apply broad character deletion without considering investigative value. A security event may need the original semantic value for analysis. Encoding is often preferable because it preserves information while preventing that information from becoming syntax.
If normalization is required, make it explicit and deterministic. For example:
func logSafeText(s string) string {
s = strings.ReplaceAll(s, "\r", `\r`)
s = strings.ReplaceAll(s, "\n", `\n`)
return s
}This narrow helper can be suitable for a legacy line-based sink, but it is not a universal sanitizer. The required encoding depends on the destination format. JSON, CSV, syslog, terminal output, and SQL storage have different syntax and framing rules.
Do not confuse log safety with input validation
A username field may have strict application rules, and those rules can reduce the set of values that reach the logger. Input validation is still not a replacement for safe logging.
Several logged values cannot be constrained to a tiny character set. Request paths, user-agent strings, external error messages, uploaded filenames, and federation attributes can legitimately contain varied text. Logging code must remain safe even when upstream validation changes or an unexpected path records rejected input.
Treat validation and output encoding as separate controls:
input validation
-> decides whether data is acceptable for an operation
log encoding
-> decides how data is represented safely in a log formatA rejected value can still appear in a security event, so the second control remains necessary.
Keep secrets out of logs
Encoding makes a value syntactically safe. It does not make the value appropriate to store.
Security logs should avoid credentials and other high-impact secrets. Common examples include passwords, session identifiers, bearer credentials, API credentials, private keys, password-reset tokens, and full authorization headers.
Prefer a deliberate field allowlist for sensitive events. Record the minimum information needed to answer operational and investigative questions, such as:
event type
principal identifier
request identifier
source network information
target resource identifier
result
policy decision
server timestampFor a credential-bearing request, record that authentication failed rather than copying the supplied secret into the event.
Redaction also needs structure. A regex applied to an already-formatted log line can miss alternate encodings or remove unrelated text. It is safer to decide which fields may be logged before serialization.
Record trustworthy context separately
Security events often mix attacker-controlled claims with server-observed facts. Keep those concepts distinct.
For example, a request might contain a claimed account name while the authenticated session resolves to a stable internal principal identifier. Record both only when both are useful, and label them clearly:
{
"event": "profile_update_denied",
"claimed_user": "admin",
"principal_id": "usr_8f2c",
"result": "denied"
}Do not let an untrusted field choose the event name, severity, timestamp, or authorization result. Those fields should come from trusted application state.
This separation makes queries more reliable. An analyst can distinguish what the requester supplied from what the server established.
Use server-generated timestamps
An audit trail should use a timestamp generated by a trusted logging component or application host. A client-supplied timestamp can be recorded as a separate data field when needed, but it should not replace the event’s authoritative time.
Clock synchronization also matters across services. Centralized time synchronization and consistent timestamp formats make cross-service investigations easier. UTC with an explicit offset or a standard machine-readable representation avoids ambiguity between regions.
For distributed requests, a request or trace identifier can connect related events without forcing analysts to infer ordering from timestamps alone.
Protect integrity after events are written
Safe encoding protects the boundary between data and log syntax. It does not address every threat to audit data.
A robust logging design also considers:
- restricted write and delete permissions;
- separation between application operators and audit administrators where appropriate;
- authenticated transport to central collectors;
- retention controls;
- detection of unexpected gaps or ingestion failures;
- storage capacity limits and backpressure behavior;
- access controls for sensitive log fields.
Applications should also define behavior when the logging destination is slow or unavailable. Blocking every request indefinitely can turn a logging failure into an availability incident, while silently discarding every security event can erase evidence. The correct policy depends on event criticality and system architecture.
Avoid logging raw exception output blindly
Exceptions and upstream service errors can contain external data. Copying them directly into a security log can reintroduce unsafe control data or expose sensitive values.
Prefer a stable event message plus structured diagnostic fields. Map expected errors to controlled codes when possible:
logger.Warn("payment request rejected",
"request_id", requestID,
"error_code", "upstream_rejected",
)Detailed exception material can go to a restricted diagnostic channel if the operational model requires it, with the same encoding and secret-handling controls.
Test the logging boundary as code
Logging security is suitable for automated tests. Tests can submit hostile values and assert that the resulting sink contains exactly one event with the original value represented as data.
A useful test strategy checks four properties:
- one application call produces one parsed event;
- embedded control data does not create extra events;
- trusted fields such as severity and result cannot be overwritten by user data; and
- prohibited secret fields never appear in serialized output.
Test the serializer, not only the human-readable console view. The stored representation is what collectors and analysis tools consume.
A practical review checklist
When reviewing a logging path, check that:
- external values are passed as fields rather than concatenated into log syntax;
- a real encoder serializes the selected structured format;
- record boundaries remain intact through collectors and transports;
- control characters are encoded or deliberately normalized for legacy sinks;
- server-generated fields cannot be replaced by request data;
- sensitive values are excluded before serialization;
- timestamps come from trusted server-side components;
- hostile-value tests cover the complete ingestion path; and
- storage and transport permissions protect audit integrity.
Final perspective
Logs become security evidence only when their structure can be trusted. A system that lets request data alter record boundaries gives an attacker influence over the evidence used to investigate that attacker.
The strongest pattern is straightforward: keep event structure under application control, pass untrusted values as data fields, serialize them with a format-aware encoder, exclude secrets before serialization, and verify that downstream components preserve each event boundary.
That approach turns logging from ad hoc text construction into a controlled security interface.