Security logs are useful only when their meaning survives the journey from an application to the people and systems that read them. If untrusted text can change where one event appears to end, create convincing fake fields, or confuse a downstream parser, an attacker may be able to make suspicious activity harder to interpret.

This problem is commonly called log injection or log forging. It happens when data controlled by an external party is treated as part of the log format rather than as data inside a log event.

The practical defense is not to remove every unusual character from every input. It is to keep the event structure under application control, place untrusted values into explicit fields, serialize those fields correctly, and encode them again when a viewer renders them into another format. This article develops that mental model and shows how to verify it.

The boundary is between event structure and event data

Imagine an application records failed sign-ins with a hand-built text line:

WARN login_failed user=<username> source=<address>

The words WARN, login_failed, user=, and source= are structure chosen by the application. The username is data supplied by a requester.

If the logger simply concatenates the username into the line, the program is asking one string to carry two different kinds of information:

trusted format + untrusted data -> one unstructured string

That becomes risky when the log format gives special meaning to characters that the untrusted value may contain. A line-oriented collector, for example, may treat a newline as the boundary between events. A key-value parser may assign meaning to separators. A web-based log viewer may later interpret characters according to HTML rules.

The important security question is therefore not “Does this value contain bad characters?” It is “Can this value alter the structure expected by the next component?”

That question scales better because logging pipelines often cross several trust boundaries: application code writes an event, a logging library serializes it, an agent transports it, a backend indexes it, and a viewer renders it.

See the failure with the smallest useful example

Suppose a failed sign-in handler constructs this message:

"login_failed user=" + username

For an ordinary username, the output is straightforward:

login_failed user=maya

Now consider a username that contains a line break followed by text that resembles another event. The exact attacker text is not important. The failure is that one logical value can become multiple physical log lines:

login_failed user=<first part of input>
<remaining input that looks like another log entry>

A human reviewing the file may mistake the second line for an event produced by the application. A line-based ingestion system may index it separately. Detection rules that count or correlate events can then receive misleading data.

This example is intentionally simplified. Modern logging libraries and collectors vary in how they escape, frame, and transport records. The defensive lesson is independent of a particular library: untrusted data must not be allowed to define record boundaries or field structure.

Log injection does not require the attacker to gain code execution. The attacker only needs influence over a value that reaches a log sink whose formatting or downstream interpretation is ambiguous.

Prefer structured events over hand-built log lines

A structured event keeps fields separate until a logging component serializes them. Conceptually, application code should express an event like this:

event = {
  event: "login_failed",
  user: supplied_username,
  source: client_address
}
logger.write(event)

The example is pseudocode, not a framework-specific API. Its purpose is to show ownership: the application chooses the field names and event type; the requester supplies only the value stored in user.

A suitable structured logger can then serialize the record into JSON or another defined event format. If the username contains a newline, a correct JSON serializer represents that character inside the JSON string rather than emitting it as an unescaped record boundary.

This changes the security property:

hand-built text:
untrusted value can compete with formatting characters

structured event:
untrusted value occupies a defined field
serializer controls representation

Use the logging library’s field or parameter API rather than constructing JSON manually. Correct JSON requires escaping quotation marks, backslashes, control characters, and other syntax according to the format. String concatenation recreates the same boundary problem in a different syntax.

Structured logging also helps operationally. Detection rules can query a stable field such as event = login_failed instead of parsing prose whose wording may change. That is not a complete security guarantee, but it reduces ambiguity between data and metadata.

Keep security-relevant fields under trusted control

Structure alone is not enough if an attacker can choose the values of fields that your monitoring treats as authoritative.

Suppose the application logs this event:

{
  event: "authorization_denied",
  account_id: authenticated_account_id,
  requested_object: object_id,
  source: observed_source
}

The account_id used for investigation should come from the authenticated server-side identity, not from a request parameter that merely claims an account ID. Similarly, a security decision such as authorization_denied should be generated from the application’s actual control flow rather than copied from client input.

This creates a useful distinction:

  • event structure should be defined by trusted code;
  • security assertions should come from the component that actually knows them;
  • untrusted observations may be logged, but they should remain clearly identified as observations.

For example, an HTTP header supplied through a request can be useful evidence, but its presence does not automatically make its contents a trustworthy statement about the original client. Infrastructure-specific headers become authoritative only when the application has a defined trusted proxy boundary and rejects or overwrites values from untrusted paths.

Good logs preserve that provenance instead of turning attacker-controlled claims into trusted facts.

Validation and logging solve different problems

Input validation is still useful, but it should enforce the application’s data rules rather than serve as the only log-injection defense.

If usernames are defined to contain a limited set of characters and have a maximum length, reject values outside that contract at the normal validation boundary. This reduces surprising inputs throughout the application, including in logs.

But many legitimate fields are intentionally broad. Search queries, document titles, user-agent strings, error details, and free-form names can contain punctuation or line breaks. Stripping characters only because they are inconvenient for one log format can corrupt useful evidence and change legitimate data.

A better separation is:

validation:   Is this value valid for the application?
serialization: How is this valid or invalid value represented safely in a log event?

