What HTTP response splitting looks like

The core problem is header injection through newline characters. Consider a feature that sets a download filename from a query parameter or returns a redirect URL from user input. If the value contains CRLF, the server may emit malformed headers.

A malicious value like this:

report.pdf\r\nSet-Cookie: session=attacker

can turn one header into two. Depending on the framework, proxy chain, and client behavior, the result may include cache poisoning, cookie injection, or response body corruption.

Common attack surfaces

Response splitting usually appears in code that:

  • Builds Location headers for redirects
  • Sets Content-Disposition filenames
  • Echoes user input in custom headers
  • Constructs CORS-related headers dynamically
  • Copies values from request parameters, form fields, or database records into response metadata

The risk is highest when developers assume “it’s just a header value” and skip validation because the value is not part of the HTML body.


Why Rust still needs explicit header validation

Rust prevents memory corruption, but HTTP header safety is a semantic problem, not a memory safety problem. A String can still contain \r and \n. If you pass that string into a response builder without checking, the runtime cannot infer your intent.

The good news is that Rust web frameworks and HTTP libraries often provide typed header APIs that reject invalid values. The best defense is to use those APIs consistently and avoid manual string concatenation.


Safe vs. unsafe header construction

The table below summarizes common patterns.

PatternRisk levelNotes
Concatenating raw strings into header linesHighEasy to inject CRLF or invalid syntax
Using typed header setters with validationLowPreferred approach
Allowing arbitrary user input in Location or Content-DispositionMedium to highSafe only with strict encoding and validation
Whitelisting known values before mapping to headersLowBest for enums and fixed options

A useful rule: if a header value can be influenced by a user, treat it as untrusted data until it has been validated or encoded for the exact header context.


Use framework and library header types

Most Rust HTTP stacks expose header abstractions that validate values before sending them. For example, http::HeaderValue rejects newline characters and other invalid bytes. That means you should prefer typed header insertion over raw string formatting.

Example: safe redirect handling with http and axum

use axum::{
    http::{header, HeaderValue, StatusCode, Uri},
    response::{IntoResponse, Response},
};

fn safe_redirect(target: &str) -> Result<Response, StatusCode> {
    // Parse as a URI first. This rejects many malformed inputs.
    let uri: Uri = target.parse().map_err(|_| StatusCode::BAD_REQUEST)?;

    // Restrict redirects to relative paths to avoid open redirect issues.
    if uri.scheme().is_some() || uri.authority().is_some() {
        return Err(StatusCode::BAD_REQUEST);
    }

    let mut response = Response::new(axum::body::Body::empty());
    *response.status_mut() = StatusCode::FOUND;

    // HeaderValue validates that the value contains no CRLF or invalid bytes.
    let location = HeaderValue::from_str(uri.to_string().as_str())
        .map_err(|_| StatusCode::BAD_REQUEST)?;

    response.headers_mut().insert(header::LOCATION, location);
    Ok(response)
}

This example addresses two concerns at once:

  1. It prevents response splitting by rejecting invalid header bytes.
  2. It reduces open redirect risk by allowing only relative targets.

Even if your framework offers a redirect helper, validate the destination before using it.


Validate by context, not just by character filtering

A common mistake is to “sanitize” header values by stripping \r and \n. That may stop the most obvious injection vector, but it is usually not enough. Different headers have different syntax rules, and some values require encoding rather than removal.

Better validation strategies

  • Allowlist expected values: Use enums for known header options such as same-site, no-store, or a fixed set of content types.
  • Parse into structured types: Use Uri, HeaderValue, or dedicated parsing crates instead of raw strings.
  • Encode for the specific header: For filenames, use proper Content-Disposition encoding rather than manual concatenation.
  • Reject ambiguous input: If a value cannot be represented safely, return 400 Bad Request or fall back to a safe default.

Example: safe custom header from a user profile field

Suppose you want to expose a user’s display name in a custom header for internal tooling. A safe approach is to validate length and character set before insertion.

use http::{header::HeaderName, HeaderValue, Response, StatusCode};

fn build_response(display_name: &str) -> Result<Response<String>, StatusCode> {
    if display_name.is_empty() || display_name.len() > 64 {
        return Err(StatusCode::BAD_REQUEST);
    }

    if !display_name.chars().all(|c| c.is_ascii_alphanumeric() || c == ' ' || c == '-' || c == '_') {
        return Err(StatusCode::BAD_REQUEST);
    }

    let mut response = Response::new("ok".to_string());
    let name = HeaderName::from_static("x-display-name");
    let value = HeaderValue::from_str(display_name).map_err(|_| StatusCode::BAD_REQUEST)?;

    response.headers_mut().insert(name, value);
    Ok(response)
}

