Skip to content
winfunc
Disclosure record · Actix

HTTP/1.1 CL.TE request smuggling in actix-http (GHSA-xhj4-vrgc-hr34)

actix-http accepted conflicting Content-Length and Transfer-Encoding: chunked request framing

ActixDisclosedSource record

CVSS 4.0 base score

Medium
6.3/ 10

Vector: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N

Record

Project
Actix
Severity
Medium6.3
CVE
None listed
Disclosed
Trace
4 steps

Summary

Source

MessageType::set_headers

actix-http/src/h1/decoder.rs:75

Sink

PayloadDecoder::chunked selection

actix-http/src/h1/decoder.rs:197

The HTTP/1.1 request parser in actix-http accepted requests containing both Content-Length and Transfer-Encoding: chunked. During header conversion, MessageType::set_headers recorded the content length and also marked the message as chunked. The payload selection logic then preferred the chunked decoder whenever chunked was true, instead of rejecting the conflicting framing.

In a CL.TE deployment, an upstream proxy can frame the request using Content-Length while forwarding the ambiguous request to an Actix backend that frames it as chunked. When the backend stops at the terminating chunk marker on a reused HTTP/1.1 connection, trailing bytes can be parsed as a second backend request. The fix, released in actix-http 3.12.1, rejects HTTP/1 requests with Transfer-Encoding when HTTP/1.0 is used, when the transfer encoding is not chunked, or when Content-Length is also present. The GitHub advisory is GHSA-xhj4-vrgc-hr34; the security change is commit 3c056bd36128d41ba130a8704ef5d648de3f2870, and the release prep is commit 0fb89457eda4a78a4cb7ccb3fdebe49a143ce2d5.

Severity

Attack vectorAV
Network
Attack complexityAC
Low
Attack requirementsAT
Present
Privileges requiredPR
None
User interactionUI
None
ConfidentialityVC
None
IntegrityVI
Low
AvailabilityVA
None
Subsequent confidentialitySC
None
Subsequent integritySI
None
Subsequent availabilitySA
None

Metric values as published in the disclosure vector. Meters show how far each value raises exposure.

Source-to-sink trace

  1. Source · attacker-controlledactix-http/src/h1/decoder.rs:75

    MessageType::set_headers

  2. Step 01actix-http/src/h1/decoder.rs:75-150

    set_headers consumed the raw httparse header indexes and accepted both framing headers: Content-Length populated content_length, while Transfer-Encoding: chunked set chunked = true.

    rust
    let mut chunked = false;
    let mut content_length = None;
    
    match name {
        header::CONTENT_LENGTH => {
            content_length = Some(len);
        }
        header::TRANSFER_ENCODING if version == Version::HTTP_11 => {
            if val.eq_ignore_ascii_case("chunked") {
                chunked = true;
            }
        }
        _ => {}
    }
    
  3. Step 02actix-http/src/h1/decoder.rs:197-209 (before fix)

    After all headers were processed, the vulnerable parser selected chunked decoding whenever chunked was true. It did not treat the simultaneously present Content-Length as an error.

    rust
    if chunked {
        Ok(PayloadLength::Payload(PayloadType::Payload(
            PayloadDecoder::chunked(),
        )))
    } else if let Some(len) = content_length {
        Ok(PayloadLength::Payload(PayloadType::Payload(
            PayloadDecoder::length(len),
        )))
    }
    
  4. Step 03actix-http/src/h1/decoder.rs:275-293 (fix)

    The patched decoder validates transfer-encoding semantics after headers are installed and before payload decoding is chosen. If both Transfer-Encoding and Content-Length are present, parsing fails with ParseError::Header.

    rust
    if msg.head().headers.contains_key(header::TRANSFER_ENCODING) {
        if ver == Version::HTTP_10 {
            return Err(ParseError::Header);
        }
    
        if !crate::HttpMessage::chunked(&msg)? {
            return Err(ParseError::Header);
        }
    
        if msg.head().headers.contains_key(header::CONTENT_LENGTH) {
            return Err(ParseError::Header);
        }
    }
    
  5. Step 04actix-http/src/h1/codec.rs:241-252

    The regression test sends a CL.TE request followed by a second request on the same buffer and asserts that the first decode fails rather than leaving the second request to be parsed on the backend connection.

    rust
    let mut buf = BytesMut::from(
        "POST /test HTTP/1.1\r\n\
         content-length: 11\r\n\
         transfer-encoding: chunked\r\n\r\n\
         0\r\n\r\n\
         GET /test2 HTTP/1.1\r\n\r\n",
    );
    
    assert!(codec.decode(&mut buf).is_err());
    
  6. Sinkactix-http/src/h1/decoder.rs:197

    PayloadDecoder::chunked selection

