Safeguard
Application Security•

ASVS 5.0 V16.4.1: Encode Log Data to Prevent Log Injection

ASVS V16.4.1 requires every logging component to encode data so attackers cannot forge log lines. Code for Python, Java and Node, plus a test.

Safeguard Security Team
6 min read

ASVS 5.0 V16.4.1 requires that all logging components encode data so that user controlled input cannot inject fake log entries or break the log format. It is a Level 2 requirement.

The core case is a newline. If a username field contains a carriage return and line feed followed by a forged line, the log now shows an event that never happened. The same idea applies to JSON logs (a stray quote breaks the structure) and to logs that are viewed in a web console (markup in a message runs in the viewer's browser).

Why it matters

Logs are evidence. During an incident the team trusts them to say what happened. Log injection lets an attacker hide their own activity, frame another user, or add a line that triggers an alert and sends responders on a detour. When logs are rendered in a browser based viewer, injected script can run with the privileges of whoever is investigating.

See CWE-117 for the weakness class.

How to implement it

Use a structured logger that serializes fields instead of concatenating strings. A JSON encoder escapes control characters for you. For plain text logs, neutralize line breaks and other control characters before writing.

import json, logging, re

CTRL = re.compile(r"[\x00-\x1f\x7f]")

def clean(value: str) -> str:
    return CTRL.sub(lambda m: "\\x%02x" % ord(m.group()), value)

log = logging.getLogger("app")

def login_failed(username: str):
    log.warning(json.dumps({"event": "login_failed", "user": clean(username)}))

In Java with Logback, use a JSON encoder (for example the Logstash encoder) and pass user data as arguments, never inside the format string:

log.warn("login_failed user={}", username);   // parameterized
// not: log.warn("login_failed user=" + username);

In Node, pino and winston serialize objects as JSON by default:

logger.warn({ event: "login_failed", user: req.body.username });

If logs go to an HTML viewer, encode for HTML at display time too. Encoding is context specific, so do it where the data is rendered as well as where it is written.

How to test it

Send a request with a payload such as bob%0d%0a2026-10-19 ERROR admin login succeeded in a field that gets logged. Then read the raw log file or the aggregator record. You should see one entry with the escaped characters, not two lines. Add a second case with a double quote and backslash for JSON output, and a third with a script tag if a web viewer is used.

Automate it: a unit test that logs a hostile string and asserts the output has exactly one line.

Where injection hides

Teams usually fix the obvious login log and miss the rest. Check each place where outside data reaches a log call: request paths and query strings, header values, form fields, uploaded file names, values returned by third party APIs, and exception messages that echo the input that caused them. A parser error such as "invalid value: " followed by the raw string is a common leak of attacker text into a log.

Think about each sink separately. A text file needs newline and control character handling. A JSON pipeline needs proper string escaping. A SIEM that renders fields in a web page needs HTML encoding. A CSV export of logs needs protection against cells that start with an equals sign. One encoding rarely fits all of them, so the rule is to encode for the destination at the point of use.

Length limits help too

Cap the length of any attacker supplied field before logging it. A 5 MB string in a log line is a storage and cost problem, and it can push real events out of retention. Truncate and mark it, for example by writing the first 200 characters plus a length field, so analysts still know something large arrived.

Making it stick

Add a lint rule or code search that fails the build when a log call concatenates strings or uses an f-string with request data. Pair it with a shared wrapper around your logger so the safe path is also the easy path. Review the logging libraries you inherit from frameworks, because the access log of a web server or proxy is also a logging component under this requirement. If you keep dependencies current, you also pick up fixes when a logging library itself had an encoding flaw, and a dependency scan will tell you which version you run.

Common mistakes

  • Sanitizing in one logger but leaving a second logging path (a framework access log, a library) untouched.
  • Escaping only newlines and forgetting other control characters.
  • Building the message with string concatenation, then handing it to a safe logger.
  • Trusting headers such as User-Agent or X-Forwarded-For, which are attacker controlled.

Try it yourself

Static analysis catches the concatenation pattern before it ships.

Click through it at your own pace, or open it full screen.

A worked example

Suppose a search endpoint logs the query so analysts can study abuse. A user submits x%0d%0a2026-10-19T10:00:00Z INFO user=admin action=password_reset_ok. With string concatenation, the log file now holds two lines, and the second looks like a genuine admin event. With a JSON logger the same input becomes one record whose query field contains the escaped characters, and every parser downstream sees one event. The attacker's text is still stored, which is fine, because it is now clearly data.

Keep the original value available where you need evidence, but never let it be interpreted as structure. Add this exact case to your regression suite so the behavior is pinned down.

Quick check

Is a JSON logger enough on its own?

It handles the format, so one record stays one record. You still need to encode for the context where the log is displayed, such as an HTML console.

Where should user data go in a Java log call?

In the arguments, never in the format string. Parameterized calls keep data separate from the template.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.