Skip to content

Commit a752404

Browse files
authored
Fix std::terminate when uri_log receives null uri (#371) (#372)
* 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. * 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. * 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. * 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(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.
1 parent 6c115f3 commit a752404

File tree

7 files changed

+144
-45
lines changed

7 files changed

+144
-45
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ jobs:
3838

3939
- name: Install libmicrohttpd dependency
4040
run: |
41-
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.64.tar.gz -o libmicrohttpd-0.9.64.tar.gz ;
42-
tar -xzf libmicrohttpd-0.9.64.tar.gz ;
43-
cd libmicrohttpd-0.9.64 ;
41+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ;
42+
tar -xzf libmicrohttpd-1.0.3.tar.gz ;
43+
cd libmicrohttpd-1.0.3 ;
4444
./configure --disable-examples ;
4545
make ;
4646
sudo make install ;

.github/workflows/release.yml

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,21 @@ jobs:
7878
id: cache-libmicrohttpd
7979
uses: actions/cache@v4
8080
with:
81-
path: libmicrohttpd-0.9.77
82-
key: ubuntu-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2
81+
path: libmicrohttpd-1.0.3
82+
key: ubuntu-latest-gcc-libmicrohttpd-1.0.3-pre-built-v2
8383

8484
- name: Build libmicrohttpd (if not cached)
8585
if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true'
8686
run: |
87-
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz
88-
tar -xzf libmicrohttpd-0.9.77.tar.gz
89-
cd libmicrohttpd-0.9.77
87+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz
88+
tar -xzf libmicrohttpd-1.0.3.tar.gz
89+
cd libmicrohttpd-1.0.3
9090
./configure --disable-examples
9191
make
9292
9393
- name: Install libmicrohttpd
9494
run: |
95-
cd libmicrohttpd-0.9.77
95+
cd libmicrohttpd-1.0.3
9696
sudo make install
9797
sudo ldconfig
9898
@@ -130,21 +130,21 @@ jobs:
130130
id: cache-libmicrohttpd
131131
uses: actions/cache@v4
132132
with:
133-
path: libmicrohttpd-0.9.77
134-
key: ubuntu-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2
133+
path: libmicrohttpd-1.0.3
134+
key: ubuntu-latest-gcc-libmicrohttpd-1.0.3-pre-built-v2
135135

136136
- name: Build libmicrohttpd (if not cached)
137137
if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true'
138138
run: |
139-
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz
140-
tar -xzf libmicrohttpd-0.9.77.tar.gz
141-
cd libmicrohttpd-0.9.77
139+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz
140+
tar -xzf libmicrohttpd-1.0.3.tar.gz
141+
cd libmicrohttpd-1.0.3
142142
./configure --disable-examples
143143
make
144144
145145
- name: Install libmicrohttpd
146146
run: |
147-
cd libmicrohttpd-0.9.77
147+
cd libmicrohttpd-1.0.3
148148
sudo make install
149149
sudo ldconfig
150150
@@ -181,21 +181,21 @@ jobs:
181181
id: cache-libmicrohttpd
182182
uses: actions/cache@v4
183183
with:
184-
path: libmicrohttpd-0.9.77
185-
key: macos-latest-gcc-libmicrohttpd-0.9.77-pre-built-v2
184+
path: libmicrohttpd-1.0.3
185+
key: macos-latest-gcc-libmicrohttpd-1.0.3-pre-built-v2
186186

187187
- name: Build libmicrohttpd (if not cached)
188188
if: steps.cache-libmicrohttpd.outputs.cache-hit != 'true'
189189
run: |
190-
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz
191-
tar -xzf libmicrohttpd-0.9.77.tar.gz
192-
cd libmicrohttpd-0.9.77
190+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz
191+
tar -xzf libmicrohttpd-1.0.3.tar.gz
192+
cd libmicrohttpd-1.0.3
193193
./configure --disable-examples
194194
make
195195
196196
- name: Install libmicrohttpd
197197
run: |
198-
cd libmicrohttpd-0.9.77
198+
cd libmicrohttpd-1.0.3
199199
sudo make install
200200
201201
- name: Fetch curl from cache
@@ -263,9 +263,9 @@ jobs:
263263
264264
- name: Build and install libmicrohttpd
265265
run: |
266-
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz
267-
tar -xzf libmicrohttpd-0.9.77.tar.gz
268-
cd libmicrohttpd-0.9.77
266+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz
267+
tar -xzf libmicrohttpd-1.0.3.tar.gz
268+
cd libmicrohttpd-1.0.3
269269
./configure --disable-examples --enable-poll=no
270270
make
271271
make install

.github/workflows/verify-build.yml

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -511,30 +511,30 @@ jobs:
511511
id: cache-libmicrohttpd
512512
uses: actions/cache@v4
513513
with:
514-
path: libmicrohttpd-0.9.77
515-
key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-0.9.77-pre-built-v2
514+
path: libmicrohttpd-1.0.3
515+
key: ${{ matrix.os }}-${{ matrix.c-compiler }}-libmicrohttpd-1.0.3-pre-built-v2
516516
if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.compiler-family != 'arm-cross' }}
517517