Impact

Reported impact

The bug creates a backend request desynchronization primitive. In affected proxy deployments, an attacker can influence the integrity of requests processed by the Actix service behind the intermediary. The advisory scores no direct vulnerable-system confidentiality or availability impact, but the practical effect depends on how the frontend and backend route, authenticate, and reuse the desynchronized connection.

Attack surface

Actix services using vulnerable actix-http versions behind an HTTP/1.1 proxy, load balancer, WAF, or other intermediary that can disagree with the backend about whether Content-Length or Transfer-Encoding: chunked defines the request body.

Preconditions

The attacker needs network access to the frontend. Exploitation requires a CL.TE topology: the frontend must honor Content-Length, forward the conflicting Transfer-Encoding: chunked header, and reuse a backend HTTP/1.1 connection to the Actix service.

Attack path

  1. 1

    Send a request containing both Content-Length and Transfer-Encoding: chunked through a frontend intermediary.

  2. 2

    Make the Content-Length view and chunked view disagree about where the first request body ends.

  3. 3

    The frontend forwards bytes according to its framing decision while the vulnerable Actix backend switches to chunked decoding.

  4. 4

    Bytes after the terminating chunk marker remain on the backend connection and can be parsed as a second request.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    Use actix-http 3.12.0 or earlier behind an HTTP/1.1 intermediary that forwards conflicting Content-Length plus Transfer-Encoding: chunked requests to the Actix backend over a reused connection.

  2. 02

    Target configuration

    The vulnerable parser is specific to HTTP/1.x request parsing. HTTP/2 framing is not part of this issue.

  3. 03

    Exploit delivery

    Send an ambiguous request shaped like:

    http
    POST /test HTTP/1.1
    Host: victim
    Content-Length: 11
    Transfer-Encoding: chunked
    
    0
    
    GET /test2 HTTP/1.1
    Host: victim
    
    

    The exact Content-Length value and smuggled request layout depend on the frontend parser behavior.

  4. 04

    Expected response

    Affected versions accept the first message and choose chunked decoding. Fixed versions return a parse error before a payload decoder is selected.

  5. 05

    Outcome

    In a vulnerable CL.TE proxy topology, the trailing bytes can be interpreted as a second backend request on the reused Actix connection.

Remediation

Guidance

Upgrade to actix-http 3.12.1 or later. If an immediate upgrade is not possible, configure upstream HTTP intermediaries to reject any HTTP/1.1 request containing both Content-Length and Transfer-Encoding, and avoid forwarding ambiguous request framing to Actix backends.

Before and after−13+16
if chunked {    // Chunked encoding    Ok(PayloadLength::Payload(PayloadType::Payload(        PayloadDecoder::chunked(),    )))} else if has_upgrade_websocket {    Ok(PayloadLength::UpgradeWebSocket)} else if let Some(len) = content_length {    // Content-Length    Ok(PayloadLength::Payload(PayloadType::Payload(        PayloadDecoder::length(len),    )))}if msg.head().headers.contains_key(header::TRANSFER_ENCODING) {    if ver == Version::HTTP_10 {        debug!("Transfer-Encoding is not allowed in HTTP/1.0 requests");        return Err(ParseError::Header);    }     if !crate::HttpMessage::chunked(&msg)? {        debug!("request Transfer-Encoding must be chunked");        return Err(ParseError::Header);    }     if msg.head().headers.contains_key(header::CONTENT_LENGTH) {        debug!("both Content-Length and Transfer-Encoding are set");        return Err(ParseError::Header);    }}
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

Investigate the paths that matter in your codebase.

Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.