Security logs are useful only when their records mean what investigators and monitoring systems think they mean. A login failure, permission change, or rejected request may contain values supplied by a client. If an application simply joins those values into a line of log text, special characters can blur the boundary between the application’s event and the data inside it.
That problem is called log injection or log forging. The defensive goal isn’t to remove every unusual character from user input. It is to make sure untrusted data remains data when it reaches the logging format. This article develops that mental model, shows why structured logging helps, and explains what still needs attention after the event boundary is protected.
A log record has its own syntax
Consider a deliberately simple text log:
2026-09-11T03:40:00+07:00 WARN login_failed user=aliceThe application intends this to be one event. The newline ends the record, while spaces and = help a human or parser interpret fields.
Now suppose the application constructs the message by concatenating an untrusted username:
log("WARN login_failed user=" + username)The problem isn’t that usernames are inherently dangerous. The problem is that the value and the log format share the same character stream. If the logging sink treats a carriage return, line feed, delimiter, quote, or other character as syntax, an untrusted value containing that character may change how the resulting record is parsed or displayed.
A useful mental model is:
application event = trusted structure + untrusted field valuesThe structure should come from application code. Untrusted values may fill fields, but they shouldn’t be able to create new fields or records.
What log injection can damage
Suppose a system records failed logins and later alerts when a particular account has many failures. If an attacker-controlled value can make one physical log record appear to be two logical records, downstream tooling may ingest misleading data. A human reviewing raw logs can also mistake attacker-supplied text for an event generated by the application.
The consequence depends on how the logs are used. Corrupted records can make investigations harder, create false signals, hide useful context among misleading entries, or interfere with parsers that expect a particular format. If an automated response trusts attacker-influenced log fields without validation, the operational impact can extend beyond confusing output.
This threat model assumes an untrusted party can influence a value that the application records and that some logging component interprets special characters or delimiters in that value as structure. Protecting the event boundary reduces this risk. It does not prove that the value itself is truthful, stop an attacker who can modify the log store, or make every logged field appropriate to retain.
Prefer structured logging over assembled messages
The strongest general design is to pass the event type and values to a logging API as separate fields rather than first constructing an informal line of text.
For example, use an interface conceptually like this:
log_event(
level = "WARN",
event = "login_failed",
user = supplied_username
)A structured logging implementation can then encode the record for its destination. In a JSON-based sink, a value containing a newline is represented as data inside a JSON string rather than as an unescaped record separator. Other formats need their own correct encoding rules.
This distinction matters. Validation asks whether a value is acceptable for the application’s business rules. Encoding represents an accepted value safely inside another syntax. They solve different problems.
A username policy might reject control characters because they have no legitimate purpose in a username. That is useful validation. But the logger should still encode values correctly because many legitimate fields, such as an error description or user-agent string, can contain punctuation that has meaning to the output format. Relying on every caller to pre-sanitize every value makes the security property fragile.
Keep the event name under application control
Structured logging is easiest to reason about when the event schema is stable. Instead of putting an entire client-supplied message into an event name, choose a fixed application-defined identifier and attach relevant input as fields.
For a rejected request, this is clearer:
event = "input_validation_failed"
field = "display_name"
reason = "contains_disallowed_control_character"than this:
event = "validation failed: " + raw_request_valueThe first form separates the security fact from the evidence associated with it. Monitoring can reliably match input_validation_failed, while the untrusted value can be encoded, shortened, redacted, or omitted according to policy.
This also reduces a common temptation: logging the complete hostile payload just because it triggered a security rule. For many detections, the rule identifier, affected parameter, source context, and outcome are more useful than a verbatim copy of the input. Recording less untrusted content reduces both injection risk and the chance of placing secrets or personal data into logs.
Encoding must match the actual sink
“Use structured logs” isn’t enough if the structure disappears later in the pipeline. An application might emit JSON correctly, then a collector might flatten selected fields into delimiter-separated text, or an operator-facing dashboard might render a field in another context.
Treat each conversion as an output boundary. The component producing a format should use the encoder for that format rather than inventing substitutions such as “replace newline with a space” and assuming the problem is solved everywhere.
For line-oriented text that cannot be replaced, the logging layer should define how control characters and delimiters are represented so one event cannot become another record. For JSON, use a JSON serializer rather than hand-building quoted strings. If logs are later rendered into HTML, terminal output, SQL queries, or another syntax, that destination has separate safety requirements.
The principle is portable even though APIs differ between languages and logging products:
untrusted value
-> validated for its application meaning
-> passed as a field
-> encoded by the producer of the destination formatDo not decode or “clean up” the value later in a way that silently restores characters that the next parser treats as structure.
Put bounds on what you record
Correct encoding preserves the record boundary, but it doesn’t make unlimited input harmless. A client may be able to submit a very large value repeatedly. Logging every byte can increase storage, ingestion, indexing, and alert-processing costs, and in extreme cases can contribute to resource exhaustion.
Set field-size limits based on what investigators actually need. If a request contains a multi-megabyte body, a security event often needs metadata such as the validation rule, parameter name, request identifier, and perhaps a carefully bounded excerpt rather than the complete body.
The limit should be applied before expensive downstream processing where practical. When a value is truncated, make that fact explicit so an investigator doesn’t mistake the shortened representation for the complete original value.
Keep secrets out even when encoding is correct
Log injection and sensitive-data exposure are separate risks. A perfectly encoded access token is still an access token in a system that may be searchable by many operators, copied into backups, or retained for months.
Passwords, session identifiers, access tokens, cryptographic keys, connection credentials, and similar secrets generally shouldn’t be written directly to application logs. Sensitive personal data also needs a deliberate collection and retention decision. Where correlation is genuinely necessary, consider a non-secret identifier or an appropriate one-way representation rather than the original secret.
Redaction should happen before the sensitive value reaches ordinary logging infrastructure. Trying to remove secrets only after central collection leaves copies in intermediate buffers, agents, files, or transport systems.
Test the boundary, not just the happy path
A useful log-injection test sends representative untrusted values through the real logging pipeline and checks the records at the destination that investigators actually use. Include characters that are structural in that pipeline, such as carriage returns, line feeds, quotes, backslashes, and configured delimiters.
The expected result is not necessarily that those characters disappear. The expected result is that one application event remains one event, fields retain their intended boundaries, and downstream parsers do not reinterpret field data as trusted metadata.
Also verify length handling and redaction. A security test should be able to answer questions such as:
- Can a client-controlled field create an extra apparent event?
- Can it change the event type, severity, timestamp, or principal field as understood by the collector?
- Does a long value stay within the intended storage and processing bounds?
- Do secret-bearing fields remain absent or appropriately transformed at every logging stage?
Run these checks after changes to logging libraries, collectors, serialization formats, or ingestion pipelines. The application can remain unchanged while a downstream formatting change reintroduces an ambiguous boundary.
Common fixes that are too narrow
Escaping only newline characters can help in one line-oriented format, but it may leave carriage returns or other delimiters meaningful to the same parser. A different sink may use quotes, tabs, commas, or another grammar. Define the format and use its encoder instead of maintaining an informal blacklist of troublesome characters.
Rejecting all punctuation is also the wrong abstraction. It can damage legitimate data and still doesn’t establish correct output encoding. Validation should express what the application accepts; encoding should protect the destination syntax.
Another weak pattern is to sanitize values in selected controllers while allowing other code to call the logger directly. Centralizing structured event creation and encoding gives the application one boundary to test and review.
Finally, don’t treat logs as authoritative statements about client-supplied facts. Encoding can ensure that claimed_email remains one field, but it cannot prove that the email belongs to the requester. Preserve provenance in field names and event design when that distinction matters.
Protect the rest of the logging chain
Preventing log injection protects the meaning of records at creation and transformation boundaries. A useful security logging system also needs controls against unauthorized reading, modification, deletion, and service disruption. Those are separate problems and deserve separate controls such as access restrictions, protected transport, retention policy, monitoring for collection failures, and appropriate integrity protection.
For low-risk diagnostic logs containing tightly constrained application-generated values, a simple logging API may be sufficient. For authentication, authorization, administrative, and other security-relevant events, structured fields, centralized encoding, bounded values, redaction, and end-to-end verification provide worthwhile defense in depth because those records may later drive incident decisions.
Make the log schema part of the security boundary
When adding a security event, decide which parts of the record are application-defined structure and which parts are observations from less-trusted sources. Keep event identifiers and security meaning under application control. Pass outside values as fields, encode them for the actual sink, bound their size, and exclude secrets that the investigation doesn’t need.
The practical next step is to choose one security-relevant event in your application and trace it all the way from the logging call to the final search or dashboard. If a client-controlled value can become record syntax anywhere along that path, fix that boundary rather than trying to teach every caller which characters happen to be dangerous.