CRLF Injection: How It Works and How to Test for It
- ✓CRLF injection abuses the carriage return (%0d) and line feed (%0a) bytes that HTTP uses to separate headers, letting you forge new headers or split the response.
- ✓If user input reaches a response header (a redirect Location, a Set-Cookie value) without newline filtering, %0d%0a lets you inject headers and %0d%0a%0d%0a lets you inject a whole body.
- ✓It enables HTTP response splitting, web cache poisoning, reflected XSS through the header channel, session fixation, and log forging.
- ✓CRLF injection is tested under WSTG-INPV-15 (HTTP Splitting and Smuggling) and maps to A03:2021 Injection in the OWASP Top 10.
- ✓The fix is one rule applied everywhere a header is built: strip or reject CR and LF, and use framework header APIs that refuse raw newlines.
Two bytes. That is all CRLF injection needs: a carriage return and a line feed, %0d%0a, the exact pair HTTP uses to end one header and start the next. Slip them into a value the server writes into a response header and you stop being a user supplying data; you start being the one deciding where headers end. The surprising part is why this still appears in 2026: header values are usually assembled before any output-encoding layer runs, so a string that is perfectly HTML-encoded for the body is wide open in a header.
This post explains the mechanics, walks a confirmed response-splitting exploit byte for byte, shows the cookie-injection and cache-poisoning escalations, and ends on the one-line fix. CRLF injection is tested under WSTG-INPV-15 and maps to A03:2021 Injection.
- What is CRLF injection?
- Two bytes turn a value into a forged header.
- What does a confirmed response-splitting exploit look like?
- How do you test for CRLF injection?
- What can an attacker do with CRLF injection?
- Modern stacks strip newlines, which is exactly why the misses are sneaky.
- How do you prevent CRLF injection?
What is CRLF injection?
CRLF injection inserts carriage-return (CR, %0d, \r) and line-feed (LF, %0a, \n) characters into application output that becomes part of an HTTP response, breaking the intended structure of the message. HTTP uses \r\n to end each header line and a doubled \r\n\r\n to mark the end of headers and the start of the body, so injecting those bytes lets you forge structure the server never intended.
The vulnerable pattern is any place user input lands in a response header: a redirect reflecting a url parameter into Location, a language preference written into Set-Cookie, or a custom header echoing a request value. Reverse proxies and CDNs that copy request headers into responses are a frequently overlooked sink. CRLF injection is the gateway to header injection and response splitting, both tested in the input-validation portion of a web application pentesting checklist.
Two bytes turn a value into a forged header.
It works by smuggling encoded newlines into a value the server writes into a header, so your text after the newline becomes a new header line. Take an endpoint that redirects using a parameter it reflects into Location. Supplying an encoded CRLF appends a header of your choosing, and the raw response confirms it:
GET /redirect?url=/home%0d%0aSet-Cookie:%20sessionid=attacker HTTP/1.1
Host: app.target.tld
HTTP/1.1 302 Found
Location: /home
Set-Cookie: sessionid=attacker <-- forged header, injected by you
Content-Length: 0That injected Set-Cookie appearing as its own line in the raw response is the confirmation. The reason output encoding does not save you here is that the value was newline-stripped for the body, not for the header, and those are different channels. Pinning a known session ID into the victim's browser this way is session fixation; the same primitive scaled up becomes response splitting.
What does a confirmed response-splitting exploit look like?
Doubling the CRLF closes the header block entirely and lets you write your own body, which is full HTTP response splitting, and the injected body is where XSS comes back from the dead. Here is the request and the raw response, with the injected second response and script body called out:
GET /redirect?url=%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(document.domain)</script> HTTP/1.1
Host: app.target.tld
HTTP/1.1 302 Found
Location:
Content-Length: 0
HTTP/1.1 200 OK <-- a second, fully attacker-controlled response
Content-Type: text/html
<script>alert(document.domain)</script> <-- script body the encoder never touchedThe script tag landing in a fresh body is the confirmation. On a recent assessment of a retail site, I chained this with the CDN sitting in front: the response on that path was cacheable, so the poisoned variant got served to every subsequent visitor of the URL, turning a self-only reflection into a stored, cross-user XSS and a far higher CVSS score. Always view the unrendered response in Burp Suite, never the browser, because the browser hides exactly the byte-level structure you are confirming.
How do you test for CRLF injection?
Find every parameter that influences a response header, inject encoded CRLF sequences, and read the raw response for a line you control. Watch redirect (Location), cookie, and any reflected custom headers, and test the double-encoded variant for filters that decode once.
# Core payloads (try each in every header-reflecting param)
%0d%0aSet-Cookie:%20test=crlf
%0d%0aX-Injected:%20yes
%0d%0a%0d%0a<script>alert(1)</script> # splitting + body
# Double-encoded, for a filter that URL-decodes once
%250d%250aX-Injected:%20yes
# Fuzz header-reflecting params at scale
ffuf -w params.txt -u "https://target/redirect?FUZZ=%0d%0aX-Injected:yes"
# match on responses where 'X-Injected: yes' appears as a header lineIf your injected header or body shows up in the raw response, it is confirmed. This is WSTG-INPV-15 and belongs in your penetration testing test cases as an explicit line item.
What can an attacker do with CRLF injection?
CRLF injection is rarely the end goal; it is a primitive that enables several higher-impact attacks depending on what you can inject. The severity ranges from log noise to full cross-user response control.
- HTTP response splitting: inject a full second response, leading to web cache poisoning that serves your content to other users.
- Reflected XSS via the header channel: if body output is encoded but headers are not, an injected body reintroduces script execution, as the exploit above shows.
- Session fixation: inject a
Set-Cookieto pin a victim's session ID to a value you know. - Log injection: inject newlines into logged values to forge entries or break the parsing your SOC relies on.
- Open redirect chaining: combine header control with the redirect itself for convincing phishing.
Modern stacks strip newlines, which is exactly why the misses are sneaky.
Scanners detect the simple reflected-header case but miss the variants that matter: filters defeated by double encoding, input that reaches a header only after a second backend hop, and the escalation from header injection to cache poisoning. A tool sends %0d%0aX-Injected:yes, sees its own header echoed, and stops, even when the same sink lets you write a full body for XSS. The false positive to watch is a reflected header the framework later strips or the browser normalizes away; confirm by reading the raw bytes.
The false negatives are worse on modern stacks precisely because most HTTP libraries silently strip \r\n, so a naive payload returns clean and the scanner clears the endpoint, but a Unicode or container-decoded variant still slips through on a proxy or legacy gateway in the chain. Header-reflecting values buried in JSON or in X-Forwarded-* echoes sit outside a scanner's parameter model entirely. Fuzz every header-influencing parameter with both %0d%0a and %250d%250a, then check cacheability of any reflected response. Continuous coverage of injection classes like this is exactly what agentic pentesting is built to automate.
How do you prevent CRLF injection?
Prevent CRLF injection by removing or rejecting CR and LF from any value before it is written into an HTTP header, and by relying on framework APIs that do this for you. Most modern HTTP libraries already refuse header values with raw newlines: Java's HttpServletResponse.setHeader throws on CR/LF, Node's http module rejects them with ERR_INVALID_CHAR, and ASP.NET strips them by default. The bug therefore surfaces where developers build raw header strings by hand, run an older platform, or set headers at a reverse proxy or CDN edge that does its own parsing.
Concretely: strip or URL-encode \r and \n in redirect targets, cookie values, and any reflected header; validate redirect destinations against an allowlist of paths rather than reflecting raw input; mark cache keys so a poisoned variant cannot be served to others; and never log unsanitized request data on the same line as structured fields. As with every injection class, the root fix is the same principle from the OWASP Top 10: keep data and protocol structure strictly separate, and never let a client-supplied byte decide where one header ends and the next begins.
Frequently asked questions
Sources and references
- OWASP CRLF Injection
- OWASP WSTG: Testing for HTTP Splitting/Smuggling (WSTG-INPV-15)
- PortSwigger: HTTP response header injection
