Description
Log injection occurs when untrusted data reaches line-oriented logs without neutralizing record delimiters. Carriage returns (\r, CR) and line feeds (\n, LF) can introduce or alter record boundaries, allowing forged entries, broken parsing, or misleading investigations.
Python's logging API substitutes positional arguments (args) into msg. Separating formatting arguments, as in logging.info("User: %s", user), does not automatically neutralize CR/LF in user.
Potential impact
- Forged records: Injected lines may appear to have a different severity or format.
- Disrupted analysis: Broken records can interfere with collectors, SIEM parsing, and alerts.
- Unreliable investigations: Misleading timelines and activity records can delay incident response.
Remediation
- Before logging untrusted messages or formatting arguments, replace both CR and LF with safe single-line characters.
- Use a serializer or formatter that escapes delimiters and emits one record per event. Key-value fields alone do not ensure safe structured logging.
- Default compact
json.dumps(...)escapes CR, LF, and control characters from U+0000 to U+001F. Avoid pretty-printing withindentor attacker-controlled serialization options for line-oriented logs. - An allow-list is sufficient only when every permitted value is a trusted, finite CR/LF-free literal. Format and length checks alone do not neutralize delimiters.
- Apply HTML encoding separately when a log viewer renders data as HTML. HTML escaping does not neutralize CR/LF in stored logs.
Examples
Before
python
import logging
from flask import Flask, request
app = Flask(__name__)
@app.route('/search')
def search():
term = request.args.get('term', '')
# CR/LF in term remains even with separate formatting arguments.
logging.info("Search term: %s", term)
return "ok"
After
CR/LF replacement
python
import logging
from flask import Flask, request
app = Flask(__name__)
@app.route('/search')
def search_safe():
term = request.args.get('term', '')
safe_term = term.replace("\r", " ").replace("\n", " ")
logging.info("Search term: %s", safe_term)
return "ok"
Compact JSON serialization
python
import json
import logging
from flask import request
def log_search():
term = request.args.get('term', '')
logging.info(json.dumps({"event": "search", "term": term}))
Explanation:
- Before: CR/LF in
termsurvives formatting and may make one event look like several records. - CR/LF replacement: Both delimiters are replaced before logging.
- Compact JSON: Default
json.dumps(...)escapes control characters inside strings.indentmakes the serialized record itself multiline, so omit it for line-oriented logs.
Usage considerations
Record boundaries depend on the final handler and formatter. Check CR/LF handling at the actual output and apply separate HTML encoding in the log viewer.
References
- CWE-117: Improper Output Neutralization for Logs
- OWASP Logging Cheat Sheet
- OWASP Top 10:2025 A09 - Security Logging and Alerting Failures
- OWASP Top 10:2021 A09 - Security Logging and Monitoring Failures
- OWASP ASVS 5.0 V16 - Security Logging and Error Handling
- Python 3.14
loggingdocumentation - Python 3.14
jsondocumentation - Flask 3.1 logging documentation