Even rejected input may need to be logged for diagnosis or detection. The logger must therefore handle hostile or malformed values safely; it cannot assume validation has already made every value harmless.

Length limits deserve special attention. A very large user-controlled field can increase storage costs, overwhelm a viewer, or make important fields difficult to inspect. When full content is unnecessary, log a bounded representation, a length, an identifier, or another deliberately chosen summary. The appropriate limit depends on the field and operational need rather than on a universal number.

Serialization is not the final rendering step

A correctly structured event can become unsafe or misleading later if a viewer inserts a field into another syntax without the encoding that syntax requires.

Consider a log backend that stores a username correctly as a string. A web interface later displays that username in an HTML page. The log serializer solved the JSON or event-format boundary, but it did not solve the HTML boundary.

The pipeline is really:

input -> event field -> serialized record -> stored value -> rendered output

Each arrow may introduce a new interpretation context. The component that renders a value into HTML must use HTML-safe output handling. A component exporting CSV must follow CSV quoting rules. A shell command should generally not be constructed from log values at all; if an operational tool needs to invoke a process, it should use APIs that pass arguments separately and apply the platform’s required controls.

This is why “we escape logs” is too vague to be a useful guarantee. Escaping is format-specific. A representation that is correct for JSON is not automatically correct for HTML, CSV, a terminal, or another parser.

The durable rule is to preserve data as data at every boundary.

Protect the pipeline after the application writes the event

Log injection is only one threat to log integrity. An application can emit well-formed events while a compromised account later deletes or edits them. A logging agent can be misconfigured. A collector can drop events under load. Clock errors can make timelines confusing.

Structured logging does not address those failures.

For security-relevant logs, decide which systems are allowed to write, transport, read, and delete records. Restrict those privileges according to operational need. Where the threat model justifies it, send important events to a separate logging service so compromise of one application host does not automatically grant the same ability to alter retained records.

Retention and availability also matter. A perfectly formatted event is not useful during an incident if it expired too early or was never delivered. Monitor the logging pipeline itself: failed deliveries, parser errors, rejected records, unexpected volume changes, and ingestion delays can all indicate that evidence is becoming incomplete.

These controls complement log-injection defenses. They solve different failure modes and should not be treated as substitutes for correct event construction.

Test the boundaries instead of trusting the happy path

A logging control is easier to trust when tests exercise the characters and sizes that challenge its assumptions.

Start with a test event containing a user-controlled field. Include representative control characters such as newline and carriage return, quotation marks, separators meaningful to the chosen format, and non-ASCII text that the application legitimately accepts. Then inspect the event at several stages:

application event
      |
      v
serialized record
      |
      v
collector/index
      |
      v
viewer

Verify that one application event remains one event after ingestion, that the untrusted value remains inside its intended field, and that the viewer displays the value as data rather than interpreting it as markup or new structure.

Also test oversized values and malformed encodings that your application may encounter. The expected behavior should be explicit: reject at the application boundary, truncate according to a documented logging policy, replace invalid input in a controlled way, or record metadata about the failure. Silent parser-dependent behavior makes incident evidence harder to reason about.

Finally, test detection logic against the stored structured fields rather than only against rendered text. A dashboard can look correct while an ingestion rule has split, dropped, or renamed data underneath it.

Avoid fixes that only move the ambiguity

Several approaches appear to solve log injection but leave important gaps.

Replacing newline characters before concatenating a text log may reduce one line-forging case, but other delimiters can still matter to downstream parsers. It also leaves every caller responsible for remembering the transformation.

Removing all punctuation from user values changes data and may be impossible for legitimate free-form fields. It confuses input policy with output representation.

Encoding a value once at input time is also fragile. The application may later use the same stored value in JSON, HTML, a database query, or another context with different rules. Context-specific serialization and output handling belong near the boundary where that context is created.

And structured logging should not become an excuse to record more sensitive information. Passwords, session tokens, API keys, recovery credentials, and other secrets can create a larger incident if logs expose them. Decide what evidence is necessary and omit or deliberately redact sensitive values before they reach general-purpose logging systems.

Choose the control according to the pipeline

For a small internal program whose logs never contain external input and are read only as plain text, a simple fixed text format may be sufficient. The key assumption is that untrusted data cannot cross into the format.

Once logs contain request data, user identifiers, filenames, headers, error messages, or other externally influenced values, structured fields provide a stronger and more maintainable boundary than ad hoc concatenation. They become especially valuable when logs feed automated detection, centralized collectors, or multiple viewers.

For high-value security events, add defense in depth: trusted sources for security assertions, access control around the logging backend, pipeline health monitoring, appropriate retention, and context-aware rendering. Those controls address integrity and availability problems that serialization alone cannot solve.

The practical decision is straightforward: treat a log event as structured security data, not as a sentence assembled from trusted and untrusted fragments. Keep the schema under trusted control, serialize values with a real logging library, preserve provenance, and verify the complete path from application to viewer. That reduces the chance that attacker-controlled text can rewrite the story your logs appear to tell.