Web Security

What Is CRLF Injection? HTTP Response Splitting Explained

A complete technical guide to CRLF injection: how unvalidated newline characters let attackers split HTTP responses, and how to detect, verify, and prevent it.

Intermediate11 min·Nyxeara Security Research·2026-09-16·CWE-93
crlf-injectionweb-securityhttpresponse-splitting
Prerequisites
what-is-http

Short answer

CRLF injection (also called HTTP response splitting) is an injection attack that occurs when an application writes user-controlled input into an HTTP response header without stripping carriage return (\r, ASCII 13) and line feed (\n, ASCII 10) characters. An attacker who can inject \r\n can terminate the current response header early, insert arbitrary headers or an entire new response body, and — in many architectures — split a single HTTP response into two, poisoning intermediate caches or serving malicious content under the application's domain.

The idea in one minute

Imagine a public notice board in a train station where staff pin departure updates. The board has a clear format: each sheet of paper lists the train number, platform, and departure time. Now imagine someone walks up with a marker and adds a line-break after a legitimate notice, then writes a fake cancellation notice underneath — same paper, same board, no obvious tampering. Passengers read the fake cancellation as official because it is physically on the station's notice board.

CRLF injection works the same way. HTTP headers are separated by \r\n, and the header section is separated from the body by \r\n\r\n. If the application lets an attacker embed newline characters into a header value, the attacker can "close" the current header block early and write whatever they want into the rest of the response — new headers that override security controls, a fake response body, or a completely second response that a downstream proxy or cache might interpret as a separate HTTP transaction. The server and browser both treat the injected content as legitimate, because it arrived over a legitimate connection and within a legitimate response.

How CRLF injection actually works

HTTP uses \r\n (CRLF) as the delimiter between header lines, and a double \r\n\r\n separates headers from the body:

HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Content-Length: 45\r\n
\r\n
<html><body>OK</body></html>

When an application embeds user input directly into a response header:

Set-Cookie: sessionid=USER_INPUT; HttpOnly

An attacker supplying injected\r\nContent-Length: 0\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body>injected</body></html> causes the response stream to become:

Set-Cookie: sessionid=injected\r\n
Content-Length: 0\r\n
\r\n
HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
\r\n
<html><body>injected</body></html>

The first response (with the injected header) ends at Content-Length: 0 — its body is empty. The bytes that follow are interpreted by the client (or an intermediary proxy) as a brand-new HTTP response. This second response is completely controlled by the attacker, including its status line, headers, and body.

The request flow

Attacker ──crafts request──▶ GET /search?q=foo%0d%0aContent-Length:%200%0d%0a%0d%0a...
           │
           ▼
    Server reflects q= into a header (e.g. Set-Cookie: last_search=foo...)
           │
           ▼
    Response stream now contains: ...\r\nContent-Length: 0\r\n\r\n
    HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html>injected</html>
           │
           ▼
    Downstream proxy or browser sees TWO responses:
    1. The truncated original response (empty body, status 200)
    2. A forged response under the app's domain

As a sequence:

sequenceDiagram
    participant Attacker
    participant Server as Vulnerable Server
    participant Proxy as Reverse Proxy / Cache
    participant Browser

    Attacker->>Server: GET /search?q=foo%0d%0aContent-Length:%200%0d%0a%0d%0a...
    Server->>Proxy: Response: Set-Cookie: last_search=foo\r\nContent-Length: 0\r\n\r\nHTTP/1.1 200 OK\r\n...
    Proxy-->>Browser: Forged second response (attacker-controlled content)
    Note over Proxy,Browser: Cache poisons proxy with attacker's content
    Attacker->>Proxy: Next visitor requests legitimate page
    Proxy-->>Attacker's victim: Serves cached attacker-controlled response

A minimal example

A Flask endpoint that reflects a search term into a response header:

from flask import Flask, request, make_response

app = Flask(__name__)

@app.route("/search")
def search():
    q = request.args.get("q", "")
    resp = make_response(f"<html><body>Search: {q}</body></html>")
    resp.headers["Set-Cookie"] = f"last_search={q}; HttpOnly"
    return resp

