On Mon, Apr 13, 2026 at 03:44:20PM +0200, Claudio Jeker wrote:
> On Mon, Apr 13, 2026 at 12:42:30PM +0000, Mohamed Lemine Ahmed Jidou wrote:
> > Hello,
> > 
> > During a source code review of httpd, I identified an RFC compliance issue 
> > in how the HTTP header parser handles unrecognized or obfuscated 
> > Transfer-Encoding values.
> > 
> > Instead of actively rejecting requests with malformed transfer encodings 
> > (as mandated by RFC 7230), httpd silently ignores the header and falls back 
> > to using the Content-Length header to determine the message body length. 
> > When httpd is deployed behind a frontend reverse proxy (e.g., HAProxy, 
> > Nginx), this semantic discrepancy allows an attacker to perform HTTP 
> > Request Smuggling (TE.CL) attacks.
> > 
> > ### 1. Vulnerability Analysis & Source Code Reference
> > 
> > In usr.sbin/httpd/server_http.c, within the server_read_http function, the 
> > parser handles the Transfer-Encoding header as follows:
> > 
> > ```c
> > /* usr.sbin/httpd/server_http.c */
> > if (strcasecmp("Transfer-Encoding", key) == 0 &&
> > strcasecmp("chunked", value) == 0)
> > desc->http_chunked = 1;
> > 
> > ```
> > 
> > The code strictly expects the value to be exactly "chunked". If an attacker 
> > supplies an obfuscated value that a frontend proxy might normalize or 
> > accept (e.g., Transfer-Encoding: chunked, identity or Transfer-Encoding: 
> > chunked\\r), the strcasecmp condition evaluates to false.
> > 
> > Because there is no else block to handle unrecognized values for this 
> > specific header, desc->http_chunked remains 0. The parser proceeds without 
> > error and later uses clt->clt_toread (which was populated if a 
> > Content-Length header was also provided in the request) to determine the 
> > body size.
> > 
> > ### 2. RFC 7230 Violation
> > 
> > This behavior is a direct violation of RFC 7230, Section 3.3.3, Paragraph 
> > 3, which dictates how servers must handle requests containing both 
> > Transfer-Encoding and Content-Length:
> > 
> > > "If a message is received with both a Transfer-Encoding and a 
> > > Content-Length header field, the Transfer-Encoding overrides the 
> > > Content-Length. [...] If a Transfer-Encoding header field is present in a 
> > > request and the chunked transfer coding is not the final encoding, the 
> > > message body length cannot be determined reliably; the server MUST 
> > > respond with the 400 (Bad Request) status code and then close the 
> > > connection."
> > 
> > By silently falling back to Content-Length rather than returning a 400 Bad 
> > Request, httpd breaks the request boundary synchronization with upstream 
> > proxies, creating a classic TE.CL smuggling vector.
> > 
> > ### 3. Steps to Reproduce (PoC)
> > 
> > To reproduce this behavior on a standalone OpenBSD httpd instance, send a 
> > POST request with an invalid Transfer-Encoding and a valid Content-Length.
> > 
> > Run the following command against httpd:
> > 
> > ```bash
> > printf "POST / HTTP/1.1\r\nHost: target.local\r\nContent-Length: 
> > 6\r\nTransfer-Encoding: chunked, invalid\r\n\r\n0\r\n\r\nX" | nc <HTTPD_IP> 
> > 80
> > 
> > **Expected Result (per RFC 7230):** The server MUST reject the request 
> > immediately with a `HTTP/1.0 400 Bad Request` and close the connection 
> > because the transfer encoding is not strictly "chunked". **Actual 
> > Vulnerable Result:** The server accepts the request and responds with a 
> > `200 OK`, `403 Forbidden`, or `404 Not Found` (depending on the configured 
> > routes). It successfully processes the request using the `Content-Length: 
> > 6` to consume the `0\\r\\n\\r\\nX` payload, completely ignoring the 
> > malformed `Transfer-Encoding` header. ### 4. Security Impact In a typical 
> > reverse-proxy architecture, the frontend proxy processes the request using 
> > the `Transfer-Encoding` (interpreting the `0\\r\\n\\r\\n` as the end of the 
> > chunked request), while `httpd` processes it using the `Content-Length`. 
> > This leaves the trailing bytes (the smuggled request) in the backend TCP 
> > buffer. The backend `httpd` will process this smuggled payload as the 
> > beginning of the next user's HTTP request, leading to cache poisoning, WAF 
> > bypass, or cross-user response hijacking. ### 5. Proposed Fix The parser 
> > should explicitly reject the request if the `Transfer-Encoding` header is 
> > present but its value is not supported. Suggested patch logic in 
> > `server_http.c`: c
> > if (strcasecmp("Transfer-Encoding", key) == 0) {
> > if (strcasecmp("chunked", value) == 0) {
> > desc->http_chunked = 1;
> > 
> > } else {
> > server_abort_http(clt, 400, "malformed transfer-encoding");
> > return;
> > }
> > }
> > ```
>  
> Playing devils advocate here. If your request makes it through the WAF /
> reverse proxy then aren't those systems vulnerable to this and not httpd?
> 
> Now the handling of Transfer-Encoding and Content-Length needs to be
> stricter in httpd, your simplistic fix is IMO not enough.
> 
> RFC9112 has:
> A sender MUST NOT send a Content-Length header field in any message that
> contains a Transfer-Encoding header field.
> 
> Your diff only covers possible headers like:
>       Transfer-Encoding: gzip, chunked
> 
> Which is a start.
> 
> -- 
> :wq Claudio
> 

Here is a diff that actually applies.

The 2nd hunk is enforcing that Content-Length and Transfer-Encoding:
chunked can not co-exist.

-- 
:wq Claudio

Index: server_http.c
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/server_http.c,v
diff -u -p -r1.161 server_http.c
--- server_http.c       2 Mar 2026 19:24:58 -0000       1.161
+++ server_http.c       13 Apr 2026 14:18:54 -0000
@@ -396,8 +396,13 @@ server_read_http(struct bufferevent *bev
                }
 
                if (strcasecmp("Transfer-Encoding", key) == 0 &&
-                   strcasecmp("chunked", value) == 0)
+                   strcasecmp("chunked", value) == 0) {
                        desc->http_chunked = 1;
+               } else {
+                       server_abort_http(clt, 400,
+                           "malformed transfer-encoding");
+                       goto abort;
+               }
 
                if (clt->clt_line != 1) {
                        if ((hdr = kv_add(&desc->http_headers, key,
@@ -479,6 +484,10 @@ server_read_http(struct bufferevent *bev
                case HTTP_METHOD_TRACE:
                default:
                        server_abort_http(clt, 405, "method not allowed");
+                       return;
+               }
+               if (clt->clt_toread > 0 && desc->http_chunked) {
+                       server_abort_http(clt, 400, "malformed");
                        return;
                }
                if (desc->http_chunked) {

Reply via email to