518518
- name: Build libmicrohttpd dependency (if not cached)
519519
run: |
520-
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz ;
521-
tar -xzf libmicrohttpd-0.9.77.tar.gz ;
522-
cd libmicrohttpd-0.9.77 ;
520+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ;
521+
tar -xzf libmicrohttpd-1.0.3.tar.gz ;
522+
cd libmicrohttpd-1.0.3 ;
523523
./configure --disable-examples ;
524524
make ;
525525
if: ${{ matrix.os-type != 'windows' && matrix.build-type != 'no-dauth' && matrix.compiler-family != 'arm-cross' && steps.cache-libmicrohttpd.outputs.cache-hit != 'true' }}
526526

527527
- name: Build libmicrohttpd without digest auth (no-dauth test)
528528
run: |
529-
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz ;
530-
tar -xzf libmicrohttpd-0.9.77.tar.gz ;
531-
cd libmicrohttpd-0.9.77 ;
529+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz ;
530+
tar -xzf libmicrohttpd-1.0.3.tar.gz ;
531+
cd libmicrohttpd-1.0.3 ;
532532
./configure --disable-examples --disable-dauth ;
533533
make ;
534534
if: ${{ matrix.build-type == 'no-dauth' }}
535535

536536
- name: Install libmicrohttpd
537-
run: cd libmicrohttpd-0.9.77 ; sudo make install ;
537+
run: cd libmicrohttpd-1.0.3 ; sudo make install ;
538538
if: ${{ matrix.os-type != 'windows' && matrix.compiler-family != 'arm-cross' }}
539539

540540
- name: Verify digest auth is disabled (no-dauth test)
@@ -550,9 +550,9 @@ jobs:
550550
- name: Build and install libmicrohttpd (Windows)
551551
if: ${{ matrix.os-type == 'windows' }}
552552
run: |
553-
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz
554-
tar -xzf libmicrohttpd-0.9.77.tar.gz
555-
cd libmicrohttpd-0.9.77
553+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz
554+
tar -xzf libmicrohttpd-1.0.3.tar.gz
555+
cd libmicrohttpd-1.0.3
556556
./configure --disable-examples --enable-poll=no
557557
make
558558
make install
@@ -561,16 +561,16 @@ jobs:
561561
id: cache-libmicrohttpd-arm
562562
uses: actions/cache@v4
563563
with:
564-
path: libmicrohttpd-0.9.77-${{ matrix.build-type }}
565-
key: ${{ matrix.os }}-${{ matrix.build-type }}-libmicrohttpd-0.9.77-cross-compiled
564+
path: libmicrohttpd-1.0.3-${{ matrix.build-type }}
565+
key: ${{ matrix.os }}-${{ matrix.build-type }}-libmicrohttpd-1.0.3-cross-compiled
566566
if: ${{ matrix.compiler-family == 'arm-cross' }}
567567

