Security logs often contain untrusted data: usernames, request paths, user-agent strings, filenames, API parameters, and error details. Recording those values is useful, but treating them as preformatted log text can blur the boundary between what happened and data supplied by the requester.
If an attacker-controlled value can create what looks like another log record, an investigator or automated parser may misread fabricated text as an event produced by the application. This is commonly called log injection or log forging.
The defensive goal is not to remove every unusual character from input. It is to preserve the structure of each event from creation through storage and display. This article develops that mental model and shows where structured logging helps, where escaping still matters, and what the control does not solve.
A log event has structure even when it looks like text
Consider a simplified authentication log written as one line:
login_failed user=<username> source=<address>The application controls the event type and field names. The requester may control the username. Those values have different trust levels even though string concatenation can make them look like one piece of text.
The useful model is:
trusted event schema + untrusted field values = one log eventThe untrusted value should remain a value. It should not be able to become a delimiter, a new field chosen by the requester, or another event.
This matters most when logs support security decisions. During an incident, responders may use them to reconstruct authentication failures, permission changes, administrative actions, or suspicious requests. If event boundaries are ambiguous, the evidence becomes harder to interpret reliably.
The threat is confusion, not magical execution
Log injection is sometimes described too broadly. Writing attacker-controlled text to a log does not by itself give that text code-execution privileges.
The direct threat discussed here is representation confusion. An untrusted value is interpreted as part of the logging format rather than as data inside one event. For a line-oriented text log, embedded line separators are an obvious example: a value that is allowed to create another physical line can make the output resemble multiple records.
Other risks depend on the downstream system. A log viewer, terminal, query engine, or export pipeline may have its own parsing and rendering rules. Those components need their own defenses. Do not assume that solving event-boundary injection also solves vulnerabilities in a log viewer or protects secrets accidentally recorded in the first place.
The control therefore reduces the risk that untrusted values forge or reshape application log records. It does not establish that every logged claim is true, nor does it make arbitrary log-processing software trustworthy.
Prefer structured logging over message construction
A strong default is to represent an event as fields rather than assembling a sentence from strings.
Instead of conceptual code like:
log("login_failed user=" + username + " source=" + source)prefer a logging API that keeps values separate:
log_event(
event = "login_failed",
user = username,
source = source
)The exact API depends on the language and logging library. The security property is more important than the syntax: the application defines the event schema, while untrusted values occupy designated fields.
A serializer can then encode the event into a format such as JSON or another structured representation. Correct serialization represents control characters inside string values according to the format instead of letting those characters redefine the record structure.
For example, the logical event might be represented as:
{"event":"login_failed","user":"example-user","source":"192.0.2.10"}This does not mean “JSON is secure.” It means a well-defined serializer has a clear job: encode data as data. Hand-building JSON strings with concatenation would recreate the same class of boundary mistake in a different format.
Keep the event name under application control
Structured logging loses much of its value if untrusted input can choose the schema.
Suppose an application accepts an action name from a request and writes that value directly as the security event type. A later query for account_deleted can no longer distinguish an application-confirmed deletion from a requester merely supplying that text.
Event names should describe states or actions the application has actually observed or completed. For example:
request_received
login_failed
permission_change_completedRequest-supplied labels belong in fields:
event = "request_received"
requested_action = <untrusted value>That distinction improves more than injection resistance. It makes logs easier to query because event types have stable semantics rather than reflecting arbitrary input.
Treat rendering as a separate trust boundary
Even a correctly structured stored event eventually has to be displayed. A web console may render it as HTML. A command-line tool may print it to a terminal. An export job may turn it into CSV or plain text.
Each output context has its own encoding rules. A safe JSON string in storage is not automatically safe HTML, and HTML escaping is not the right operation for a terminal or CSV file.
The general flow should be:
untrusted input
|
v
structured event field
|
v
stored representation
|
v
context-appropriate output encodingDo not permanently HTML-escape values before logging just because one viewer happens to be a web page. That mixes storage with presentation and can create double-encoding problems elsewhere. Keep the stored data structurally valid, then let each renderer encode values for its own context.
If logs are printed to interactive terminals, consider how the chosen terminal and viewer handle control characters. The correct treatment is platform- and tool-dependent. A practical design is to use a viewer that renders untrusted strings visibly rather than passing raw control sequences through as presentation instructions.
Plain-text logs need an explicit encoding rule
Sometimes a system genuinely needs line-oriented text logs. Structured logging may be unavailable in a legacy component, or a protocol may define a textual format.
In that case, define how field values are encoded before they enter the record. At minimum, values must not be able to introduce record delimiters. Depending on the format, delimiters between fields may also need escaping or an unambiguous length/encoding scheme.
The important point is to make the transformation reversible or at least unmistakable where operationally useful. Silently deleting characters can merge distinct inputs and make investigation harder. A visible escaped representation usually preserves more evidence than ad hoc removal.
Do not write a custom escaping routine unless the format truly requires one. Prefer an established serializer or logging library whose encoding behavior is documented and tested.
Logging failures should not become application failures
Security logging is useful, but untrusted values should not be able to crash a request merely because the logger cannot serialize them.
Define limits and error behavior deliberately. Very large values can consume storage and make events difficult to inspect, so log only the portion or representation needed for investigation. If truncation is used, mark it clearly rather than making a shortened value look complete.
Encoding errors also need predictable handling. Modern application strings are commonly Unicode, but data can cross boundaries where decoding has already failed. The logging layer should have a documented way to represent invalid or unavailable data without abandoning the entire security event.
These are availability and evidence-quality concerns. They complement event-boundary protection rather than replacing it.
Do not log secrets just because fields are structured
Structured logging makes data easier to separate and query. That can make sensitive data more discoverable if the wrong fields are recorded.
Passwords, session identifiers, reset tokens, API keys, private cryptographic material, and similar credentials generally do not belong in routine application logs. If a value grants authority, storing it in a broadly accessible logging system creates another place from which that authority may leak.
For identifiers that are useful but sensitive, decide what investigators actually need. A stable internal identifier may be more useful than a full request body. In some environments, a carefully chosen derived or redacted value can support correlation while reducing exposure, but the appropriate design depends on the investigation and privacy requirements.
Log injection defenses preserve event structure. Data minimization controls what sensitive material enters that structure in the first place.
Verify the whole logging path
Testing only the logging call misses downstream transformations. A useful test follows an event from application code to the place responders actually read it.
Create inert test inputs containing characters that exercise the chosen format: line separators for line-oriented output, quotes and backslashes for JSON strings, and field delimiters for any custom textual representation. Then verify that one application event remains one parsed event and that the original value is represented as a field rather than new structure.
For a structured pipeline, useful assertions include:
emit one event with an unusual field value
parse stored output with the normal parser
assert exactly one event exists
assert event type is application-controlled
assert field value remains dataThen inspect the normal viewer or export path. The value should be displayed as content, not interpreted as markup or another record according to that output context.
Also test size limits and logger failure behavior. A defensive logging path should remain predictable when values are empty, unusually long, or cannot be represented exactly.
Preserve meaning from source to investigation
The most reliable defense against log forging is architectural rather than a blacklist of suspicious characters. Define security events in application code, keep untrusted values in fields, serialize them with a format-aware library, and apply output encoding when those fields are rendered.
For a small application with trusted operators and a mature structured logger, that may be enough for the event-boundary threat. Systems that feed many collectors, exporters, terminals, or web consoles need defense in depth because every transformation introduces another parser and another possible interpretation.
When reviewing a logging path, ask one concrete question: can data supplied by a requester change the meaning or boundary of the event that contains it? If the answer is no from event creation through the investigator’s view, the log is much more useful as security evidence.