CRLF Injection: How Two Invisible Characters Split an HTTP Response
A carriage return and a line feed are all it takes to inject headers, forge cookies and poison caches. Here is how CRLF injection leads to HTTP response splitting, and how to shut it down.
%0d%0aSet-Cookie: session=1At a glance
CWE-113- Interpreter
- HTTP header and message parser
- Required condition
- Decoded line-break characters reach a header or message boundary without rejection.
- Potential impact
- Header injection, response splitting, cache poisoning, or forged log records.
- Primary defense
- Reject CR and LF and use framework APIs that enforce valid header values.
Safe practice: use benign proof only, in a disposable local lab or on a system you are explicitly authorized to test.
On this page
In HTTP/1.x, a carriage return followed by a line feed — \r\n, often written CRLF — separates one header from the next, and a blank line separates headers from the body. CRLF injection occurs when decoded line breaks enter a field that a server, proxy or downstream component later treats as message structure.
The classic vulnerability appears when an application copies untrusted input into an HTTP/1 response header without rejecting line breaks. If an attacker can smuggle \r\n into a redirect's Location value, an unsafe serializer may append a header the developer never wrote. Two line breaks can end the header section early and begin a second body or response, a technique known as HTTP response splitting.
The mental model: decoding happens before framing
The dangerous value often looks harmless at the first layer because the line breaks are encoded as %0d%0a. A URL decoder turns them into bytes, and a later HTTP serializer or intermediary interprets those bytes as boundaries.
| Layer | Value |
|---|---|
| URL | %2Fhome%0d%0aX-Lab-Marker%3A%20yes |
| Decoded application value | /home followed by CRLF and X-Lab-Marker: yes |
| Unsafe HTTP/1 serialization | Two separate header lines |
HTTP/2 and HTTP/3 use binary framing rather than CRLF-delimited header lines, but applications and gateways still translate between protocol versions and still process header values. The durable rule is protocol-independent: control characters do not belong in a header value.
How it happens
The root cause is decoded user input flowing into a header value without validation. Modern Flask and Werkzeug reject newline characters in headers, so a normal response.headers['Location'] = value example will not reproduce this flaw on current versions. A vulnerable example needs a legacy, custom or otherwise unsafe response serializer such as this simplified socket handler:
from urllib.parse import unquote def redirect_response(encoded_target): target = unquote(encoded_target) wire = "HTTP/1.1 302 Found\r\nLocation: " + target + "\r\n\r\n" return wire.encode("latin-1")
For a normal value like /dashboard, the bytes look plausible. But target is concatenated directly into the wire representation after URL decoding. The code has erased the distinction between a header value and the characters that end the header.
The same flaw shows up wherever input reaches a header: a Set-Cookie built from a query parameter, a custom X-* header echoing a request value, or a logging layer that writes user data into a response header for tracing.
A concrete attack
The attacker crafts a URL whose url parameter carries an encoded CRLF followed by an extra header:
GET /redirect?url=/home%0d%0aSet-Cookie:%20session=attacker HTTP/1.1
Host: shop.example
The unsafe handler decodes %0d%0a into a real line break and writes the whole value into Location. The response bytes become:
HTTP/1.1 302 Found
Location: /home
Set-Cookie: session=attacker
The attacker just injected a header. With a double CRLF, the attack escalates to response splitting — the blank line terminates the headers, and everything after it is interpreted as the body:
GET /redirect?url=/home%0d%0a%0d%0aLab%20marker HTTP/1.1
The precise result depends on the client, status code, connection reuse, proxy chain and cache. A blank line can make later bytes part of a body or, in a more complex splitting condition, the beginning of another response. A harmless marker is sufficient to test framing; script content is unnecessary.
The attacker did not break into the server. They simply spoke the server's own framing language. To HTTP, a line break is structure, not data — so a line break in your input rewrites the response.
What an attacker gains
Control over response framing unlocks several distinct attacks:
- Header injection — set arbitrary headers, including security-relevant ones the app relies on.
- Session fixation — inject a
Set-Cookiethat pins the victim to a session the attacker controls. - Reflected XSS via the split body — deliver script in a body the attacker fully authored, even on endpoints that never render user input.
- Web cache poisoning — if a caching layer stores the split response, every later visitor is served the attacker's content.
Other CRLF sinks include request headers constructed for an upstream service, email headers, structured logs and text protocols that also treat line breaks as record boundaries. The impact and correct encoder depend on the destination, so “remove CRLF everywhere” is less useful than validating each value at the boundary where it becomes structure.
The fix: never put raw input in a header
The reliable defense is to keep carriage returns and line feeds out of header values entirely. Reject or strip them before the value ever reaches a header, and prefer framework APIs that encode header values for you.
@app.route('/redirect') def redirect_to(): target = request.args.get('url', '') if '\r' in target or '\n' in target: abort(400) if not target.startswith('/'): abort(400) return redirect(target, code=302)
Reinforce it with these habits:
- Sanitize every input that lands in a header, including cookies, custom headers and redirect targets — strip or reject CR and LF rather than trying to escape them.
- Use the framework's header API, not manual string assembly; modern stacks reject embedded newlines, but only if you let them build the header.
- Validate redirect targets against an allowlist of paths or hosts instead of reflecting arbitrary URLs.
- Validate after canonical decoding so
%0d%0a, lone line feeds and double-decoding paths cannot reintroduce control characters later.
Fixes that do not hold
- Blocking the literal text
%0d%0a: another layer may decode mixed case, repeated encoding or a raw control character. - Removing CR but allowing LF: many parsers accept a lone line feed as a boundary or normalize it.
- Relying on the browser: proxies, caches and upstream services may process the malformed value first.
- Setting one security header: an injected or split response is a framing problem, not a missing-header problem.
- Assuming HTTP/2 removes the risk: protocol translation and non-HTTP line-oriented sinks still exist.
A developer review checklist
- Inventory request and response headers built from request or stored user data.
- Use maintained framework APIs rather than serializing HTTP messages manually.
- Reject CR, LF and other prohibited control characters after decoding.
- Allowlist redirect destinations and other fields with a narrow expected shape.
- Test the deployed proxy, cache and protocol-conversion path, not only the application server.
- Use a unique harmless header marker and stop once boundary injection is proven.
References
How SelfSec proves this one
Scanner behaviour- Module
- CRLF Injection
- How it probes
- Encoded and raw carriage-return and line-feed sequences in every parameter that reaches a response header.
- How it confirms
- The injected header or body split has to appear in the raw response, which the scanner keeps alongside the finding as evidence.
Reported as CWE-113 · ATT&CK T1190 · SARIF 2.1.0
A reflected response alone is not enough to promote a CRLF Injection finding — the classical engine has to confirm the behaviour before it reaches the report. See the detection engine
SelfSec is intended strictly for authorized security testing of systems you own or are explicitly permitted to assess.
Do both things about CRLF Injection
SelfSec covers this class from both sides — the scanner confirms it in your own app, the firewall blocks it in front of your origin while the fix ships.