CRLF Injection
Updated August 24, 2026
3 min read
CRLF injection happens when user-controlled input containing a carriage return (%0D) and a line feed (%0A) reaches an HTTP response header, letting an attacker append headers of their own — or, with a doubled CRLF, end the header block and control what follows. Modern runtimes reject CR/LF in header values, so it survives mainly in hand-rolled header code, legacy stacks and proxy configs. Rated medium: the impact is real, but it needs a reflection point and a stack that will actually emit the bytes.
What is CRLF injection?
HTTP/1.1 is line-oriented: every header line ends with CR LF (\r\n, %0D%0A), and an empty line — two CRLFs — separates headers from body. Copy attacker input into a header value without stripping those bytes and the attacker stops writing a value and starts writing protocol. HTTP/2 and HTTP/3 are binary and reject CR/LF, though a gateway downgrading to HTTP/1.1 upstream reintroduces the bug.
Two outcomes:
- Header injection (CWE-93): one CRLF appends a header of the attacker's choosing.
- HTTP response splitting (CWE-113): two CRLFs end the header block, so everything after them is parsed as a body.
A third variant never touches HTTP: newlines written to a log file forge log entries (CWE-117). All three sit in the Injection category of the OWASP Top 10 — A05:2025, previously A03:2021.
Usual sinks: Location from ?next=/?url=, Set-Cookie from a username or locale, custom headers echoing X-Forwarded-*, and raw socket writes.
Why it's a risk / how it's exploited
Say /redirect?url= copies its parameter into Location. The attacker sends:
/redirect?url=%0d%0aSet-Cookie:%20session%3Dattacker%0d%0a%0d%0a%3Cscript%3Ealert(1)%3C/script%3E
The server emits a Set-Cookie it never intended — session fixation, one more reason to get cookie flags right — then a blank line, then attacker bytes.
On a 302 the browser follows Location and never renders the body, so this is not instant XSS. It turns serious when something stores the split response: a CDN or proxy caches it and serves the attacker's HTML to later visitors (web cache poisoning), or the bytes land in a response the browser does render.
Preconditions are real. Node's res.setHeader() throws TypeError [ERR_INVALID_CHAR], Django raises BadHeaderError, PHP's header() has refused multi-header values since 5.1.2, Tomcat replaces CR/LF with spaces, ASP.NET encodes or rejects them. What is left: hand-rolled headers, older or embedded servers, raw log pipelines, and — still common — nginx configs interpolating $uri (URL-decoded) into return, add_header or proxy_set_header.
How to detect it
Send encoded CR/LF into every parameter that reaches a header and read the raw response, without following redirects:
# header injection — does a new header line appear?
curl -si --path-as-is "https://target.tld/r?url=%2Fhome%0d%0aX-Injected%3A%20pwned"
# full split — does a body start where headers should be?
curl -si --path-as-is "https://target.tld/r?url=%0d%0a%0d%0a%3Ch1%3Esplit%3C/h1%3E"
curl does not decode %0d%0a, so only a decoding sink turns it into a real CRLF. Vulnerable: X-Injected: pwned arrives as its own header line. Safe: the sequence stays inside the value, the server answers 400, or the bytes are stripped or replaced with spaces. Try double-encoding (%250d%250a) — an edge proxy may decode once and pass a live CRLF to the origin.
How to fix it
Use the header API, never string concatenation, and validate before you write:
// Express — reject, don't silently sanitise
const next = req.query.next ?? '';
if (typeof next !== 'string' || /[\r\n]/.test(next)) return res.status(400).end();
res.redirect(safeInternalPath(next)); // allowlisted path only
In nginx, never interpolate $uri or $document_uri; use $request_uri, the raw target, which cannot carry a real CRLF:
location = /go {
# return 302 https://$host$uri; # vulnerable: $uri is decoded
return 302 https://$host$request_uri; # safe: raw, still-encoded
}
Apache needs mod_headers and mod_setenvif (SetEnvIf backreferences need httpd 2.0.51+) and should emit only values it derived itself:
SetEnvIf Request_URI "^/(dashboard|billing)$" SAFE_PATH=$1
Header always set X-Requested-Path "%{SAFE_PATH}e" env=SAFE_PATH
The anchored allowlist makes it safe. For logs, escape newlines or emit structured JSON.
Caveat: stripping CR/LF makes the header well-formed, not the destination safe — a cleaned Location can still be an open redirect.
FAQ
Is CRLF injection the same as HTTP response splitting? No — CRLF injection is the primitive, response splitting is the escalation. One CRLF appends a header (CWE-93); two CRLFs end the header block so the rest is parsed as a body (CWE-113). Same input bug, very different blast radius.
My framework already rejects \r\n in headers. Am I safe?
For header sinks on that runtime, largely yes. It does nothing for log injection, raw socket writes, or an nginx or CDN layer building headers from decoded variables. Test the deployed edge, not just the app code.
Header reflection points are easy to miss by hand. Run a free website vulnerability scan — Exploita's agent probes redirect, cookie and custom-header sinks and reports only verified, reproducible findings.