568568
- name: Cross-compile libmicrohttpd for ARM
569569
run: |
570-
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-0.9.77.tar.gz -o libmicrohttpd-0.9.77.tar.gz
571-
tar -xzf libmicrohttpd-0.9.77.tar.gz
572-
mv libmicrohttpd-0.9.77 libmicrohttpd-0.9.77-${{ matrix.build-type }}
573-
cd libmicrohttpd-0.9.77-${{ matrix.build-type }}
570+
curl https://s3.amazonaws.com/libhttpserver/libmicrohttpd_releases/libmicrohttpd-1.0.3.tar.gz -o libmicrohttpd-1.0.3.tar.gz
571+
tar -xzf libmicrohttpd-1.0.3.tar.gz
572+
mv libmicrohttpd-1.0.3 libmicrohttpd-1.0.3-${{ matrix.build-type }}
573+
cd libmicrohttpd-1.0.3-${{ matrix.build-type }}
574574
mkdir -p ${{ github.workspace }}/arm-sysroot
575575
if [ "${{ matrix.build-type }}" = "arm32" ]; then
576576
./configure --host=arm-linux-gnueabihf --prefix=${{ github.workspace }}/arm-sysroot --disable-examples --disable-doc
@@ -583,7 +583,7 @@ jobs:
583583

584584
- name: Install cross-compiled libmicrohttpd from cache
585585
run: |
586-
cd libmicrohttpd-0.9.77-${{ matrix.build-type }}
586+
cd libmicrohttpd-1.0.3-${{ matrix.build-type }}
587587
mkdir -p ${{ github.workspace }}/arm-sysroot
588588
make install
589589
if: ${{ matrix.compiler-family == 'arm-cross' && steps.cache-libmicrohttpd-arm.outputs.cache-hit == 'true' }}

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Version 0.20.0
1717
Fixed auth skip path bypass via path traversal (e.g. /public/../protected).
1818
Fixed use of free() instead of MHD_free() for digest auth username.
1919
Fixed unchecked write error during file upload.
20+
Fixed std::terminate when MHD invokes the URI log callback with a
21+
null uri pointer (e.g. port scans, half-open connections, or
22+
non-HTTP traffic). Resolves issue #371.
2023

2124
Version 0.19.0 - 2023-06-15
2225

src/webserver.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,12 @@ void* uri_log(void* cls, const char* uri, struct MHD_Connection *con) {
626626
std::ignore = con;
627627

628628
auto mr = std::make_unique<details::modded_request>();
629-
mr->complete_uri = uri;
629+
// MHD may invoke this callback with a null uri before the request line
630+
// has been parsed (e.g. port scans, half-open connections, or non-HTTP
631+
// traffic on the listening port). Treat that as an empty URI so the
632+
// std::string assignment does not throw std::logic_error and abort the
633+
// process via std::terminate. See issue #371.
634+
mr->complete_uri = (uri != nullptr) ? uri : "";
630635
return reinterpret_cast<void*>(mr.release());
631636
}
632637

test/Makefile.am

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ LDADD += -lcurl
2626

2727
AM_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/httpserver/
2828
METASOURCES = AUTO
29-
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
29+
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
3030

3131
MOSTLYCLEANFILES = *.gcda *.gcno *.gcov
3232

@@ -44,6 +44,11 @@ nodelay_SOURCES = integ/nodelay.cpp
4444
http_resource_SOURCES = unit/http_resource_test.cpp
4545
http_response_SOURCES = unit/http_response_test.cpp
4646
create_webserver_SOURCES = unit/create_webserver_test.cpp
47+
uri_log_SOURCES = unit/uri_log_test.cpp
48+
# uri_log_test directly references libmicrohttpd via ~modded_request(), so
49+
# it needs an explicit -lmicrohttpd in its link line on top of the default
50+
# LDADD (modern ld enforces --no-copy-dt-needed-entries).
51+
uri_log_LDADD = $(LDADD) -lmicrohttpd
4752

4853
noinst_HEADERS = littletest.hpp
4954
AM_CXXFLAGS += -Wall -fPIC -Wno-overloaded-virtual