A request to /search?q=foo%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<html>injected</html> will cause the Set-Cookie header value to contain raw CRLF sequences. The fix is to strip or reject control characters from any value that will appear in a header:

import re

def sanitize_header_value(value: str) -> str:
    # Remove CR, LF, and null bytes entirely
    return re.sub(r"[\r\n\x00]", "", value)

@app.route("/search")
def search():
    q = request.args.get("q", "")
    resp = make_response(f"<html><body>Search: {sanitize_header_value(q)}</body></html>")
    resp.headers["Set-Cookie"] = f"last_search={sanitize_header_value(q)}; HttpOnly"
    return resp

Most modern web frameworks (Flask, Django, Express, ASP.NET) strip CRLF from header values automatically when using their high-level APIs. The vulnerability surfaces when developers bypass those APIs — for example, by writing raw headers with Response(headers=...) in Python, or constructing header lines with string concatenation in any language.

Why the vulnerability exists

  • HTTP's wire format uses \r\n as a structural delimiter, so any code that constructs header lines by concatenating user input onto a string is vulnerable by design
  • Developers treat header values as opaque strings and do not consider that certain byte sequences are significant to the HTTP parser
  • Logging and redirect parameters are common injection points — a server that reflects the original request URI or a log message into a response header is particularly exposed
  • Legacy or low-level HTTP APIs (raw socket writes, custom CGI scripts, hand-rolled header builders) lack the automatic sanitization that framework-level APIs provide
  • URL-decoded input (%0d%0a) bypasses simple blocklist filters that look for literal \r\n sequences without decoding first

What attackers look for

  • Any response header whose value includes user-controlled input: Set-Cookie, Location, Content-Disposition, custom debug headers like X-Requested-URI, log correlation IDs, error messages that include the original request path
  • Application-level logging endpoints that reflect the request URL or user-agent in a response header
  • Redirect handlers that embed the destination URL in a Location header — a CRLF injection here becomes both an open redirect and a response-splitting vector
  • File download endpoints that set Content-Disposition from a user-supplied filename parameter
  • Any custom header the application writes using raw string formatting rather than a structured header API

Detection

  • Static/code review: search for response header assignments where the value contains a variable derived from user input (request.args, request.form, request.headers, URL path segments). Trace whether the value passes through a sanitization function or a framework API that strips control characters.
  • Dynamic/DAST: inject %0d%0a (URL-encoded CRLF) followed by a unique marker string into every header-shaped parameter and observe whether the marker appears in a response header boundary position. A common test payload: test%0d%0aX-Injected:%20true. If the response contains a header X-Injected: true, the injection succeeded.
  • OOB (out-of-band): inject %0d%0aLocation:%20http://your-server/marker and watch for an inbound HTTP request to your-server. This confirms both the injection and the ability to redirect the client, and is the most reliable way to distinguish true positives from server-side filtering that strips the payload silently.

Verification: real vulnerability or false positive?

