May 7, 2026·7 min read·1 visit
Bandit < 1.11.0 accepts duplicate Content-Length headers and processes only the first one, violating RFC 9112. When deployed behind certain reverse proxies, this allows attackers to smuggle hidden HTTP requests to bypass frontend access controls.
The Bandit HTTP server for Elixir versions prior to 1.11.0 fails to correctly process requests containing multiple Content-Length headers. This inconsistent interpretation creates a CL.CL HTTP request smuggling vulnerability when Bandit is deployed behind a reverse proxy that parses the headers differently. Attackers exploit this desynchronization to smuggle secondary HTTP requests past edge security controls.
Bandit is a prevalent HTTP server written in Elixir, often utilized as a robust backend for Phoenix applications. Versions prior to 1.11.0 contain a critical protocol parsing flaw classified under CWE-444: Inconsistent Interpretation of HTTP Requests. The server improperly handles incoming HTTP requests that present duplicate Content-Length headers, violating the strict parsing requirements outlined in RFC 9112.
The vulnerability surfaces specifically in distributed architectures where Bandit is situated behind a frontend reverse proxy, such as older versions of Nginx, HAProxy, or specific cloud load balancers. When an attacker submits an HTTP request containing multiple Content-Length headers, the frontend proxy and the Bandit backend must agree on the exact boundaries of the request body. If the parsing logic diverges, the backend and frontend lose synchronization over the TCP stream.
In this specific case, the desynchronization manifests as a CL.CL request smuggling vector. Bandit relies on a "first-wins" logic for extracting the payload length, whereas a vulnerable frontend proxy might utilize a "last-wins" logic. This discrepancy allows an attacker to dictate two different request lengths simultaneously.
The resulting impact is the ability to smuggle arbitrary HTTP requests within the apparent body of a primary request. The frontend proxy inspects only the primary request, allowing the smuggled payload to bypass Web Application Firewalls (WAFs), rate-limiting mechanisms, and path-based access control lists (ACLs) entirely.
The root cause resides in the header extraction logic implemented within lib/bandit/headers.ex. Specifically, the function 'Elixir.Bandit.Headers':get_content_length/1 is responsible for determining the size of the incoming HTTP request body. This function processes the parsed header list to extract the standard Content-Length integer value.
The vulnerability is introduced through the use of the Elixir standard library function List.keyfind/3. In Elixir, HTTP headers are typically represented as a list of two-element tuples, structured as {String.t(), String.t()}. List.keyfind/3 operates by iterating over the list and returning the very first tuple that matches the specified key.
When an attacker transmits a request containing two separate Content-Length headers, List.keyfind/3 successfully locates the first instance and halts its search. Bandit subsequently utilizes this initial value to determine the boundaries of the request body. The server entirely ignores the presence of the secondary Content-Length header.
RFC 9112 Section 6.3 explicitly mandates that a recipient must reject requests containing multiple Content-Length headers with varying values, typically by issuing a 400 Bad Request response. By silently accepting the first value and continuing execution, Bandit inadvertently facilitates the exact framing error required for request smuggling.
The vulnerable implementation blindly trusts the first matching header tuple without validating the uniqueness of the key across the entire header list. The execution flow extracts the initial value, parses it as an integer, and proceeds to read from the socket buffer.
# Vulnerable logic (simplified representation)
def get_content_length(headers) do
case List.keyfind(headers, "content-length", 0) do
{_, value} -> parse_content_length(value)
nil -> {:ok, nil}
end
endThe fix, introduced in commit f2ca636eb6df385219957e8934e9fc6efa1630d1, completely replaces the List.keyfind/3 approach. The patched code utilizes Enum.filter/2 to explicitly collect every instance of the content-length key present in the header list.
# Patched logic in lib/bandit/headers.ex
def get_content_length(headers) do
case Enum.filter(headers, &(elem(&1, 0) == "content-length")) do
[] -> {:ok, nil}
[{"content-length", value}] -> parse_content_length(value)
_ -> {:error, "invalid content-length header (RFC9112§6.3)"}
end
endThis structural change enforces strict protocol compliance. The case statement validates the output of the filter operation. If the resulting list contains exactly one tuple, the value is parsed. If the list contains multiple tuples, the execution drops to the catch-all _ branch, actively returning an error tuple. This error state triggers an immediate 400 Bad Request response, neutralizing the smuggling vector.
Exploiting this vulnerability strictly requires a frontend proxy that prioritizes the last Content-Length header or forwards duplicate headers unaltered to the backend. The attacker targets the application by crafting a monolithic HTTP payload that embeds a secondary, malicious request within the body boundaries defined by the frontend.
The attacker constructs a request with two conflicting length declarations. For example, the first header asserts Content-Length: 4, while the second header asserts Content-Length: 50. The body of the request contains the 4 legitimate bytes, immediately followed by the smuggled 46-byte HTTP request targeting a restricted endpoint.
The frontend proxy, adhering to its "last-wins" logic, processes the Content-Length: 50 header. It consumes the entire 50-byte payload from the attacker and forwards the monolithic block over a persistent keep-alive connection to Bandit. The proxy evaluates only the primary request headers against its security rules.
Bandit receives the transmission and parses the headers, terminating its search at Content-Length: 4. It reads exactly 4 bytes from the socket buffer to complete the first request. Because the connection utilizes keep-alive, Bandit immediately attempts to parse the next request from the remaining 46 bytes sitting in the buffer. The smuggled request is executed with the internal trust level of the proxy connection.
The security impact of a successful HTTP request smuggling attack primarily involves the bypass of security perimeter controls. Frontend reverse proxies typically enforce path-based access control, block malicious query parameters, and handle authentication validation. By smuggling a request, an attacker routes traffic directly to the backend Elixir application without triggering any of these edge inspections.
This bypass enables direct access to internal administrative endpoints, internal APIs, or unprotected administrative panels that rely on frontend routing restrictions for security. Depending on the backend application logic, this can lead to credential extraction, data manipulation, or unauthorized administrative actions.
The CVSS v4.0 score is calculated as 6.3 (Medium), utilizing the vector CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N. The Attack Requirements (AT) metric is classified as Present (P) because the vulnerability cannot be exploited in isolation; it strictly requires a misconfigured or vulnerable frontend proxy to facilitate the payload delivery.
The Exploit Prediction Scoring System (EPSS) assigns a probability score of 0.00017, placing it in the 4.03 percentile. This low score accurately reflects the complex environmental prerequisites necessary for successful exploitation. Widespread, automated exploitation is unlikely without specific target profiling.
The primary and most effective remediation is upgrading the bandit dependency to version 1.11.0 or later. This patch fundamentally corrects the header parsing logic, ensuring that Bandit rejects malformed requests at the protocol level. Development teams must update their mix.exs files and execute mix deps.get to apply the upstream fix.
In environments where immediate backend patching is restricted, security engineers must implement strict header normalization at the frontend reverse proxy or WAF layer. Proxies must be configured to actively drop requests containing duplicate Content-Length headers, or alternatively, normalize the request to contain a single, validated length declaration before forwarding traffic to the backend.
Security operations teams should deploy detection rules to identify exploitation attempts. Network intrusion detection systems can flag inbound HTTP requests bearing multiple Content-Length headers. Additionally, backend application logs should be monitored for the specific error string invalid content-length header (RFC9112§6.3), which indicates that the patched Bandit server is actively terminating smuggled payloads.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
bandit mtrudel | < 1.11.0 | 1.11.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-444 |
| Attack Vector | Network |
| CVSS v4.0 | 6.3 |
| EPSS Score | 0.00017 (4.03%) |
| Impact | Security Control Bypass |
| Exploit Status | Proof of Concept |
| KEV Status | Not Listed |
Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')