CRLF injection and HTTP response splitting
CWE-113OWASP A03:2021Updated August 31, 20266 min read
CRLF injection is a vulnerability that lets an attacker place a line break inside an HTTP header value, adding headers of their own or splitting the response into two messages. CRLF stands for carriage return and line feed, the two control characters that end every header line in HTTP. The fix is to reject control characters and to set headers only through your framework API.
An HTTP response is plain text with a strict layout: every header line ends in a line break, and a blank line separates the headers from the body. The moment unfiltered user input reaches a header value, an attacker can supply that line break themselves and take over the structure of the message. That is CRLF injection. Here is how it works and how to close it.
What is CRLF injection?
CRLF injection is a vulnerability that lets an attacker place a line break inside an HTTP header value, and with it add headers of their own or split the response into two separate messages. CRLF stands for carriage return and line feed: the two control characters, written as \r and \n, that end every header line in HTTP/1.1. Two of those pairs in a row, in other words a blank line, mark the end of the headers and the start of the body.
An everyday comparison: you dictate an address to a typist who writes down exactly what they hear. If someone says out loud halfway through “new line, sender is Jones”, the typist dutifully starts a new field. They cannot hear which part was your data and which part was an instruction about the layout. An HTTP parser does precisely the same with the characters 13 and 10.
The consequences come in two degrees. With header injection the attacker adds one or more extra header lines to an otherwise normal response, for example a Set-Cookie of their choosing. With HTTP response splitting they also send the blank line and write a complete second response, with its own status line, headers and body.
How does a CRLF injection attack work?
Take an endpoint that returns the user to the page they came from after signing in. The destination comes from a query parameter and goes straight into the Location header. The code assembles the response as text itself, as happens in a hand-written HTTP layer, in a proxy, or in older code that writes to the socket directly.
Vulnerable:
function sendRedirect(socket, next) {
// The response is built as plain text and written out unchecked
const response =
"HTTP/1.1 302 Found\r\n" +
"Location: " + next + "\r\n" +
"Content-Length: 0\r\n" +
"\r\n";
socket.write(response);
}
An ordinary user requests /go?next=/dashboard and is redirected as expected. An attacker puts a URL-encoded line break, %0d%0a, into that same parameter, followed by a header of their choice:
GET /go?next=/dashboard%0d%0aSet-Cookie:%20session=6f21ab%3B%20Path=/ HTTP/1.1
Host: app.example.com
The server decodes the parameter and pastes it into the header. What goes over the wire is this:
HTTP/1.1 302 Found
Location: /dashboard
Set-Cookie: session=6f21ab; Path=/
Content-Length: 0
To the browser, that third line is indistinguishable from a header your application set itself. The attacker therefore decides the victim’s session cookie: they lure someone to the link, the server sets the session identifier they already know, and once the victim signs in, they share that session. This is session fixation, delivered through a header you never wrote.
Send two line breaks in a row and the first response ends, while a new one begins that the attacker fills in, including their own Content-Type and a body containing HTML or script.
The fix has two layers: never let a header carry free text chosen by the user, and use the header API of your framework instead of assembling response text by hand.
Secure:
const ALLOWED_PATHS = new Set(["/dashboard", "/profile", "/settings"]);
function sendRedirect(res, next) {
// 1. Reject every control character, not just CR and LF
if (typeof next !== "string" || /[\x00-\x1f\x7f]/.test(next)) {
res.writeHead(400).end("Invalid parameter");
return;
}
// 2. Allowlist: the header only ever carries a value the server knows
const target = ALLOWED_PATHS.has(next) ? next : "/dashboard";
// 3. Framework API instead of hand-built response text
res.writeHead(302, { Location: target });
res.end();
}
Node.js refuses a header value containing a line break, and most modern frameworks behave similarly. Treat that check as your safety net, not as your design. The allowlist makes sure nothing the user invented reaches the header in the first place, and it keeps holding if a less strict component is ever placed in front.
What is the impact of CRLF injection?
Severity depends on what the attacker can put into the response and on who receives it. If they can only manipulate their own response, it is worth reporting but has no victim. If they can force a header that steers someone else’s browser, the stakes rise: a forged Set-Cookie leads to session fixation and from there to the victim’s account, and a complete second response containing HTML gives cross-site scripting in the context of your own domain.
The most serious variant runs through caching. If a reverse proxy, a CDN or the browser cache stores the split response under the URL of a normal page, it then serves the attacker’s content to every visitor, including people who never clicked the link. One mistake in a single parameter turns into a defacement or a phishing page on your own domain, and it stays there until the cache is purged.
In business terms that means account takeover, reputational damage and, where personal data is involved, a notification duty. Because the reach varies so much per situation, the rating runs from medium to high.
How do you detect CRLF injection?
Start by mapping every place where input reaches a header: the Location of a redirect, a Set-Cookie holding a language preference or a return path, the file name in Content-Disposition on a download, and custom headers that echo a request id or a tenant name.
Testing means placing an encoded line break into such a parameter, followed by a harmless header of your own with a recognisable value. Read the raw response afterwards, using an intercepting proxy such as Burp or curl, rather than the browser view, which hides headers entirely. If your test header appears as its own line, you have a finding. If nothing happens, try a bare line feed and a double-encoded variant.
Test the whole chain rather than the application alone: the application server may reject the value while the proxy in front of it still lets it through. Scanners report the textbook case reliably, but miss what sits behind authentication or what only appears between two servers. AssistSec covers this ground in a penetration test and shows, per finding, the raw response in which the extra header appears.
How do you prevent CRLF injection?
- Set headers through your framework API. Never assemble response text yourself. The built-in functions validate the value and refuse control characters.
- Reject control characters on input. Discard the entire control character range, not just carriage return and line feed, and do it after decoding the parameter.
- Use an allowlist for header values. Let the user pick from a fixed set of paths, languages or file names and translate that choice server-side into the real value.
- Encode values that genuinely have to be dynamic. A file name in
Content-Dispositionbelongs there percent-encoded, and so does a value inside a cookie. - Check the whole chain and keep it patched. Reverse proxies, CDN rules and custom middleware assemble headers too, and outdated components have a long history of bugs here.
- Sanitise your log lines as well. The same characters forge log entries and make post-incident analysis unreliable.
Sources
Frequently asked questions
What does CRLF stand for?
CRLF is short for carriage return and line feed, the two control characters with codes 13 and 10 that together form a line break. In HTTP/1.1 that pair ends every header line, and a blank line, meaning two pairs in a row, separates the headers from the body. Anyone who can put those characters into a header value controls the structure of the message rather than just its content.
What is the difference between CRLF injection and HTTP response splitting?
CRLF injection describes the technique: a line break ends up inside a header value. HTTP response splitting describes its most serious outcome: the attacker also sends the blank line and writes a complete second response with its own status line, headers and body. The intermediate case, where only one extra header line is added, is usually called header injection.
Is CRLF injection still relevant now that frameworks validate headers?
Yes. Modern frameworks reject a line break in a header value, but that check only protects code that actually uses the framework API. Custom HTTP layers, reverse proxies, CDN rules and older components still assemble headers as strings. On top of that, not every hop in the chain applies the same validation.
How severe is CRLF injection?
It depends on who receives the manipulated response. If the attacker can only alter their own response, the impact stays limited. If they can force a Set-Cookie on someone else, or their split response lands in a shared cache, the result is session fixation, cross-site scripting or cache poisoning, and the rating moves up to high.
How do I test a parameter for CRLF injection?
Send a URL-encoded line break followed by a header of your own, for example a test header with a recognisable value, in every parameter that reaches a response header. Then read the raw response rather than the browser rendering. If your test header shows up as a separate line, the parameter is vulnerable; if nothing happens, try a bare line feed and a double-encoded variant.
Related articles
- VulnerabilitiesCWE-79A03:2021Cross-site scripting (XSS)Cross-site scripting (XSS) lets attackers inject malicious scripts into web pages that run in visitors' browsers. Learn how XSS works and how to prevent it.
- VulnerabilitiesCWE-644A03:2021Host header injectionIf your application builds links from the Host header, the attacker decides where they point. Learn how that hijacks password resets.
- VulnerabilitiesCWE-601A01:2021Open redirectOpen redirect explained: how a returnUrl parameter sends visitors to a phishing site, which bypasses work, and how to redirect safely.
- VulnerabilitiesCWE-384A07:2021Session fixationSession fixation explained: how an attacker plants a session id in advance, why logging in keeps it valid, and how rotating the id prevents it.