Safeguard
Vulnerability Analysis

CRLF injection and HTTP response splitting explained

CRLF injection lets attackers forge HTTP headers and split responses. Here's how it works, real CVEs behind it, and how to detect and stop it.

Nayan Dey
Security Researcher
7 min read

CRLF injection is a vulnerability where an application writes attacker-controlled input into HTTP headers or log lines without stripping the carriage-return (0x0D) and line-feed (0x0A) bytes that terminate a header. Because HTTP/1.1 uses a bare CRLF sequence to separate one header field from the next, an attacker who can smuggle %0D%0A into a reflected parameter — a redirect URL, a cookie value, a search term echoed into a log — can forge new header lines, split a single server response into two, or inject fake entries into an audit trail. The vulnerability class is tracked as CWE-93 (Improper Neutralization of CRLF Sequences), with its most damaging variant, CWE-113 (HTTP Response Splitting), first documented publicly by security researcher Amit Klein in a March 2004 Sanctum/Watchfire whitepaper. Two decades later it still surfaces in modern stacks — Python's urllib shipped with an unpatched header-injection flaw until March 2019 (CVE-2019-9740, CVE-2019-9947). Here's how the mechanics work, what an attacker gains, and how to close it off.

What Is CRLF Injection?

CRLF injection is the insertion of carriage-return and line-feed characters (\r\n, or %0D%0A URL-encoded) into data that an application later writes into a context where those two bytes carry structural meaning. In HTTP/1.1, RFC 7230 §3.2 defines the header block as a series of field-name: field-value lines, each terminated by CRLF, with the entire header block ended by an empty line (a second CRLF). If a server takes user-supplied input — say, a redirect_url query parameter — and drops it directly into a Location: header without removing \r\n, the attacker's input is no longer just a value; it becomes protocol syntax. A payload like /home%0D%0ASet-Cookie:%20session=attacker123 turns one header into two, because the application, the proxy, and the browser all parse CRLF as "end of this line, start of the next." The same primitive applies outside HTTP: SMTP headers, HTTP request smuggling, and flat-file or syslog log entries are all delimited by newlines, so CRLF injection into any of them is sometimes called log injection or log forging (CWE-117).

How Does CRLF Injection Turn Into HTTP Response Splitting?

HTTP response splitting happens when injected CRLF sequences let an attacker close out the current response's header block early and start writing a second, fully attacker-controlled response inside the same TCP stream. A vulnerable pattern looks like response.setHeader("Location", request.getParameter("url")) in a Java servlet, or header("Location: " . $_GET['url']) in PHP. If the url parameter contains %0D%0A%0D%0AHTTP/1.1 200 OK%0D%0AContent-Type: text/html%0D%0AContent-Length: 30%0D%0A%0D%0A<html>attacker content</html>, the server emits what looks like one HTTP response followed immediately by a second, attacker-authored response. A browser reading that stream, or — more dangerously — a shared proxy or CDN cache sitting in front of the origin, can be tricked into treating the injected content as a distinct, cacheable response tied to the original request URL. That's the pivot from "header injection" to "response splitting": the attacker doesn't just add a header, they hijack the response stream itself.

What Can an Attacker Actually Achieve With It?

An attacker who controls response splitting can poison shared caches, plant persistent cross-site scripting, and forge session state. If a CDN or reverse proxy caches the attacker's injected second response against the vulnerable URL, every subsequent visitor to that URL — not just the one who clicked the malicious link — receives the attacker's HTML, a technique known as web cache poisoning. Because the injected content includes its own Content-Type and body, an attacker can set Content-Type: text/html and drop arbitrary <script> tags, achieving reflected or cache-persisted XSS even in applications that otherwise encode output correctly in the page body. Injecting a Set-Cookie header lets an attacker fix a victim's session ID before authentication (session fixation), and injecting extra headers into a log-writing call lets an attacker fabricate a fake "login successful" or "admin access granted" line in an audit log that incident responders later trust. In multi-hop architectures, CRLF injection into request headers also feeds HTTP request smuggling, letting one attacker-crafted request be interpreted differently by a front-end proxy and a back-end server sharing the same TCP connection.

Which Real CVEs Show CRLF Injection in Production Code?

Real CRLF injection bugs have shipped in both application servers and core language libraries, not just custom web code. Searching the National Vulnerability Database for CWE-113 returns several hundred entries, with a heavy concentration between 2004 and 2009 against Java EE application servers such as IBM WebSphere and BEA WebLogic, from an era when the Servlet API let developers call response.setHeader() and response.sendRedirect() with raw, unvalidated request parameters. The pattern didn't disappear with Java EE hardening: in March 2019, researchers disclosed CVE-2019-9740 and CVE-2019-9947, both describing CRLF injection in Python's urllib/urllib2 modules, where a crafted URL containing encoded \r\n sequences let an attacker inject arbitrary headers into outbound requests made by any Python application using the standard library's HTTP client. Go's standard library took a proactive stance rather than reacting to a disclosed CVE — starting with Go 1.6 in 2016, net/http began rejecting header field values containing raw control characters at write time, specifically to close off response-splitting vectors before they shipped in production Go services. Node.js followed a similar path, validating header values against control characters in its http module since Node 10. The throughline across all three ecosystems: CRLF injection is a language- and framework-level footgun, not just an application bug, which is why fixes eventually moved into the standard library instead of being left to individual developers.

How Do You Detect and Prevent CRLF Injection?

You prevent CRLF injection by never writing raw, attacker-influenced input into header values, redirect targets, or log lines, and by relying on framework APIs that reject or strip \r\n rather than hand-rolled string concatenation. Concretely: use your framework's structured header-setting APIs (HttpServletResponse.setHeader() on any Servlet container 3.0+, Go's net/http Header.Set(), or equivalent) and confirm they're a version that validates control characters — Java Servlet containers have rejected embedded CR/LF in header values since the Servlet 3.0 specification, so an outdated container or a raw socket write can still reintroduce the flaw. For redirects, validate the target against an allow-list of known paths or domains instead of reflecting user input verbatim. For logging, use structured logging libraries (JSON-line loggers, logrus, zap, structlog) that escape or reject embedded newlines by default, rather than printf-style string concatenation into flat text logs. Static analysis rules mapped to CWE-93/CWE-113 in your SAST pipeline will catch the servlet- and PHP-style header patterns described above, and dependency scanning should flag any HTTP client library below the version that added header-value validation. Finally, test for it directly: send %0D%0A sequences through every parameter that ends up in a redirect, a cookie, or a log statement, and confirm the response headers and log output come back unmodified.

How Safeguard Helps

Safeguard's reachability analysis flags CRLF injection findings from SAST and SCA scans only when the vulnerable header-writing or logging call sits on a path actually exercised by untrusted input, so security teams triage the handful of exploitable instances instead of every setHeader() call in the codebase. Griffin AI, Safeguard's autonomous security analyst, correlates those findings against the specific framework version in use — confirming, for example, whether a Servlet container or HTTP client already validates control characters — to cut false positives on libraries that patched the issue years ago. Safeguard's SBOM generation and ingest give teams a live inventory of exactly which services still run pre-fix versions of libraries like urllib or older Servlet containers, closing the CVE-2019-9740-style blind spot at scale. When a genuine CRLF injection path is confirmed, Safeguard opens an auto-fix pull request that swaps raw header/log concatenation for the framework's validated API, so remediation ships as a reviewable diff rather than a ticket.

Never miss an update

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