Here is an example of Python code that is vulnerable to CRLF response header injection:
🥺 Vulnerable Code
def send_download_headers(conn, filename):
# Vulnerable: the filename comes from the query string and is written
# directly into the raw HTTP response
response = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/octet-stream\r\n"
f"Content-Disposition: attachment; filename={filename}\r\n"
"\r\n"
)
conn.sendall(response.encode())A filename of report.pdf%0d%0aSet-Cookie:%20session=attacker adds an attacker controlled header to the response. Two consecutive CRLF sequences end the header block entirely, which lets the attacker write their own response body - that is response splitting, and it leads to reflected XSS, cache poisoning, and session fixation depending on what sits in front of the application.
😎 Secure Code
Here is a version of the same code that is secured against CRLF response header injection:
import re
SAFE_FILENAME = re.compile(r"^[A-Za-z0-9._-]{1,100}$")
def send_download_headers(conn, filename):
if not SAFE_FILENAME.match(filename):
raise ValueError("Invalid filename")
# No CR, LF, NUL, or quote can survive the allowlist above
response = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/octet-stream\r\n"
f'Content-Disposition: attachment; filename="{filename}"\r\n'
"\r\n"
)
conn.sendall(response.encode())The allowlist keeps control characters out of the header value, and quoting the filename stops parameter confusion. Better still, let a framework build the response: modern header APIs reject CR and LF in values for you. If you must accept arbitrary names, generate a safe server-side filename and put the original in a URL-encoded filename* parameter.