All advisories
Draft

Internal Redirect Content-Length Mismatch Enables Request Smuggling

envoyproxy/envoy

Affected packages

envoy other
Affected versions= c33d01624d5b173b5d6e5c0c0474d480cab69b7b
Patched versionsNot specified

Description

Internal Redirect Content-Length Mismatch Enables Request Smuggling

Affected commit: c33d01624d
Sink: source/common/router/router.cc:2282

Summary

On routes that copy content-length during internal redirects, Envoy recreates a request with the upstream response's content-length value while replaying the original buffered body. An HTTP/1.1 upstream parses the leftover bytes as a second (smuggled) request, bypassing downstream RBAC.

Detail

Lines 2267-2284 iterate the responseHeadersToCopy() list from the internal redirect policy. If content-length is in that list, the upstream response's content-length overwrites the original. The buffered body remains unchanged, creating a framing mismatch.

Reproduce

#!/bin/bash
set -e

# Clone and identify HEAD
git clone --depth 1 https://github.com/envoyproxy/envoy.git /tmp/envoy-repro
cd /tmp/envoy-repro
echo "HEAD: $(git rev-parse HEAD)"

# Verify responseHeadersToCopy loop overwrites downstream headers
echo "--- Internal redirect header copy loop ---"
sed -n '2267,2286p' source/common/router/router.cc

The vulnerable code at router.cc:2267-2284:

for (const Http::LowerCaseString& header :
     route_entry_->internalRedirectPolicy().responseHeadersToCopy()) {
  Http::HeaderMap::GetResult result = upstream_headers.get(header);
  Http::HeaderMap::GetResult downstream_result = downstream_headers.get(header);
  if (result.empty()) {
    if (downstream_result.empty()) { continue; }
    downstream_headers.remove(header);
  } else {
    if (!downstream_result.empty()) {
      downstream_headers.remove(header);
    }
    for (size_t idx = 0; idx < result.size(); idx++) {
      downstream_headers.addCopy(header, result[idx]->value().getStringView());
    }
  }
}

When content-length is included in response_headers_to_copy, the upstream response's content-length header overwrites the downstream request's content-length. The buffered request body is unchanged. If the upstream returns a 302 redirect with content-length: 0 while the original request had a 100-byte body, Envoy re-sends the request to the redirect target with content-length: 0 but still writes the 100-byte body. An HTTP/1.1 upstream receiving this sees a 0-length first request followed by 100 bytes it parses as a second, smuggled request.

Expected output:

HEAD: <resolved-commit>
--- Internal redirect header copy loop ---
  for (const Http::LowerCaseString& header :
       route_entry_->internalRedirectPolicy().responseHeadersToCopy()) {
    Http::HeaderMap::GetResult result = upstream_headers.get(header);
    ...
      downstream_headers.addCopy(header, result[idx]->value().getStringView());

Credit

Zheng Yu @ Depthfirst