This code is intentionally strict. For security-sensitive headers, strict allowlists are usually safer than permissive filtering.


Handle redirects carefully

Redirects are one of the most common places where response splitting and related header issues appear. A redirect target often comes from a query parameter like next, return_to, or redirect_uri.

Risks to avoid

  • CRLF injection into Location
  • Open redirects to attacker-controlled domains
  • Scheme confusion such as javascript: or data:
  • Path confusion caused by malformed relative URLs

Recommended pattern

  1. Parse the input as a URI.
  2. Require a relative path unless external redirects are explicitly needed.
  3. Normalize and validate the path.
  4. Insert the value using a typed header API.

If you must support external redirects, maintain an allowlist of trusted hosts and exact schemes. Do not rely on substring checks such as ends_with("example.com"), which can be bypassed with crafted domains.


Safely building Content-Disposition

File download endpoints often set a Content-Disposition header using a filename derived from user input or stored metadata. This header is especially sensitive because filenames may contain quotes, semicolons, spaces, and non-ASCII characters.

Unsafe approach

let header = format!("attachment; filename=\"{}\"", filename);

This is unsafe because filename can break out of the quoted string or include CRLF.

Safer approach

Use a dedicated builder or encode the filename according to RFC 5987 / RFC 6266 rules when supported by your framework or library. If no helper exists, validate the filename strictly and reject unsafe characters.

A practical policy is:

  • Accept only a limited character set for simple filenames
  • Replace unsupported characters with _ only if that does not create ambiguity
  • Prefer server-generated filenames when possible

For example, invoice-2024-06.pdf is a much safer header value than a raw user-supplied title.


Defensive design patterns for Rust services

The best protection is to make unsafe header construction difficult to write in the first place.

1. Wrap header values in domain types

Instead of passing raw strings around, define a type that validates on construction.

use http::HeaderValue;

pub struct SafeHeaderValue(HeaderValue);

impl SafeHeaderValue {
    pub fn new(input: &str) -> Result<Self, &'static str> {
        if input.contains('\r') || input.contains('\n') {
            return Err("invalid header value");
        }
        let value = HeaderValue::from_str(input).map_err(|_| "invalid header value")?;
        Ok(Self(value))
    }

    pub fn as_header_value(&self) -> &HeaderValue {
        &self.0
    }
}

This pattern centralizes validation and makes unsafe insertion harder to repeat across the codebase.

2. Keep header logic close to the response layer

Do not let arbitrary application code assemble header strings. Instead, have a small response-building module that owns all header insertion logic. That makes it easier to audit and test.

3. Prefer fixed mappings

If a user choice maps to a header value, use an enum:

enum CachePolicy {
    NoStore,
    Private,
    Public,
}

Then convert the enum to a known-safe header string. This avoids accepting arbitrary text where only a few values are needed.


Testing for response splitting vulnerabilities

Security bugs in headers are easy to miss in manual testing because most normal inputs work fine. Add tests that explicitly cover malicious payloads.

Useful test cases

  • Values containing \r
  • Values containing \n
  • Values containing \r\n
  • Very long values
  • Unicode and non-ASCII input
  • Inputs that look like header syntax, such as foo: bar

Example test

#[test]
fn rejects_crlf_in_header_value() {
    let bad = "report.pdf\r\nSet-Cookie: session=evil";
    let result = http::HeaderValue::from_str(bad);
    assert!(result.is_err());
}

For integration tests, send crafted requests through your full stack and inspect the raw HTTP response. This is especially important if your service sits behind a reverse proxy, because intermediary behavior can differ from local unit tests.


Operational best practices

Secure code is only part of the solution. Deployment and framework configuration also matter.

  • Use current framework versions: Header parsing and response helpers improve over time.
  • Log rejected inputs carefully: Record enough context for debugging, but do not reflect malicious header payloads back into logs without escaping.
  • Review proxy behavior: Some reverse proxies normalize or rewrite headers. Test the full request path.
  • Set strict response policies: If your application never needs dynamic custom headers, avoid them entirely.
  • Audit helper functions: Small utility functions that “just build headers” are common sources of bugs.

A good code review question is: “Can any untrusted value reach this header insertion point without structured validation?”


Practical checklist

Before shipping a feature that sets response headers from application data, verify the following:

  • The value is parsed or validated for the exact header context
  • CRLF characters are rejected, not merely stripped
  • Redirect targets are restricted to safe destinations
  • HeaderValue or equivalent typed APIs are used
  • Content-Disposition filenames are encoded or strictly allowlisted
  • Tests cover malicious inputs and edge cases
  • Header construction is centralized and easy to audit

If you can answer “yes” to all of these, your application is much less likely to be vulnerable to response splitting.


Learn more with useful resources