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
CVSS 4.0 base score
MediumVector: 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
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
- Source · attacker-controlledactix-http/
src/ h1/ decoder.rs:75 MessageType::set_headers
- Step 01actix-http/
src/ h1/ decoder.rs:75-150 set_headersconsumed the raw httparse header indexes and accepted both framing headers:Content-Lengthpopulatedcontent_length, whileTransfer-Encoding: chunkedsetchunked = 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; } } _ => {} } - Step 02actix-http/
src/ h1/ decoder.rs:197-209 (before fix) After all headers were processed, the vulnerable parser selected chunked decoding whenever
chunkedwas true. It did not treat the simultaneously presentContent-Lengthas 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), ))) } - 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-EncodingandContent-Lengthare present, parsing fails withParseError::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); } } - 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()); - 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
Send a request containing both
Content-LengthandTransfer-Encoding: chunkedthrough a frontend intermediary. - 2
Make the
Content-Lengthview and chunked view disagree about where the first request body ends. - 3
The frontend forwards bytes according to its framing decision while the vulnerable Actix backend switches to chunked decoding.
- 4
Bytes after the terminating chunk marker remain on the backend connection and can be parsed as a second request.
Proof of concept
- 01
Environment setup
Use
actix-http3.12.0 or earlier behind an HTTP/1.1 intermediary that forwards conflictingContent-LengthplusTransfer-Encoding: chunkedrequests to the Actix backend over a reused connection. - 02
Target configuration
The vulnerable parser is specific to HTTP/1.x request parsing. HTTP/2 framing is not part of this issue.
- 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: victimThe exact
Content-Lengthvalue and smuggled request layout depend on the frontend parser behavior. - 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.
- 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.
if chunked {// Chunked encodingOk(PayloadLength::Payload(PayloadType::Payload(PayloadDecoder::chunked(),)))} else if has_upgrade_websocket {Ok(PayloadLength::UpgradeWebSocket)} else if let Some(len) = content_length {// Content-LengthOk(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); }}
Check the upstream record for the project's current remediation status.
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.

