Why header handling becomes a security problem

HTTP headers are not free-form text. A header name must follow token rules, and a header value must not contain control characters such as carriage return or line feed. If an attacker can inject \r\n, they may be able to terminate one header and start another, or even manipulate the request body boundary in systems that serialize headers incorrectly.

Common failure modes include:

  • building raw HTTP messages with format!
  • copying user input into header names
  • forwarding untrusted values into proxy or upstream request headers
  • using custom serialization code that does not reject control characters
  • trusting values from query parameters, cookies, or form fields without normalization

Rust’s ecosystem helps here, but only if you use the typed APIs instead of assembling protocol text manually.

Prefer typed header APIs over raw string construction

The safest pattern is to use the http::HeaderMap and typed header types rather than concatenating strings. These APIs enforce syntax rules and reject invalid names or values.

Safe example with HeaderMap

use http::{HeaderMap, HeaderName, HeaderValue};

fn build_headers(user_agent: &str, request_id: &str) -> Result<HeaderMap, http::header::InvalidHeaderValue> {
    let mut headers = HeaderMap::new();

    headers.insert(
        HeaderName::from_static("user-agent"),
        HeaderValue::from_str(user_agent)?,
    );

    headers.insert(
        HeaderName::from_static("x-request-id"),
        HeaderValue::from_str(request_id)?,
    );

    Ok(headers)
}

This code is safe because HeaderValue::from_str rejects invalid bytes, including control characters. If an attacker submits evil\r\nx-admin: true, the conversion fails instead of silently producing a malformed request.

Unsafe pattern to avoid

fn build_raw_headers(user_agent: &str) -> String {
    format!("User-Agent: {}\r\n", user_agent)
}

This is dangerous because it treats untrusted input as protocol text. Even if you later “sanitize” it, the burden is easy to get wrong and hard to audit.

Validate both header names and values

Header names are more restrictive than values. If your application allows users to choose custom header names, validate them against a known allowlist whenever possible.

Use an allowlist for dynamic headers

use http::{HeaderMap, HeaderName, HeaderValue};

fn insert_custom_header(
    headers: &mut HeaderMap,
    name: &str,
    value: &str,
) -> Result<(), String> {
    let allowed = ["x-client-version", "x-trace-id", "x-feature-flag"];

    if !allowed.contains(&name) {
        return Err("unsupported header name".into());
    }

    let header_name = HeaderName::from_bytes(name.as_bytes())
        .map_err(|_| "invalid header name")?;

    let header_value = HeaderValue::from_str(value)
        .map_err(|_| "invalid header value")?;

    headers.insert(header_name, header_value);
    Ok(())
}

This pattern gives you two layers of protection:

  1. business-level restriction through an allowlist
  2. protocol-level validation through typed parsing

That combination is much safer than accepting arbitrary header names from users or configuration.

Be careful when forwarding headers from incoming requests

A common server-side pattern is to forward selected headers from an inbound request to an upstream service. This is useful for correlation IDs, locale preferences, or authorization tokens, but it can become dangerous if you forward too much.

Forward only explicitly allowed headers

Header typeSafe to forward?Notes
x-request-idYesUseful for tracing if validated
accept-languageYesUsually harmless, but normalize if needed
authorizationSometimesForward only when the upstream is trusted and intended to receive it
hostNoCan alter routing and virtual host behavior
content-lengthNoCan create request smuggling issues
transfer-encodingNoShould be controlled by the HTTP client
connectionNoHop-by-hop header; do not propagate

Hop-by-hop headers are especially important. They are meant for a single transport hop and should not be forwarded by proxies or application code. Forwarding them can create ambiguous request semantics or break intermediaries.

Example: filtering headers before proxying

use http::{HeaderMap, HeaderName};

fn copy_safe_headers(inbound: &HeaderMap) -> HeaderMap {
    let mut outbound = HeaderMap::new();

    for name in ["accept-language", "x-request-id"] {
        let header_name = HeaderName::from_static(name);
        if let Some(value) = inbound.get(&header_name) {
            outbound.insert(header_name, value.clone());
        }
    }

    outbound
}

This approach is intentionally narrow. If you need more headers, add them one by one and document why they are safe.

Avoid raw HTTP serialization unless you fully control the protocol

Some applications build HTTP requests manually for specialized integrations, embedded systems, or testing tools. If you do this, you must treat the output as a protocol encoder, not as string formatting.

