From 2efd41acd4299f415441d0134df9f1ad7c854374 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 7 Apr 2026 10:52:51 -0700 Subject: [PATCH 1/5] Fix std::terminate when uri_log receives null uri pointer libmicrohttpd may invoke MHD_OPTION_URI_LOG_CALLBACK with a null uri pointer before the request line is parsed - for example on port scans, TLS clients hitting a plain HTTP port, or half-open connections. The previous code assigned the raw pointer directly into a std::string, which throws std::logic_error("basic_string::_M_construct null not valid"). Because the throw originates inside an MHD C callback with no enclosing handler, std::terminate() was called and the process aborted under load. Treat a null uri as an empty string so the assignment is well-defined. An empty URI fails to match any registered resource and surfaces as a 404, which is the correct graceful behaviour. Resolves #371. --- ChangeLog | 3 +++ src/webserver.cpp | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 6e9532e3..fb6d7594 100644 --- a/ChangeLog +++ b/ChangeLog @@ -17,6 +17,9 @@ Version 0.20.0 Fixed auth skip path bypass via path traversal (e.g. /public/../protected). Fixed use of free() instead of MHD_free() for digest auth username. Fixed unchecked write error during file upload. + Fixed std::terminate when MHD invokes the URI log callback with a + null uri pointer (e.g. port scans, half-open connections, or + non-HTTP traffic). Resolves issue #371. Version 0.19.0 - 2023-06-15 diff --git a/src/webserver.cpp b/src/webserver.cpp index 971d3d5a..2b5a8028 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -626,7 +626,12 @@ void* uri_log(void* cls, const char* uri, struct MHD_Connection *con) { std::ignore = con; auto mr = std::make_unique(); - mr->complete_uri = uri; + // MHD may invoke this callback with a null uri before the request line + // has been parsed (e.g. port scans, half-open connections, or non-HTTP + // traffic on the listening port). Treat that as an empty URI so the + // std::string assignment does not throw std::logic_error and abort the + // process via std::terminate. See issue #371. + mr->complete_uri = (uri != nullptr) ? uri : ""; return reinterpret_cast(mr.release()); } From 0c83a2230659ee9f7f66c1a62e3bffb1db31b13d Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 7 Apr 2026 11:01:02 -0700 Subject: [PATCH 2/5] ci(codeql): bump bundled libmicrohttpd to 1.0.3 The CodeQL workflow was still pulling libmicrohttpd-0.9.64 from S3, which is below the project's stated minimum of 1.0.0 and is no longer served by the bucket - the install step was failing with "gzip: stdin: not in gzip format" because curl received a 243-byte error response instead of the tarball. Bump to 1.0.3 from the same S3 location so CodeQL can build the project again. --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cc0f95be..54d61fbc 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,9 +38,9 @@ jobs: - name: Install libmicrohttpd dependency run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.64.tar.gz -o libmicrohttpd-0.9.64.tar.gz ; - tar -xzf libmicrohttpd-0.9.64.tar.gz ; - cd libmicrohttpd-0.9.64 ; + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ; + tar -xzf libmicrohttpd-1.0.3.tar.gz ; + cd libmicrohttpd-1.0.3 ; ./configure --disable-examples ; make ; sudo make install ; From ddef5c78f50ba690ce2cc0aadd74ce2446ccf8a0 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 7 Apr 2026 11:02:20 -0700 Subject: [PATCH 3/5] ci: bump bundled libmicrohttpd to 1.0.3 in release and verify-build Aligns release.yml and verify-build.yml with codeql-analysis.yml so all workflows pull the same libmicrohttpd-1.0.3.tar.gz from S3. This also brings CI in line with the project's documented minimum of >= 1.0.0 (0.9.77 was below that threshold). Cache keys include the new version so existing 0.9.77 entries are not reused. --- .github/workflows/release.yml | 42 +++++++++++++++--------------- .github/workflows/verify-build.yml | 38 +++++++++++++-------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 81db8332..9430f7ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -78,21 +78,21 @@ jobs: id: cache-libmicrohttpd uses: actions/cache@v4 with: - path: libmicrohttpd-0.9.77 - key: ubuntu-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2 + path: libmicrohttpd-1.0.3 + key: ubuntu-latest-gcc-libmicrohttpd-1.0.3-pre-built-v2 - name: Build libmicrohttpd (if not cached) if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz - tar -xzf libmicrohttpd-0.9.77.tar.gz - cd libmicrohttpd-0.9.77 + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 ./configure --disable-examples make - name: Install libmicrohttpd run: | - cd libmicrohttpd-0.9.77 + cd libmicrohttpd-1.0.3 sudo make install sudo ldconfig @@ -130,21 +130,21 @@ jobs: id: cache-libmicrohttpd uses: actions/cache@v4 with: - path: libmicrohttpd-0.9.77 - key: ubuntu-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2 + path: libmicrohttpd-1.0.3 + key: ubuntu-latest-gcc-libmicrohttpd-1.0.3-pre-built-v2 - name: Build libmicrohttpd (if not cached) if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz - tar -xzf libmicrohttpd-0.9.77.tar.gz - cd libmicrohttpd-0.9.77 + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 ./configure --disable-examples make - name: Install libmicrohttpd run: | - cd libmicrohttpd-0.9.77 + cd libmicrohttpd-1.0.3 sudo make install sudo ldconfig @@ -181,21 +181,21 @@ jobs: id: cache-libmicrohttpd uses: actions/cache@v4 with: - path: libmicrohttpd-0.9.77 - key: macos-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2 + path: libmicrohttpd-1.0.3 + key: macos-latest-gcc-libmicrohttpd-1.0.3-pre-built-v2 - name: Build libmicrohttpd (if not cached) if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true' run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz - tar -xzf libmicrohttpd-0.9.77.tar.gz - cd libmicrohttpd-0.9.77 + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 ./configure --disable-examples make - name: Install libmicrohttpd run: | - cd libmicrohttpd-0.9.77 + cd libmicrohttpd-1.0.3 sudo make install - name: Fetch curl from cache @@ -263,9 +263,9 @@ jobs: - name: Build and install libmicrohttpd run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz - tar -xzf libmicrohttpd-0.9.77.tar.gz - cd libmicrohttpd-0.9.77 + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 ./configure --disable-examples --enable-poll=no make make install diff --git a/.github/workflows/verify-build.yml b/.github/workflows/verify-build.yml index 17ae2955..db762069 100644 --- a/.github/workflows/verify-build.yml +++ b/.github/workflows/verify-build.yml @@ -511,30 +511,30 @@ jobs: id: cache-libmicrohttpd uses: actions/cache@v4 with: - path: libmicrohttpd-0.9.77 - key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built-v2 + path: libmicrohttpd-1.0.3 + key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-1.0.3-pre-built-v2 if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.compiler-family != 'arm-cross' }} - name: Build libmicrohttpd dependency (if not cached) run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz ; - tar -xzf libmicrohttpd-0.9.77.tar.gz ; - cd libmicrohttpd-0.9.77 ; + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ; + tar -xzf libmicrohttpd-1.0.3.tar.gz ; + cd libmicrohttpd-1.0.3 ; ./configure --disable-examples ; make ; if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.compiler-family != 'arm-cross' && steps.cache-libmicrohttpd.outputs.cache-hit != 'true' }} - name: Build libmicrohttpd without digest auth (no-dauth test) run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz ; - tar -xzf libmicrohttpd-0.9.77.tar.gz ; - cd libmicrohttpd-0.9.77 ; + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ; + tar -xzf libmicrohttpd-1.0.3.tar.gz ; + cd libmicrohttpd-1.0.3 ; ./configure --disable-examples --disable-dauth ; make ; if: ${{ matrix.build-type == 'no-dauth' }} - name: Install libmicrohttpd - run: cd libmicrohttpd-0.9.77 ; sudo make install ; + run: cd libmicrohttpd-1.0.3 ; sudo make install ; if: ${{ matrix.os-type != 'windows' && matrix.compiler-family != 'arm-cross' }} - name: Verify digest auth is disabled (no-dauth test) @@ -550,9 +550,9 @@ jobs: - name: Build and install libmicrohttpd (Windows) if: ${{ matrix.os-type == 'windows' }} run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz - tar -xzf libmicrohttpd-0.9.77.tar.gz - cd libmicrohttpd-0.9.77 + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + cd libmicrohttpd-1.0.3 ./configure --disable-examples --enable-poll=no make make install @@ -561,16 +561,16 @@ jobs: id: cache-libmicrohttpd-arm uses: actions/cache@v4 with: - path: libmicrohttpd-0.9.77-${{ matrix.build-type }} - key: ${{ matrix.os }}-${{ matrix.build-type }}-libmicrohttpd-0.9.77-cross-compiled + path: libmicrohttpd-1.0.3-${{ matrix.build-type }} + key: ${{ matrix.os }}-${{ matrix.build-type }}-libmicrohttpd-1.0.3-cross-compiled if: ${{ matrix.compiler-family == 'arm-cross' }} - name: Cross-compile libmicrohttpd for ARM run: | - curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz - tar -xzf libmicrohttpd-0.9.77.tar.gz - mv libmicrohttpd-0.9.77 libmicrohttpd-0.9.77-${{ matrix.build-type }} - cd libmicrohttpd-0.9.77-${{ matrix.build-type }} + curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz + tar -xzf libmicrohttpd-1.0.3.tar.gz + mv libmicrohttpd-1.0.3 libmicrohttpd-1.0.3-${{ matrix.build-type }} + cd libmicrohttpd-1.0.3-${{ matrix.build-type }} mkdir -p ${{ github.workspace }}/arm-sysroot if [ "${{ matrix.build-type }}" = "arm32" ]; then ./configure --host=arm-linux-gnueabihf --prefix=${{ github.workspace }}/arm-sysroot --disable-examples --disable-doc @@ -583,7 +583,7 @@ jobs: - name: Install cross-compiled libmicrohttpd from cache run: | - cd libmicrohttpd-0.9.77-${{ matrix.build-type }} + cd libmicrohttpd-1.0.3-${{ matrix.build-type }} mkdir -p ${{ github.workspace }}/arm-sysroot make install if: ${{ matrix.compiler-family == 'arm-cross' && steps.cache-libmicrohttpd-arm.outputs.cache-hit == 'true' }} From 9fde1286aaaa7d1c253f3ba6d1f671701dd74c55 Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 7 Apr 2026 11:54:40 -0700 Subject: [PATCH 4/5] test: add unit test for uri_log null/empty/valid uri handling Adds test/unit/uri_log_test.cpp to lock in the fix for issue #371. The test calls uri_log() directly (re-declaring the symbol since it has no public header) and verifies three cases: - null uri does not throw and yields an empty complete_uri - valid uri is stored verbatim - empty uri is stored verbatim The first case is the regression check: against the unfixed code, running the test crashes the process (SIGSEGV from dereferencing the null pointer inside std::string's assignment operator on libstdc++ 13; on the older libstdc++ 10 from the bug report it threw std::logic_error and aborted via std::terminate). With the fix in place, all three sub- tests pass cleanly. The new test target needs an explicit -lmicrohttpd in its link line because it instantiates ~modded_request() directly, which references MHD_destroy_post_processor; the default LDADD only pulls libmicrohttpd in transitively via libhttpserver.la, and modern ld enforces --no-copy-dt-needed-entries. --- test/Makefile.am | 7 +++- test/unit/uri_log_test.cpp | 86 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 test/unit/uri_log_test.cpp diff --git a/test/Makefile.am b/test/Makefile.am index cdbacf26..0aa413ef 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -26,7 +26,7 @@ LDADD += -lcurl AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/ METASOURCES = AUTO -check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver +check_PROGRAMS = basic file_upload http_utils threaded nodelay string_utilities http_endpoint ban_system ws_start_stop authentication deferred http_resource http_response create_webserver uri_log MOSTLYCLEANFILES = *.gcda *.gcno *.gcov @@ -44,6 +44,11 @@ nodelay_SOURCES = integ/nodelay.cpp http_resource_SOURCES = unit/http_resource_test.cpp http_response_SOURCES = unit/http_response_test.cpp create_webserver_SOURCES = unit/create_webserver_test.cpp +uri_log_SOURCES = unit/uri_log_test.cpp +# uri_log_test directly references libmicrohttpd via ~modded_request(), so +# it needs an explicit -lmicrohttpd in its link line on top of the default +# LDADD (modern ld enforces --no-copy-dt-needed-entries). +uri_log_LDADD = $(LDADD) -lmicrohttpd noinst_HEADERS = littletest.hpp AM_CXXFLAGS += -Wall -fPIC -Wno-overloaded-virtual diff --git a/test/unit/uri_log_test.cpp b/test/unit/uri_log_test.cpp new file mode 100644 index 00000000..6eb58191 --- /dev/null +++ b/test/unit/uri_log_test.cpp @@ -0,0 +1,86 @@ +/* + This file is part of libhttpserver + Copyright (C) 2011-2019 Sebastiano Merlino + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA +*/ + +#include + +#include "httpserver.hpp" +#include "httpserver/details/modded_request.hpp" + +#include "./littletest.hpp" + +// uri_log is the MHD URI-log callback defined in src/webserver.cpp. It is +// exported from the library but has no public header, so we re-declare its +// signature here. MHD_Connection is opaque to this test - we only ever pass +// nullptr, mirroring how MHD itself may invoke the callback before the +// connection is fully initialised. +namespace httpserver { +void* uri_log(void* cls, const char* uri, struct MHD_Connection* con); +} // namespace httpserver + +LT_BEGIN_SUITE(uri_log_suite) + void set_up() { + } + + void tear_down() { + } +LT_END_SUITE(uri_log_suite) + +// Regression test for issue #371: under load (port scans, half-open +// connections, non-HTTP traffic on the listening port) MHD may invoke the +// URI-log callback with a null uri pointer before the request line has +// been parsed. The previous implementation assigned the raw pointer into +// std::string, which throws std::logic_error and aborts the process via +// std::terminate because the throw escapes a C callback. +LT_BEGIN_AUTO_TEST(uri_log_suite, null_uri_does_not_throw) + void* raw = nullptr; + LT_CHECK_NOTHROW(raw = httpserver::uri_log(nullptr, nullptr, nullptr)); + LT_CHECK(raw != nullptr); + + auto* mr = static_cast(raw); + LT_CHECK_EQ(mr->complete_uri, std::string("")); + delete mr; +LT_END_AUTO_TEST(null_uri_does_not_throw) + +// Sanity check that the happy path still records the URI as before. +LT_BEGIN_AUTO_TEST(uri_log_suite, valid_uri_is_stored) + const char* uri = "/some/path?with=query"; + void* raw = httpserver::uri_log(nullptr, uri, nullptr); + LT_CHECK(raw != nullptr); + + auto* mr = static_cast(raw); + LT_CHECK_EQ(mr->complete_uri, std::string(uri)); + delete mr; +LT_END_AUTO_TEST(valid_uri_is_stored) + +// Empty (but non-null) URI should be stored verbatim - this is the same +// observable state the null-uri path now produces, so route matching falls +// through to a 404 in both cases. +LT_BEGIN_AUTO_TEST(uri_log_suite, empty_uri_is_stored) + void* raw = httpserver::uri_log(nullptr, "", nullptr); + LT_CHECK(raw != nullptr); + + auto* mr = static_cast(raw); + LT_CHECK_EQ(mr->complete_uri, std::string("")); + delete mr; +LT_END_AUTO_TEST(empty_uri_is_stored) + +LT_BEGIN_AUTO_TEST_ENV() + AUTORUN_TESTS() +LT_END_AUTO_TEST_ENV() From f9c0d4eb213167f9aef3b3365003ff9d639b60cc Mon Sep 17 00:00:00 2001 From: Sebastiano Merlino Date: Tue, 7 Apr 2026 12:10:17 -0700 Subject: [PATCH 5/5] test(uri_log): satisfy cpplint build/include_subdir for httpserver.hpp cpplint flags bare "httpserver.hpp" with build/include_subdir [4]. Match the convention used by every other test file in the repo and prefix the include with "./" so cpplint considers the directory explicit. --- test/unit/uri_log_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/uri_log_test.cpp b/test/unit/uri_log_test.cpp index 6eb58191..b7972bef 100644 --- a/test/unit/uri_log_test.cpp +++ b/test/unit/uri_log_test.cpp @@ -20,7 +20,7 @@ #include -#include "httpserver.hpp" +#include "./httpserver.hpp" #include "httpserver/details/modded_request.hpp" #include "./littletest.hpp"