test/unit/uri_log_test.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
This file is part of libhttpserver
3+
Copyright (C) 2011-2019 Sebastiano Merlino
4+
5+
This library is free software; you can redistribute it and/or
6+
modify it under the terms of the GNU Lesser General Public
7+
License as published by the Free Software Foundation; either
8+
version 2.1 of the License, or (at your option) any later version.
9+
10+
This library is distributed in the hope that it will be useful,
11+
but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
Lesser General Public License for more details.
14+
15+
You should have received a copy of the GNU Lesser General Public
16+
License along with this library; if not, write to the Free Software
17+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
18+
USA
19+
*/
20+
21+
#include <string>
22+
23+
#include "./httpserver.hpp"
24+
#include "httpserver/details/modded_request.hpp"
25+
26+
#include "./littletest.hpp"
27+
28+
// uri_log is the MHD URI-log callback defined in src/webserver.cpp. It is
29+
// exported from the library but has no public header, so we re-declare its
30+
// signature here. MHD_Connection is opaque to this test - we only ever pass
31+
// nullptr, mirroring how MHD itself may invoke the callback before the
32+
// connection is fully initialised.
33+
namespace httpserver {
34+
void* uri_log(void* cls, const char* uri, struct MHD_Connection* con);
35+
} // namespace httpserver
36+
37+
LT_BEGIN_SUITE(uri_log_suite)
38+
void set_up() {
39+
}
40+
41+
void tear_down() {
42+
}
43+
LT_END_SUITE(uri_log_suite)
44+
45+
// Regression test for issue #371: under load (port scans, half-open
46+
// connections, non-HTTP traffic on the listening port) MHD may invoke the
47+
// URI-log callback with a null uri pointer before the request line has
48+
// been parsed. The previous implementation assigned the raw pointer into
49+
// std::string, which throws std::logic_error and aborts the process via
50+
// std::terminate because the throw escapes a C callback.
51+
LT_BEGIN_AUTO_TEST(uri_log_suite, null_uri_does_not_throw)
52+
void* raw = nullptr;
53+
LT_CHECK_NOTHROW(raw = httpserver::uri_log(nullptr, nullptr, nullptr));
54+
LT_CHECK(raw != nullptr);
55+
56+
auto* mr = static_cast<httpserver::details::modded_request*>(raw);
57+
LT_CHECK_EQ(mr->complete_uri, std::string(""));
58+
delete mr;
59+
LT_END_AUTO_TEST(null_uri_does_not_throw)
60+
61+
// Sanity check that the happy path still records the URI as before.
62+
LT_BEGIN_AUTO_TEST(uri_log_suite, valid_uri_is_stored)
63+
const char* uri = "/some/path?with=query";
64+
void* raw = httpserver::uri_log(nullptr, uri, nullptr);
65+
LT_CHECK(raw != nullptr);
66+
67+
auto* mr = static_cast<httpserver::details::modded_request*>(raw);
68+
LT_CHECK_EQ(mr->complete_uri, std::string(uri));
69+
delete mr;
70+
LT_END_AUTO_TEST(valid_uri_is_stored)
71+
72+
// Empty (but non-null) URI should be stored verbatim - this is the same
73+
// observable state the null-uri path now produces, so route matching falls
74+
// through to a 404 in both cases.
75+
LT_BEGIN_AUTO_TEST(uri_log_suite, empty_uri_is_stored)
76+
void* raw = httpserver::uri_log(nullptr, "", nullptr);
77+
LT_CHECK(raw != nullptr);
78+
79+
auto* mr = static_cast<httpserver::details::modded_request*>(raw);
80+
LT_CHECK_EQ(mr->complete_uri, std::string(""));
81+
delete mr;
82+
LT_END_AUTO_TEST(empty_uri_is_stored)
83+
84+
LT_BEGIN_AUTO_TEST_ENV()
85+
AUTORUN_TESTS()
86+
LT_END_AUTO_TEST_ENV()

0 commit comments

Comments
 (0)