Document CRLF handling for `http.server`
Bug report
Bug description:
Vulnerability Description
The send_header method in Lib/http/server.py writes headers directly to the output stream without checking for line breaks. When user-controlled input is passed to send_header, an attacker can inject CRLF sequences (\r\n) to terminate the current header and inject new headers or manipulate the response.
Vulnerable Code:
def send_header(self, keyword, value):
"""Send a MIME header to the headers buffer."""
if self.request_version != 'HTTP/0.9':
if not hasattr(self, '_headers_buffer'):
self._headers_buffer = []
self._headers_buffer.append(
("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict'))
# No validation for \r or \n characters!
Attack Scenarios
Scenario 1: Set-Cookie Injection (Session Fixation)
Vulnerable Application:
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlparse
class VulnerableHandler(BaseHTTPRequestHandler):
def do_GET(self):
query = parse_qs(urlparse(self.path).query)
custom_val = query.get('val', [''])[0]
self.send_response(200)
# VULNERABLE: Direct injection into header
self.send_header('X-Custom', custom_val)
self.end_headers()
self.wfile.write(b"Hello World")
Attack URL:
http://localhost:8000/?val=test%0d%0aSet-Cookie:%20pwned=true
Result:
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.x
Date: ...
X-Custom: test
Set-Cookie: pwned=true
Impact: Attacker can inject session cookies, leading to session fixation attacks.
Scenario 2: Location Header Injection (Malicious Redirect)
Attack URL:
http://localhost:8000/?val=test%0d%0ALocation:%20http://evil.com/
Result:
HTTP/1.0 200 OK
Server: BaseHTTP/0.6 Python/3.x
Date: ...
X-Custom: test
Location: http://evil.com/
Impact:
- Users are redirected to malicious websites
- Phishing attacks
- Open redirect vulnerabilities
- Cache poisoning (if cached responses include the injected Location header)
Verified Test Results:
✓ LOCATION HEADER INJECTION CONFIRMED!
Injected Location: http://evil.com/
✓ MALICIOUS REDIRECT CONFIRMED!
Browser would redirect to: http://evil.com/
✓ MALICIOUS REDIRECT SUCCESSFUL!
Attack Vector
- Type: Remote
-
Prerequisites:
- Application uses
http.server.BaseHTTPRequestHandler - User input is reflected in HTTP headers via
send_header() - Common patterns: query parameters, user-agent reflection, custom headers
- Application uses
- Complexity: Low - Simple URL manipulation
- Authentication: Not required
Impact
-
Session Fixation: Inject
Set-Cookieheaders to control user sessions -
Malicious Redirects: Inject
Locationheaders to redirect users to attacker-controlled sites - Cache Poisoning: Inject headers that affect cached responses
- Cross-Site Scripting (XSS): Inject headers that enable XSS attacks
- Web Cache Deception: Manipulate cache behavior via injected headers
CPython versions tested on:
CPython main branch
Operating systems tested on:
macOS
Linked PRs
- gh-142605
- gh-143395
Thank you for reporting, the bug makes sense. Goes without saying both keyword and value are vulnerable.
I would like to work on this if that's okay.
http.server is not meant to be used in production, I honestly don't consider this a vulnerability worth the change. If the validation is small, we could consider it but there is a huge warning saying that we are aware of those vulns (or if it's not mentioned, we just update the docs).
There’s a warning advising against using it in production, but I haven’t found any specific explanation about whether CRLF injection vulnerabilities are taken into account.
The relevant section is here: https://docs.python.org/3/library/http.server.html#security-considerations
Yes, so we have two possibilities:
- Make the validation. But honestly, I don't want the code to become more and more complex. The module is not meant for advanced use, and usually only for local testing. Now, if the validation algorithm is not too big, we can add it. What matters is that we don't overcomplicate code for which there are better replacements on PyPI.
- Update the docs. That's minimal effort on our side, and at least we would mention this. I know that there are CRLF vulns in wsgiref for instance that were reported (and maybe fixed I don't remember now) so maybe people do expect to have the same minimal guards for whatever server we provide.
I think the fix is straightforward. A simple validation check should be enough. I also reviewed the wsgiref implementation and didn't see any existing safeguard for this. We can apply the same fix to both components without much complexity. What do you think?
if '\r' in value or '\n' in value:
raise ValueError("Header value contains invalid characters (CR or LF)")
The suggestion to apply fixes for both wsgiref and http.server makes sense.
Taking into account the above suggestion to keep changes minimal, I implemented the change in the linked PR.
Also adjusted the ValueError message slightly for consistency with other wsgiref exceptions.
HTTP server previously reported in Issue #73610, also covering the send_response_only method. If any change is made I’d prefer to raise an exception than quietly alter the fields as proposed there.
Issues #72964 and #55880 are open reports for wsgiref.headers.
@vadmium thanks for the suggestion about send_response_only. It’s now covered in the latest commit.
The other cases from the linked issues are already handled in the PR by raising an exception, matching the approach you outlined. A review of the PR when you have time would be much appreciated.
@serhiy-storchaka, since you contributed to both http.server and wsgiref in the past, I was wondering if you might have the bandwidth to review this fix for the mentioned vulnerability.
http.server does not perform encoding (using QP or Base64) and line wrapping. Therefore, it is implied that the user is responsible for this -- it must pass an already encoded string, which can contain CRLF if the long string was folded. Isn't it?