Skip to main content

Overview

CRLF Injection occurs when an attacker can inject Carriage Return (CR, %0d, \r) and Line Feed (LF, %0a, \n) characters into an application’s output that is included in an HTTP response header. Since CR+LF sequences (\r\n) are used to separate headers and delimit the start of the response body in HTTP/1.1, injecting them allows an attacker to add fake headers or even inject content into the response body. This can lead to HTTP Response Splitting, Cross-Site Scripting (XSS), session fixation, or cache poisoning. 📄✂️

Business Impact

CRLF Injection enables several other attacks:
  • Cross-Site Scripting (XSS): By injecting CRLF sequences followed by HTML/JavaScript into the response body.
  • Session Fixation: By injecting a Set-Cookie header with a session ID they control.
  • Cache Poisoning: By crafting a response that gets stored in intermediate caches, affecting other users.
  • Phishing/Defacement: By injecting custom content into the response body.
  • Information Disclosure: By manipulating headers (e.g., Location) to redirect requests or expose internal details.

Reference Details

CWE ID: CWE-113 OWASP Top 10 (2021): A03:2021 - Injection Severity: Medium to High (depending on the resulting attack)

Framework-Specific Analysis and Remediation

This vulnerability happens when user-supplied data is directly included in HTTP response headers without proper sanitization to remove or encode CR (\r) and LF (\n) characters. Common sinks include custom header values, redirect URLs (Location header), and cookie values (Set-Cookie header). Modern web frameworks and libraries often provide some level of automatic protection by validating or encoding header values, but vulnerabilities can still occur in custom code or older/misconfigured setups. The primary defense is output encoding/sanitization specifically targeting CR and LF characters in any data destined for headers.
  • Python
  • Java
  • .NET(C#)
  • PHP
  • Node.js
  • Ruby

Framework Context

Manually setting headers in Django/Flask using user input without sanitization. Django’s built-in HttpResponseRedirect generally handles the Location header safely, but custom headers are a risk.

Vulnerable Scenario 1: Custom Header Reflection

A view reflects a user-provided parameter in a custom header.
# views/custom_header.py
from django.http import HttpResponse

def reflect_header(request):
    user_data = request.GET.get('x_info', '')
    response = HttpResponse("Content Here")
    # DANGEROUS: User input directly used in header value.
    # Input: x_info = "value%0d%0aInjected-Header:%20Evil"
    # Output Header:
    # X-Custom-Info: value
    # Injected-Header: Evil
    response['X-Custom-Info'] = user_data
    return response

Vulnerable Scenario 2: Unvalidated Redirect URL

Constructing a redirect URL using potentially tainted input (less common with Django’s built-in redirect, but possible in custom logic).
# views/redirector.py
from django.http import HttpResponse

def custom_redirect(request):
    target = request.GET.get('target', '/')
    # DANGEROUS: If target contains CRLF, it could split the Location header.
    # Input: target = "/path%0d%0aContent-Type:%20text/html%0d%0a%0d%0a<script>alert(1)</script>"
    # (Modern browsers/servers might block this, but illustrates the principle)
    response = HttpResponse(status=302)
    response['Location'] = target # Manual setting is risky
    return response

Mitigation and Best Practices

Avoid reflecting user input directly into headers. If necessary, sanitize the input by removing or encoding \r and \n characters. Use Django’s built-in HttpResponseRedirect or redirect() shortcut which handles URL validation for the Location header.

Secure Code Example

# views/custom_header.py (Secure)
from django.http import HttpResponse

def reflect_header_secure(request):
    user_data = request.GET.get('x_info', '')
    # SECURE: Remove CR and LF characters.
    sanitized_data = user_data.replace('\r', '').replace('\n', '')
    response = HttpResponse("Content Here")
    # Only set header if data is safe/non-empty after sanitizing
    if sanitized_data:
         response['X-Custom-Info'] = sanitized_data
    return response

# views/redirector.py (Secure - using Django's redirect)
from django.shortcuts import redirect
from django.utils.http import url_has_allowed_host_and_scheme # Also prevents Open Redirect

def custom_redirect_secure(request):
    target = request.GET.get('target', '/')
    # SECURE: Use Django's redirect which validates the URL.
    # Also add Open Redirect protection.
    allowed_hosts = {request.get_host()}
    if url_has_allowed_host_and_scheme(target, allowed_hosts):
         return redirect(target)
    else:
         return redirect('/') # Default safe redirect

Testing Strategy

Identify all instances where user input is incorporated into response headers (response['Header-Name'] = user_input). Submit URL-encoded CRLF sequences (%0d%0a) followed by test headers (e.g., Injected-Header: test) or HTML content (%0d%0a%0d%0a<script>...). Use curl -v or browser developer tools to inspect the raw response headers and body for unexpected content.