commit ec6e6d288876606af964747f97379aa9371c4206
parent db3d2d32f56fadc0f668d3113a9b3b78a9ab0c96
Author: Evgeny Grin (Karlson2k) <k2k@narod.ru>
Date: Sat, 28 Feb 2026 23:38:52 +0100
Added MHD_OPTION_ALLOW_BIN_ZERO_IN_URI_PATH and MHD_get_connection_URI_path_n()
Diffstat:
5 files changed, 140 insertions(+), 13 deletions(-)
diff --git a/ChangeLog b/ChangeLog
@@ -1,3 +1,7 @@
+Sat Feb 28 23:38:39 CET 2026
+ Added MHD_OPTION_ALLOW_BIN_ZERO_IN_URI_PATH daemon option.
+ Added new function MHD_get_connection_URI_path_n(). -EG
+
Mon Jul 14 05:03:07 PM CEST 2025
Fix double-close bugs on bind() errors reported by MC on the list.
Removed MHD2 draft code, now in libmicrohttpd2.git.
diff --git a/src/include/microhttpd.h b/src/include/microhttpd.h
@@ -101,7 +101,7 @@ MHD_C_DECLRATIONS_START_HERE_
* they are parsed as decimal numbers.
* Example: 0x01093001 = 1.9.30-1.
*/
-#define MHD_VERSION 0x01000200
+#define MHD_VERSION 0x01000201
/* If generic headers don't work on your platform, include headers
which define 'va_list', 'size_t', 'ssize_t', 'intptr_t', 'off_t',
@@ -2198,6 +2198,26 @@ enum MHD_OPTION
* @note Available since #MHD_VERSION 0x00097709
*/
MHD_OPTION_DIGEST_AUTH_DEFAULT_MAX_NC = 42
+ ,
+ /**
+ * Default maximum nc (nonce count) value used for Digest Auth.
+ * This option must be followed by an 'int' argument.
+ * If followed by '0' (default) then:
+ * + requests are rejected if request URI has binary zero (the result
+ * of %00 decoding) in the path part of the URI.
+ * If followed by '1' then:
+ * + binary zero is allowed in request URI path;
+ * + #MHD_AccessHandlerCallback called with NULL in @a url parameter when
+ * request URI path has binary zero, the full @a url is available only
+ * via #MHD_get_connection_URI_path_n()
+ * If followed by '2' (unsafe!) then:
+ * + binary zero is allowed in request URI path;
+ * + #MHD_AccessHandlerCallback called with truncated (unsafe!) @a url
+ * parameter when request URI path has binary zero.
+ * @see #MHD_get_connection_URI_path_n()
+ * @note Available since #MHD_VERSION 0x01000201
+ */
+ MHD_OPTION_ALLOW_BIN_ZERO_IN_URI_PATH = 43
} _MHD_FIXED_ENUM;
@@ -2689,7 +2709,9 @@ typedef enum MHD_Result
* @param cls argument given together with the function
* pointer when the handler was registered with MHD
* @param connection the connection handle
- * @param url the requested url
+ * @param url the requested url, can be truncated or can be NULL, depending
+ * on #MHD_OPTION_ALLOW_BIN_ZERO_IN_URI_PATH option,
+ * see #MHD_get_connection_URI_path_n()
* @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
* #MHD_HTTP_METHOD_PUT, etc.)
* @param version the HTTP version string (i.e.
@@ -2718,7 +2740,7 @@ typedef enum MHD_Result
* #MHD_NO if the socket must be closed due to a serious
* error while handling the request
*
- * @sa #MHD_queue_response()
+ * @sa #MHD_queue_response(), #MHD_get_connection_URI_path_n()
*/
typedef enum MHD_Result
(*MHD_AccessHandlerCallback)(void *cls,
@@ -3518,6 +3540,39 @@ MHD_run_from_select2 (struct MHD_Daemon *daemon,
/* **************** Connection handling functions ***************** */
+
+/**
+ * Get request URI path (the request target without query part).
+ *
+ * The value obtained by this function is the same value as @a url provided
+ * for #MHD_AccessHandlerCallback callback, but this function also provides
+ * the size of the string.
+ *
+ * This function is critically important when binary zero is allowed by daemon
+ * option #MHD_OPTION_ALLOW_BIN_ZERO_IN_URI_PATH as this is the only way to
+ * get the non-truncated request URI.
+ *
+ * Returned @a uri pointer is valid until response is started or connection
+ * is terminated.
+ *
+ * @param connection the connection to URI from
+ * @param[out] uri set to the request URI without query part, may contain
+ * binary zeros (NUL) characters, never set to NULL on
+ * success; can be NULL
+ * @param[out] uri_size set to the size of the @a uri in bytes, not including
+ * final zero-termination; can be NULL
+ * @return #MHD_NO if failed (request is not yet processed or response has
+ * been queued already);
+ * #MHD_YES on success (*uri set to valid pointer)
+ * @note Available since #MHD_VERSION 0x01000201
+ * @sa #MHD_OPTION_ALLOW_BIN_ZERO_IN_URI_PATH, #MHD_AccessHandlerCallback
+ * @ingroup request
+ */
+_MHD_EXTERN enum MHD_Result
+MHD_get_connection_URI_path_n (struct MHD_Connection *connection,
+ const char **uri,
+ size_t *uri_size);
+
/**
* Get all of the headers from the request.
*
diff --git a/src/microhttpd/connection.c b/src/microhttpd/connection.c
@@ -513,6 +513,13 @@
#endif
/**
+ * Response text used when the request target path has %00 sequence.
+ */
+#define REQUEST_HAS_NUL_CHAR_IN_PATH \
+ "<html><head><title>Bad Request Path</title></head>" \
+ "<body>The request path contains invalid characters.</body></html>"
+
+/**
* Response text used when the request HTTP chunked encoding is
* malformed.
*/
@@ -792,6 +799,32 @@ recv_param_adapter (struct MHD_Connection *connection,
}
+_MHD_EXTERN enum MHD_Result
+MHD_get_connection_URI_path_n (struct MHD_Connection *connection,
+ const char **uri,
+ size_t *uri_size)
+{
+ if (NULL != uri)
+ *uri = NULL;
+ if (NULL != uri_size)
+ *uri_size = 0u;
+
+ if (connection->state < MHD_CONNECTION_REQ_LINE_RECEIVED)
+ return MHD_NO;
+ if (connection->state >= MHD_CONNECTION_START_REPLY)
+ return MHD_NO;
+ if (NULL == connection->rq.url)
+ return MHD_NO;
+
+ if (NULL != uri)
+ *uri = connection->rq.url;
+ if (NULL != uri_size)
+ *uri_size = connection->rq.url_len;
+
+ return MHD_YES;
+}
+
+
/**
* Get all of the headers from the request.
*
@@ -2917,6 +2950,7 @@ transmit_error_response_len (struct MHD_Connection *connection,
connection->rq.method = NULL;
connection->rq.url = NULL;
connection->rq.url_len = 0;
+ connection->rq.url_for_callback = NULL;
connection->rq.headers_received = NULL;
connection->rq.headers_received_tail = NULL;
connection->write_buffer = NULL;
@@ -4402,7 +4436,7 @@ call_connection_handler (struct MHD_Connection *connection)
if (MHD_NO ==
daemon->default_handler (daemon->default_handler_cls,
connection,
- connection->rq.url,
+ connection->rq.url_for_callback,
connection->rq.method,
connection->rq.version,
NULL,
@@ -4435,11 +4469,9 @@ process_request_body (struct MHD_Connection *connection)
bool instant_retry;
char *buffer_head;
const int discp_lvl = daemon->client_discipline;
- /* Treat bare LF as the end of the line.
- RFC 9112, section 2.2-3
- Note: MHD never replaces bare LF with space (RFC 9110, section 5.5-5).
- Bare LF is processed as end of the line or rejected as broken request. */
- const bool bare_lf_as_crlf = MHD_ALLOW_BARE_LF_AS_CRLF_ (discp_lvl);
+ /* RFC does not allow LF as the line termination in chunk headers.
+ See RFC 9112, section 7.1 and section 2.2-3 */
+ const bool bare_lf_as_crlf = (-2 > discp_lvl);
/* Allow "Bad WhiteSpace" in chunk extension.
RFC 9112, Section 7.1.1, Paragraph 2 */
const bool allow_bws = (2 > discp_lvl);
@@ -4462,6 +4494,7 @@ process_request_body (struct MHD_Connection *connection)
connection->rq.current_chunk_size) &&
(0 != connection->rq.current_chunk_size) )
{
+ /* Skip CRLF chunk termination */
size_t i;
mhd_assert (0 != available);
/* skip new line at the *end* of a chunk */
@@ -4491,12 +4524,10 @@ process_request_body (struct MHD_Connection *connection)
}
if (0 != connection->rq.current_chunk_size)
{
+ /* Process chunk "content" */
uint64_t cur_chunk_left;
mhd_assert (connection->rq.current_chunk_offset < \
connection->rq.current_chunk_size);
- /* we are in the middle of a chunk, give
- as much as possible to the client (without
- crossing chunk boundaries) */
cur_chunk_left
= connection->rq.current_chunk_size
- connection->rq.current_chunk_offset;
@@ -4532,10 +4563,12 @@ process_request_body (struct MHD_Connection *connection)
broken = (0 == num_dig);
if (broken)
+ {
/* Check whether result is invalid due to uint64_t overflow */
overflow = ((('0' <= buffer_head[0]) && ('9' >= buffer_head[0])) ||
(('A' <= buffer_head[0]) && ('F' >= buffer_head[0])) ||
(('a' <= buffer_head[0]) && ('f' >= buffer_head[0])));
+ }
else
{
/**
@@ -4646,7 +4679,7 @@ process_request_body (struct MHD_Connection *connection)
if (MHD_NO ==
daemon->default_handler (daemon->default_handler_cls,
connection,
- connection->rq.url,
+ connection->rq.url_for_callback,
connection->rq.method,
connection->rq.version,
buffer_head,
@@ -5589,6 +5622,7 @@ process_request_target (struct MHD_Connection *c)
mhd_assert (MHD_CONNECTION_REQ_LINE_RECEIVING == c->state);
mhd_assert (NULL == c->rq.url);
mhd_assert (0 == c->rq.url_len);
+ mhd_assert (NULL == c->rq.url_for_callback);
mhd_assert (NULL != c->rq.hdrs.rq_line.rq_tgt);
mhd_assert ((NULL == c->rq.hdrs.rq_line.rq_tgt_qmark) || \
(c->rq.hdrs.rq_line.rq_tgt <= c->rq.hdrs.rq_line.rq_tgt_qmark));
@@ -5630,6 +5664,7 @@ process_request_target (struct MHD_Connection *c)
params_len = 0;
#endif /* _DEBUG */
+ mhd_assert (NULL == c->rq.url_for_callback);
mhd_assert (strlen (c->rq.hdrs.rq_line.rq_tgt) == \
c->rq.req_target_len - params_len);
@@ -5640,6 +5675,18 @@ process_request_target (struct MHD_Connection *c)
c->rq.hdrs.rq_line.rq_tgt);
c->rq.url = c->rq.hdrs.rq_line.rq_tgt;
+ if (2 == c->daemon->allow_bzero_in_url)
+ c->rq.url_for_callback = c->rq.url;
+ else if (strlen (c->rq.url) == c->rq.url_len)
+ c->rq.url_for_callback = c->rq.url;
+ else if (0 == c->daemon->allow_bzero_in_url)
+ {
+ transmit_error_response_static (c,
+ MHD_HTTP_BAD_REQUEST,
+ REQUEST_HAS_NUL_CHAR_IN_PATH);
+ return false;
+ }
+
return true;
}
@@ -5684,6 +5731,7 @@ get_request_line (struct MHD_Connection *c)
mhd_assert (MHD_CONNECTION_REQ_LINE_RECEIVING == c->state);
mhd_assert (NULL == c->rq.url);
mhd_assert (0 == c->rq.url_len);
+ mhd_assert (NULL == c->rq.url_for_callback);
mhd_assert (NULL != c->rq.hdrs.rq_line.rq_tgt);
if (0 != c->rq.hdrs.rq_line.num_ws_in_uri)
{
diff --git a/src/microhttpd/daemon.c b/src/microhttpd/daemon.c
@@ -7125,6 +7125,12 @@ parse_options_va (struct MHD_Daemon *daemon,
}
#endif /* HAVE_MESSAGES */
break;
+ case MHD_OPTION_ALLOW_BIN_ZERO_IN_URI_PATH:
+ daemon->allow_bzero_in_url = va_arg (ap, int);
+ if ((0 > daemon->allow_bzero_in_url) ||
+ (2 < daemon->allow_bzero_in_url))
+ daemon->allow_bzero_in_url = 1;
+ break;
case MHD_OPTION_ARRAY:
params->num_opts--; /* Do not count MHD_OPTION_ARRAY */
oa = va_arg (ap, struct MHD_OptionItem *);
@@ -7189,6 +7195,7 @@ parse_options_va (struct MHD_Daemon *daemon,
case MHD_OPTION_SIGPIPE_HANDLED_BY_APP:
case MHD_OPTION_TLS_NO_ALPN:
case MHD_OPTION_APP_FD_SETSIZE:
+ case MHD_OPTION_ALLOW_BIN_ZERO_IN_URI_PATH:
if (MHD_NO == parse_options (daemon,
params,
opt,
diff --git a/src/microhttpd/internal.h b/src/microhttpd/internal.h
@@ -1106,6 +1106,12 @@ struct MHD_Request
size_t req_target_len;
/**
+ * Requested URL (everything after "GET" only).
+ * Depending on daemon setting either the same as @a URL or NULL.
+ */
+ const char *url_for_callback;
+
+ /**
* Linked list of parsed headers.
*/
struct MHD_HTTP_Req_Header *headers_received;
@@ -2264,6 +2270,13 @@ struct MHD_Daemon
*/
int client_discipline;
+ /**
+ * Allow binary zero in the URI (excluding query part).
+ * When set to '1' then @a url parameter must be NULL, when binary zero
+ * is used in URI.
+ */
+ int allow_bzero_in_url;
+
#ifdef HAS_FD_SETSIZE_OVERRIDABLE
/**
* The value of FD_SETSIZE used by the daemon.