Crlf Injection in Django (Python)
Crlf Injection in Django with Python — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject carriage return (CR, \r) and line feed (\n) characters into an HTTP header, causing the header to be split and additional headers or response content to be injected. In Django, this typically arises when user-controlled data is used to construct HTTP responses, such as the Location header in a redirect or values in Set-Cookie, without proper validation or sanitization. Because Django is a Python web framework, the vulnerability is realized through Python string handling when composing headers, and through Django’s utilities that forward user input into HTTPMessage or HttpResponse structures.
Consider a user-supplied "next" parameter used in a redirect:
next_url = request.GET.get('next', '/')
If this value is passed directly into an HttpResponseRedirect or used to build a Location header, an attacker can supply a payload like https://example.com\r\nContent-Type: text/html\r\n\r\n. The \r\n sequences split the header, injecting a second header that may change content type or trigger response splitting. In Python’s http.client and Django’s HttpResponse, headers are assembled and sent to the WSGI server; injected CR LF can cause the server to treat injected text as a new header or as body content, leading to response splitting, cache poisoning, or XSS in some contexts.
Another common pattern is Set-Cookie construction using user input, for example:
username = request.GET.get('username', 'guest')
response.set_cookie('welcome', f'Hello {username}')
If the cookie value contains \r\n, an attacker can inject additional Set-Cookie headers or malformed attributes. Python’s email and HTTP message libraries used by Django’s HttpResponse can treat \r\n as header delimiters, enabling injection when values are not strictly validated. Even when using Django’s JsonResponse, if the JSON string is later embedded in a header context without escaping, CRLF injection remains possible.
Django does not automatically sanitize header inputs; therefore, any user-controlled string that reaches a header context must be validated. Attack patterns include using CR LF to inject new headers (e.g., Content-Security-Policy), splitting responses to hide malicious content, or bypassing same-site cookie protections by injecting Path or Domain attributes. The framework relies on developers to ensure that such inputs are restricted to safe characters and that no newlines are present.
Python-Specific Remediation in Django — concrete code fixes
Remediation centers on strict input validation and avoiding direct concatenation of user input into HTTP headers. For redirects, use Django’s redirect utilities that accept a view name or a validated absolute URL, and reject any user-provided values containing CR or LF characters.
import re
from django.http import HttpResponseRedirect
from django.urls import reverse
SAFE_URL_CHARS = re.compile(r'^[a-zA-Z0-9_\-\.~%:/?#[\]@!$&\'()*+,;=]*$')
def redirect_to_dashboard(request):
next_url = request.GET.get('next')
if next_url is not None:
# Reject any header-injection-sensitive characters
if '\n' in next_url or '\r' in next_url:
return HttpResponseRedirect(reverse('dashboard'))
# Optionally enforce a safe pattern or absolute same-origin URL
if not next_url.startswith('/'):
return HttpResponseRedirect(reverse('dashboard'))
return HttpResponseRedirect(next_url)
return HttpResponseRedirect(reverse('dashboard'))
For cookies, avoid embedding raw user input into cookie values that could be concatenated into a Set-Cookie header. Instead, use Django’s cookie API safely and validate values:
from django.http import HttpResponse
username = request.GET.get('username', 'guest')
# Reject newline characters
if '\n' in username or '\r' in username:
username = 'guest'
response = HttpResponse('OK')
response.set_cookie('welcome', f'Hello {username}')
response.set_cookie('session_id', 'abc123', samesite='Lax', httponly=True)
response['Content-Security-Policy'] = "default-src 'self'"
response['X-Content-Type-Options'] = 'nosniff'
return response
When building custom headers, always sanitize inputs by removing or rejecting control characters:
import re
def sanitize_header_value(value: str) -> str:
# Remove CR and LF and any non-printable characters
return re.sub(r'[\r\n\x00-\x1f\x7f]', '', value)
user_comment = request.GET.get('comment', '')
safe_comment = sanitize_header_value(user_comment)
response['X-Comment'] = safe_comment
return response
For JSON-based endpoints, ensure that responses are generated by Django’s serializers rather than manually concatenated strings. This prevents accidental header-like content in JSON that could be misused elsewhere:
from django.http import JsonResponse
def user_profile(request):
user_data = {'username': request.GET.get('username', 'guest')}
# Let JsonResponse handle serialization; avoid manual string assembly
return JsonResponse(user_data, json_dumps_params={'ensure_ascii': False})
Finally, integrate these checks into development practices by adding unit tests that assert the absence of CR/LF in constructed headers and by using middleBrick to scan your endpoints. middleBrick’s scans can surface exposed injection points and provide prioritized findings with remediation guidance, helping you catch misconfigurations before deployment.