Risks of manual serialization

  • CRLF injection through values
  • duplicated headers with conflicting semantics
  • malformed line endings
  • incorrect content length handling
  • accidental inclusion of user-controlled header names

If you cannot avoid manual serialization, validate every field and reject any value containing control characters. In most cases, using reqwest or hyper is the better choice.

Use reqwest and hyper to delegate protocol correctness

Higher-level clients already know how to encode headers safely. They also handle details such as HTTP/2 framing, connection reuse, and content length management.

Example with reqwest

use reqwest::header::{HeaderMap, HeaderName, HeaderValue};

async fn send_request(token: &str) -> Result<(), reqwest::Error> {
    let mut headers = HeaderMap::new();
    headers.insert(
        HeaderName::from_static("x-api-token"),
        HeaderValue::from_str(token).map_err(|_| {
            reqwest::Error::new(
                reqwest::StatusCode::BAD_REQUEST,
                "invalid token header value",
            )
        })?,
    );

    let client = reqwest::Client::new();
    let response = client
        .get("https://api.example.com/data")
        .headers(headers)
        .send()
        .await?;

    println!("status: {}", response.status());
    Ok(())
}

In real code, prefer returning your own application error type instead of trying to construct a reqwest::Error directly. The important point is that header validation happens before the request is sent.

Normalize and constrain user-controlled header values

Even when a value is syntactically valid, it may still be unsafe from a business perspective. For example, a locale header could be used to trigger unexpected backend behavior if you accept arbitrary strings.

Good practices for values

  • trim leading and trailing whitespace if your application semantics allow it
  • restrict values to a known set when possible
  • cap length to prevent resource abuse
  • reject embedded control characters
  • avoid passing raw user input into security-sensitive headers

A good rule is: if the header affects routing, authentication, caching, or authorization, treat it as security-sensitive and validate it strictly.

Protect proxies and middleware from header confusion

If your Rust service sits behind a reverse proxy or acts as one, header handling becomes even more important. Different components may interpret duplicate headers differently. Attackers can exploit inconsistencies between layers.

Defensive measures

  • reject or normalize duplicate security-sensitive headers
  • do not let clients set Host, Content-Length, or Transfer-Encoding
  • preserve a clear boundary between inbound and outbound header sets
  • use a single source of truth for request metadata
  • log header names only when necessary, and avoid logging values that may contain secrets

If your application uses middleware, ensure each layer knows whether it is reading client headers, internal headers, or upstream headers. Mixing those responsibilities is a common source of bugs.

Test for malformed and malicious inputs

Security-sensitive code should be tested with invalid header data, not just happy-path examples. Include cases with control characters, oversized values, duplicate fields, and unsupported names.

Example tests

#[cfg(test)]
mod tests {
    use http::header::HeaderValue;

    #[test]
    fn rejects_crlf_in_header_value() {
        assert!(HeaderValue::from_str("abc\r\nx-test: injected").is_err());
    }

    #[test]
    fn accepts_simple_value() {
        assert!(HeaderValue::from_str("trace-123").is_ok());
    }
}

You can also add property-based tests to ensure that arbitrary input never produces a serialized header containing CR or LF. That is especially useful if you have custom encoding or proxy logic.

Practical checklist for safe header handling

GoalRecommended approach
Build headersUse HeaderMap, HeaderName, and HeaderValue
Accept user-supplied namesPrefer an allowlist
Accept user-supplied valuesValidate syntax and business rules
Forward headersCopy only explicitly allowed headers
Prevent injectionReject control characters and avoid raw string concatenation
Handle proxiesDo not forward hop-by-hop headers
Test securityInclude malformed input and duplicate-header cases

A secure design pattern for real applications

A robust Rust service usually follows this flow:

  1. receive inbound request data
  2. extract only the headers you need
  3. validate names and values separately
  4. store normalized data in typed structures
  5. build outbound requests using typed HTTP APIs
  6. forward only a minimal, documented header set

This pattern keeps protocol concerns isolated from business logic. It also makes reviews easier because unsafe behavior is concentrated in a few small functions.

Conclusion

Unsafe HTTP header handling is a subtle but serious security risk. The main defense is to stop treating headers as plain strings and instead use typed APIs that enforce protocol syntax. Combine that with allowlists, strict validation, and minimal forwarding, and your Rust code will be much harder to abuse through header injection or smuggling.

If you remember one rule, make it this: never let untrusted input become raw HTTP text.

Learn more with useful resources