Security Resources

⌘K
  1. Home
  2. Security Resources
  3. Secure Code Explain
  4. HTTP Response Header Injection (CRLF)

HTTP Response Header Injection (CRLF)

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.