Skip to content

HTTP/1.0 detection off-by-one enables request smuggling via chunked TE

Critical
scaprile published GHSA-mgp5-rjrv-h5j3 Aug 12, 2026

Package

https://github.com/cesanta/mongoose

Affected versions

7.21

Patched versions

7.22

Description

Summary

An off-by-one error in Mongoose's HTTP version detection causes the server to never recognize HTTP/1.0 requests. The check hm.proto.len > 8 at line 2781 is always false because mg_http_parse() enforces proto.len == 8 for valid requests. As a result, Transfer-Encoding: chunked is accepted for HTTP/1.0 requests, violating RFC 7230 (chunked was introduced in HTTP/1.1). When Mongoose sits behind a proxy operating in HTTP/1.0 mode, this enables CL.TE request smuggling — the proxy uses Content-Length while Mongoose processes chunked encoding, allowing an attacker to inject arbitrary requests.


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


Vulnerability Details

Root Cause

The HTTP version detection at line 2780–2781:

is_http_1_0 =
    hm.proto.len > 8 && mg_ncasecmp(hm.proto.buf, "HTTP/1.0", 8) == 0;

The condition hm.proto.len > 8 requires the protocol string to be longer than 8 characters. However, mg_http_parse() enforces hm->proto.len == 8 for all valid HTTP requests (line 2032):

if (!is_response && hm->proto.len > 0 &&
    (!version_prefix_valid || hm->proto.len != 8 ||  // <-- enforces len == 8
     (hm->proto.buf[5] < '0' || hm->proto.buf[5] > '9') ||
     (hm->proto.buf[6] != '.') ||
     (hm->proto.buf[7] < '0' || hm->proto.buf[7] > '9'))) {
  return -1;
}

Since 8 > 8 is always false, is_http_1_0 is never true. The guard at line 2783 that prevents chunked TE for HTTP/1.0 never activates:

// HTTP/1.0 does not use "Transfer-Encoding: chunked"
if (!is_http_1_0 &&                                          // <-- always true
    (te = mg_http_get_header(&hm, "Transfer-Encoding")) != NULL) {
  if (mg_strcasecmp(*te, mg_str("chunked")) == 0) {
    is_chunked = true;                                       // <-- always reached
  }
}

Smuggling Scenario

When Mongoose sits behind a reverse proxy operating in HTTP/1.0 mode:

  1. Proxy (HTTP/1.0): Sees Content-Length: 0 and Transfer-Encoding: chunked. Since HTTP/1.0 does not define chunked TE, the proxy ignores the Transfer-Encoding header and uses Content-Length: 0. It considers the request body empty and forwards everything after the headers as the start of the next request.
  2. Mongoose (backend): Due to the off-by-one bug, does not detect HTTP/1.0 and processes Transfer-Encoding: chunked. It reads the chunked body, then treats any data after the 0\r\n\r\n chunk terminator as a new pipelined request.
  3. Result: The attacker's smuggled request (embedded in the chunked body) is processed by Mongoose as a separate request, but the proxy attributes it to the next legitimate connection — a classic CL.TE desync.

Proof of Concept

// Minimal C proof that HTTP/1.0 detection is broken in Mongoose v7.21
// Build: gcc -DMG_ENABLE_LINES -I<mongoose_src> poc.c -o poc
// Run:   ./poc

#include "mongoose.c"
#include <stdio.h>

int main(void) {
    struct mg_http_message hm;
    const char *req10 = "GET / HTTP/1.0\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n";
    const char *req11 = "GET / HTTP/1.1\r\nHost: test\r\nTransfer-Encoding: chunked\r\n\r\n";

    printf("Mongoose v%s HTTP/1.0 off-by-one PoC\n\n", MG_VERSION);

    mg_http_parse(req10, strlen(req10), &hm);
    int bug_1_0 = hm.proto.len > 8 && mg_ncasecmp(hm.proto.buf, "HTTP/1.0", 8) == 0;
    printf("HTTP/1.0: proto.len=%zu, is_http_1_0=%d (SHOULD BE 1)\n",
           hm.proto.len, bug_1_0);

    mg_http_parse(req11, strlen(req11), &hm);
    int bug_1_1 = hm.proto.len > 8 && mg_ncasecmp(hm.proto.buf, "HTTP/1.1", 8) == 0;
    printf("HTTP/1.1: proto.len=%zu, is_http_1_1=%d (SHOULD BE 0 — not used)\n",
           hm.proto.len, bug_1_1);

    printf("\nBUG: hm.proto.len > 8 is ALWAYS false (proto.len is always 8)\n");
    printf("FIX: Change > 8 to >= 8 at mongoose.c:2781\n");
    printf("\nIMPACT: Chunked Transfer-Encoding accepted for HTTP/1.0 requests.\n");
    printf("        Enables CL.TE request smuggling behind HTTP/1.0 proxies.\n");
    return bug_1_0 == 0 ? 0 : 1;
}

Steps to Reproduce

gcc -DMG_ENABLE_LINES -I<mongoose_src> poc.c -o poc
./poc

Expected (correct) Output

HTTP/1.0: proto.len=8, is_http_1_0=1 (SHOULD BE 1)

Actual Output (vulnerable)

Mongoose v7.21 HTTP/1.0 off-by-one PoC

HTTP/1.0: proto.len=8, is_http_1_0=0 (SHOULD BE 1)
HTTP/1.1: proto.len=8, is_http_1_1=0 (SHOULD BE 0 — not used)

BUG: hm.proto.len > 8 is ALWAYS false (proto.len is always 8)
FIX: Change > 8 to >= 8 at mongoose.c:2781

IMPACT: Chunked Transfer-Encoding accepted for HTTP/1.0 requests.
        Enables CL.TE request smuggling behind HTTP/1.0 proxies.

Output Analysis

Input proto.len > 8 (buggy) >= 8 (fixed) Result
GET / HTTP/1.0\r\n... 8 false true HTTP/1.0 NOT detected
GET / HTTP/1.1\r\n... 8 false true (not checked for 1.1)

The bug is unconditional — every HTTP/1.0 request is misidentified.


Impact

  • Confidentiality: High — smuggled requests access resources intended for other sessions (cached responses, in-flight authenticated requests).
  • Integrity: High — smuggled requests can modify state on behalf of other users (CSRF-equivalent without same-origin requirements).
  • Availability: None.
  • Attack vector: Network. Requires Mongoose behind a proxy operating in HTTP/1.0 mode. Common configurations include stunnel → Mongoose, HAProxy with option http-server-close, legacy load balancers, and embedded gateway appliances using HTTP/1.0 upstream. Mongoose is widely deployed in IoT/embedded devices, which are frequently accessed through reverse proxies, VPN appliances, or cloud gateways that may use HTTP/1.0 for backend connections.

Suggested Fix

Change > to >= at line 2781:

// Before (buggy):
is_http_1_0 =
    hm.proto.len > 8 && mg_ncasecmp(hm.proto.buf, "HTTP/1.0", 8) == 0;

// After (fixed):
is_http_1_0 =
    hm.proto.len >= 8 && mg_ncasecmp(hm.proto.buf, "HTTP/1.0", 8) == 0;

Patched In

Fixed in the 2026-06-23 Mongoose release (SBOM 7.21_1ddf6c2cbd_5ab08203 or later).


References

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

CVE ID

CVE-2026-73256

Weaknesses

Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')

The product acts as an intermediary HTTP agent (such as a proxy or firewall) in the data flow between two entities such as a client and server, but it does not interpret malformed HTTP requests or responses in ways that are consistent with how the messages will be processed by those entities that are at the ultimate destination. Learn more on MITRE.

Credits