A finding is confirmed when:

  • The response body or response headers contain a header name or value you supplied after a CRLF sequence, visible in the raw HTTP response (use curl -v or a proxy like Burp Suite's Repeater to view the raw bytes — browser DevTools often normalize or hide injected headers)
  • An out-of-band callback is received from the target server or an intermediary (cache, reverse proxy) after injecting a Location or Set-Cookie payload containing your server's URL
  • The injected payload results in observable cache poisoning: a subsequent request to the same URL (without the payload) returns content you injected, served from the cache

A response that echoes back the payload URL-decoded but enclosed within a single header value (e.g., X-Debug: test\r\nX-Injected: true appearing as a single mangled header rather than two distinct headers) is not a finding — the server likely sanitizes or encodes the value. Confirmation requires that the HTTP parser actually splits on the injected CRLF.

Real-world impact

CRLF injection is rated higher than open redirect because its consequences extend beyond redirection:

  • HTTP response splitting: a single incoming request produces two HTTP responses. The second response is attacker-controlled and can include arbitrary HTML, JavaScript, or headers. If an intermediate proxy or cache interprets the second response as a separate transaction, the cache may store attacker-controlled content under the application's URL — poisoning every subsequent visitor's view.
  • Cache poisoning: the injected second response overwrites the cache entry for the attacked URL (or a different URL, depending on the Host header in the forged response). Visitors who request the legitimate page receive the attacker's content without ever seeing the injected URL. This is the most dangerous impact because it turns a single injection into a mass-distribution vector.
  • Header injection without splitting: even in HTTP/2 (where response splitting is structurally impossible because framing is binary, not delimiter-based), header injection remains possible — an attacker can inject Set-Cookie or override Content-Security-Policy by embedding CRLF in a header value, influencing the browser's security posture for the current request.
  • XSS chaining: if the injected content includes a <script> tag, every user who hits the poisoned cache endpoint executes arbitrary JavaScript in the context of the application's origin.
  • Cookie manipulation: injecting Set-Cookie headers via CRLF allows an attacker to set session cookies, track the user, or potentially fixate a session.

Prevention

  • Use framework-level header APIs rather than constructing header strings manually. Flask's response.headers["Name"] = value, Django's HttpResponse, Express's res.setHeader(), and ASP.NET's Response.Headers all strip or reject CRLF. The vulnerability exists only when these APIs are bypassed.
  • Validate and sanitize all user input that will appear in a response header: strip \r, \n, and \x00 bytes before the value reaches the header builder. A deny-list of these three characters is sufficient — no need for a comprehensive blocklist.
  • Encode the value for the context: if the value is embedded in a Location header, URL-encode it; if in Set-Cookie, ensure the value does not contain cookie-delimiter characters (;, =, ). This is defense-in-depth; the primary defense is stripping control characters.
  • Prefer structured data over header reflection: instead of reflecting user input in a custom X- header, consider JSON-encoding the data in the response body and emitting a single Content-Type: application/json header. This eliminates the injection surface entirely.
  • Register a CSP with report-uri or report-to: while this does not prevent injection, it provides visibility into attempts to inject script content via response splitting.

Related vulnerabilities

  • Open redirect — CRLF in a Location header is both an open redirect and a response-splitting vector; the same payload enables both
  • XSS — injected CRLF can deliver arbitrary HTML/JavaScript as the forged response body, or override Content-Security-Policy header in the same response
  • HTTP request smuggling — response splitting and request smuggling both exploit parser disagreements between front-end and back-end, but smuggling targets request boundaries while splitting targets response boundaries
  • Cache poisoning — CRLF injection is a primary technique for web cache poisoning attacks

Testing methodology (do this safely)

  • Test only on applications you own or have explicit authorization to test. CRLF injection combined with cache poisoning can affect real users of shared hosting or CDN-backed applications.
  • Use a callback-capable testing tool (Burp Collaborator, Interactsh, or a server you control) to confirm out-of-band injection — the presence of your payload in the response body alone is insufficient to prove response splitting.
  • Test with curl -v to inspect raw response bytes. Browser DevTools normalize headers and will not show injected headers as separate entities; use a network proxy that preserves raw HTTP framing.
  • If testing behind a reverse proxy or CDN, inject a Host header in the forged response to target a specific cached URL — this confirms whether cache poisoning is practically achievable.
  • Document the exact injection point, the raw bytes observed in the response, and whether an out-of-band callback was received.

Further reading

  • OWASP Cheat Sheet Series: CRLF Injection Cheat Sheet
  • PortSwigger Web Security Academy: Request smuggling (response splitting is covered in the smuggling context)
  • MITRE: CWE-93 — Improper Neutralization of CRLF Sequences ('CRLF Injection')

Nyxeara perspective

CRLF injection is among the most underrated web vulnerabilities on modern platforms. Many teams assume it has been eliminated by framework-level safeguards, but it reappears whenever developers write header values through string concatenation — typically in custom middleware, legacy CGI scripts, or low-level network handlers. Automated detection should combine in-band probes (injecting a unique header name after a CRLF and checking for its presence in the raw response) with out-of-band confirmation (injecting a Location pointing to a callback server). The out-of-band check is essential because many modern reverse proxies collapse or reject malformed responses silently, producing a false-positive in-band injection but no actual splittable response. When a true positive is confirmed, the priority is immediate remediation: header injection enables cache poisoning, which transforms a single-request exploit into a mass-impersonation attack against every user of the affected cache.

Published 2026-09-16 · Updated 2026-09-16