diff --git a/test/fuzzing/Makefile b/test/fuzzing/Makefile index f352385d..716a8aac 100644 --- a/test/fuzzing/Makefile +++ b/test/fuzzing/Makefile @@ -13,14 +13,15 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz BROTLI_DIR = /usr/local/opt/brotli # BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec -FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer multipart_parser_fuzzer websocket_fuzzer sse_parser_fuzzer +FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer multipart_parser_fuzzer websocket_fuzzer sse_parser_fuzzer server_mount_fuzzer # Runs all the tests and also fuzz tests against seed corpus. all : $(FUZZERS) ./server_fuzzer corpus/* + ./server_mount_fuzzer corpus_mount/* # Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use. -server_fuzzer : server_fuzzer.cc ../../httplib.h +server_fuzzer : server_fuzzer.cc fuzzed_stream.h ../../httplib.h # $(CXX) $(CXXFLAGS) -o $@ $< -Wl,-Bstatic $(OPENSSL_SUPPORT) -Wl,-Bdynamic -ldl $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread $(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl zip -q -r server_fuzzer_seed_corpus.zip corpus @@ -44,5 +45,9 @@ websocket_fuzzer : websocket_fuzzer.cc ../../httplib.h sse_parser_fuzzer : sse_parser_fuzzer.cc ../../httplib.h $(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl +server_mount_fuzzer : server_mount_fuzzer.cc fuzzed_stream.h ../../httplib.h + $(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl + zip -q -r server_mount_fuzzer_seed_corpus.zip corpus_mount + clean: rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip diff --git a/test/fuzzing/corpus_mount/mount-head b/test/fuzzing/corpus_mount/mount-head new file mode 100644 index 00000000..47e8efd8 --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-head @@ -0,0 +1,3 @@ +HEAD /mnt/a.txt HTTP/1.1 +Host: x + diff --git a/test/fuzzing/corpus_mount/mount-ims b/test/fuzzing/corpus_mount/mount-ims new file mode 100644 index 00000000..5ee8298c --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-ims @@ -0,0 +1,4 @@ +GET /mnt/a.txt HTTP/1.1 +Host: x +If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT + diff --git a/test/fuzzing/corpus_mount/mount-index b/test/fuzzing/corpus_mount/mount-index new file mode 100644 index 00000000..8387587c --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-index @@ -0,0 +1,3 @@ +GET /mnt/index.html HTTP/1.1 +Host: x + diff --git a/test/fuzzing/corpus_mount/mount-inm b/test/fuzzing/corpus_mount/mount-inm new file mode 100644 index 00000000..a8280d83 --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-inm @@ -0,0 +1,4 @@ +GET /mnt/index.html HTTP/1.1 +Host: x +If-None-Match: W/"bogusetag" + diff --git a/test/fuzzing/corpus_mount/mount-linkin b/test/fuzzing/corpus_mount/mount-linkin new file mode 100644 index 00000000..78239ca5 --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-linkin @@ -0,0 +1,3 @@ +GET /mnt/link-in.txt HTTP/1.1 +Host: x + diff --git a/test/fuzzing/corpus_mount/mount-linkout b/test/fuzzing/corpus_mount/mount-linkout new file mode 100644 index 00000000..170a461c --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-linkout @@ -0,0 +1,3 @@ +GET /mnt/link-out.txt HTTP/1.1 +Host: x + diff --git a/test/fuzzing/corpus_mount/mount-linksibling b/test/fuzzing/corpus_mount/mount-linksibling new file mode 100644 index 00000000..ec8e6bd1 --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-linksibling @@ -0,0 +1,3 @@ +GET /mnt/link-sibling.txt HTTP/1.1 +Host: x + diff --git a/test/fuzzing/corpus_mount/mount-missing b/test/fuzzing/corpus_mount/mount-missing new file mode 100644 index 00000000..ba2bf9e7 --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-missing @@ -0,0 +1,3 @@ +GET /mnt/missing.txt HTTP/1.1 +Host: x + diff --git a/test/fuzzing/corpus_mount/mount-range b/test/fuzzing/corpus_mount/mount-range new file mode 100644 index 00000000..f8a7298c --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-range @@ -0,0 +1,4 @@ +GET /mnt/a.txt HTTP/1.1 +Host: x +Range: bytes=0-3 + diff --git a/test/fuzzing/corpus_mount/mount-subdir b/test/fuzzing/corpus_mount/mount-subdir new file mode 100644 index 00000000..585099b3 --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-subdir @@ -0,0 +1,3 @@ +GET /mnt/sub/ HTTP/1.1 +Host: x + diff --git a/test/fuzzing/corpus_mount/mount-traversal b/test/fuzzing/corpus_mount/mount-traversal new file mode 100644 index 00000000..8ebdfb02 --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-traversal @@ -0,0 +1,3 @@ +GET /mnt/../../../../etc/passwd HTTP/1.1 +Host: x + diff --git a/test/fuzzing/corpus_mount/mount-traversal-enc b/test/fuzzing/corpus_mount/mount-traversal-enc new file mode 100644 index 00000000..5ac1d3ba --- /dev/null +++ b/test/fuzzing/corpus_mount/mount-traversal-enc @@ -0,0 +1,3 @@ +GET /mnt/%2e%2e/secret.txt HTTP/1.1 +Host: x + diff --git a/test/fuzzing/fuzzed_stream.h b/test/fuzzing/fuzzed_stream.h new file mode 100644 index 00000000..d3b53561 --- /dev/null +++ b/test/fuzzing/fuzzed_stream.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include + +class FuzzedStream : public httplib::Stream { +public: + FuzzedStream(const uint8_t *data, size_t size) + : data_(data), size_(size), read_pos_(0) {} + + ssize_t read(char *ptr, size_t size) override { + if (size + read_pos_ > size_) { size = size_ - read_pos_; } + memcpy(ptr, data_ + read_pos_, size); + read_pos_ += size; + // Like SocketStream, pull in more than was asked for, which the line + // reader then takes through buffered_data(), so both of its paths run. + buffered_end_ = + (std::max)(buffered_end_, (std::min)(size_, read_pos_ + 4096)); + return static_cast(size); + } + + const char *buffered_data(size_t &size) const override { + size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0; + return size ? reinterpret_cast(data_ + read_pos_) : nullptr; + } + + void consume_buffered(size_t size) override { read_pos_ += size; } + + ssize_t write(const char *ptr, size_t size) override { + response_.append(ptr, size); + return static_cast(size); + } + + ssize_t write(const char *ptr) { return write(ptr, strlen(ptr)); } + + ssize_t write(const std::string &s) { return write(s.data(), s.size()); } + + bool is_readable() const override { return true; } + + bool wait_readable() const override { return true; } + + bool wait_writable() const override { return true; } + + void get_remote_ip_and_port(std::string &ip, int &port) const override { + ip = "127.0.0.1"; + port = 8080; + } + + void get_local_ip_and_port(std::string &ip, int &port) const override { + ip = "127.0.0.1"; + port = 8080; + } + + socket_t socket() const override { return 0; } + + time_t duration() const override { return 0; }; + + const std::string &response() const { return response_; } + +private: + const uint8_t *data_; + size_t size_; + size_t read_pos_; + size_t buffered_end_ = 0; + std::string response_; +}; + +class FuzzableServer : public httplib::Server { +public: + // A server that is not listening counts as shutting down, and then writes + // no file. The socket is never used. + void MarkAsRunning() { svr_sock_ = 0; } + + void ProcessFuzzedRequest(FuzzedStream &stream) { + bool connection_close = false; + process_request(stream, + /*remote_addr=*/"", + /*remote_port =*/0, + /*local_addr=*/"", + /*local_port =*/0, + /*last_connection=*/false, connection_close, nullptr); + } +}; diff --git a/test/fuzzing/server_fuzzer.cc b/test/fuzzing/server_fuzzer.cc index e09ba746..f058ee11 100644 --- a/test/fuzzing/server_fuzzer.cc +++ b/test/fuzzing/server_fuzzer.cc @@ -1,79 +1,4 @@ -#include - -#include - -class FuzzedStream : public httplib::Stream { -public: - FuzzedStream(const uint8_t *data, size_t size) - : data_(data), size_(size), read_pos_(0) {} - - ssize_t read(char *ptr, size_t size) override { - if (size + read_pos_ > size_) { size = size_ - read_pos_; } - memcpy(ptr, data_ + read_pos_, size); - read_pos_ += size; - // Like SocketStream, pull in more than was asked for, which the line - // reader then takes through buffered_data(), so both of its paths run. - buffered_end_ = - (std::max)(buffered_end_, (std::min)(size_, read_pos_ + 4096)); - return static_cast(size); - } - - const char *buffered_data(size_t &size) const override { - size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0; - return size ? reinterpret_cast(data_ + read_pos_) : nullptr; - } - - void consume_buffered(size_t size) override { read_pos_ += size; } - - ssize_t write(const char *ptr, size_t size) override { - response_.append(ptr, size); - return static_cast(size); - } - - ssize_t write(const char *ptr) { return write(ptr, strlen(ptr)); } - - ssize_t write(const std::string &s) { return write(s.data(), s.size()); } - - bool is_readable() const override { return true; } - - bool wait_readable() const override { return true; } - - bool wait_writable() const override { return true; } - - void get_remote_ip_and_port(std::string &ip, int &port) const override { - ip = "127.0.0.1"; - port = 8080; - } - - void get_local_ip_and_port(std::string &ip, int &port) const override { - ip = "127.0.0.1"; - port = 8080; - } - - socket_t socket() const override { return 0; } - - time_t duration() const override { return 0; }; - -private: - const uint8_t *data_; - size_t size_; - size_t read_pos_; - size_t buffered_end_ = 0; - std::string response_; -}; - -class FuzzableServer : public httplib::Server { -public: - void ProcessFuzzedRequest(FuzzedStream &stream) { - bool connection_close = false; - process_request(stream, - /*remote_addr=*/"", - /*remote_port =*/0, - /*local_addr=*/"", - /*local_port =*/0, - /*last_connection=*/false, connection_close, nullptr); - } -}; +#include "fuzzed_stream.h" static FuzzableServer g_server; diff --git a/test/fuzzing/server_mount_fuzzer.cc b/test/fuzzing/server_mount_fuzzer.cc new file mode 100644 index 00000000..26c80baf --- /dev/null +++ b/test/fuzzing/server_mount_fuzzer.cc @@ -0,0 +1,117 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "fuzzed_stream.h" + +// Fixture layout, with unknown to the fuzzer: +// root/mnt/{index.html,a.txt,data.bin,sub/index.html} served at /mnt +// root/mnt/link-in.txt -> a.txt (stays inside, may be served) +// root/mnt/link-out.txt -> ../outside.txt (escapes, must be refused) +// root/mnt/link-sibling.txt -> ../mnt_secret/secret.txt (shares the "mnt" +// prefix, must be refused) +// The response must never contain . + +namespace { + +FuzzableServer g_server; +std::string g_root; +std::string g_token; +std::vector g_paths; // In creation order + +void check(bool ok, const char *what) { + if (!ok) { + fprintf(stderr, "fixture setup failed: %s\n", what); + abort(); + } +} + +void make_dir(const std::string &path) { + check(mkdir(path.c_str(), 0755) == 0, path.c_str()); + g_paths.push_back(path); +} + +void make_file(const std::string &path, const std::string &content) { + { + std::ofstream out(path, std::ios::binary); + out << content; + check(out.good(), path.c_str()); + } + // Fixed, so that conditional requests behave the same in every run + utimbuf times = {1700000000, 1700000000}; + check(utime(path.c_str(), ×) == 0, path.c_str()); + g_paths.push_back(path); +} + +void make_link(const std::string &target, const std::string &path) { + check(symlink(target.c_str(), path.c_str()) == 0, path.c_str()); + g_paths.push_back(path); +} + +std::string serve(const uint8_t *data, size_t size) { + FuzzedStream stream{data, size}; + g_server.ProcessFuzzedRequest(stream); + return stream.response(); +} + +void check_served(const char *path) { + auto req = std::string("GET ") + path + " HTTP/1.1\r\nHost: x\r\n\r\n"; + auto res = serve(reinterpret_cast(req.data()), req.size()); + // Without this, a fuzzer that serves nothing would pass its check forever + check(res.find("abcdef") != std::string::npos, path); +} + +void remove_fixture() { + for (auto it = g_paths.rbegin(); it != g_paths.rend(); ++it) { + if (unlink(it->c_str()) != 0) { rmdir(it->c_str()); } + } + rmdir(g_root.c_str()); +} + +} // namespace + +extern "C" int LLVMFuzzerInitialize(int * /*argc*/, char *** /*argv*/) { + std::random_device rd; + char token[33]; + snprintf(token, sizeof(token), "%08x%08x%08x%08x", rd(), rd(), rd(), rd()); + g_token = token; + + auto tmpdir = getenv("TMPDIR"); + auto tmpl = std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") + + "/httplib-mount-XXXXXX"; + check(mkdtemp(&tmpl[0]) != nullptr, tmpl.c_str()); + g_root = tmpl; + atexit(remove_fixture); + + auto mnt = g_root + "/mnt"; + make_dir(mnt); + make_dir(mnt + "/sub"); + make_dir(g_root + "/mnt_secret"); + make_file(mnt + "/index.html", "index"); + make_file(mnt + "/a.txt", "abcdef"); + make_file(mnt + "/data.bin", std::string("\x01\x02\x03\x04\x05\x06", 6)); + make_file(mnt + "/sub/index.html", "sub"); + make_file(g_root + "/outside.txt", g_token); + make_file(g_root + "/mnt_secret/secret.txt", g_token); + make_link("a.txt", mnt + "/link-in.txt"); + make_link("../outside.txt", mnt + "/link-out.txt"); + make_link("../mnt_secret/secret.txt", mnt + "/link-sibling.txt"); + + g_server.MarkAsRunning(); + check(g_server.set_mount_point("/mnt", mnt), "set_mount_point"); + check_served("/mnt/a.txt"); + check_served("/mnt/link-in.txt"); + return 0; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (serve(data, size).find(g_token) != std::string::npos) { abort(); } + return 0; +} diff --git a/test/fuzzing/server_mount_fuzzer.dict b/test/fuzzing/server_mount_fuzzer.dict new file mode 100644 index 00000000..48d48ff7 --- /dev/null +++ b/test/fuzzing/server_mount_fuzzer.dict @@ -0,0 +1,24 @@ +"GET" +"HEAD" +"HTTP/1.1" + +"/mnt/" +"/mnt/index.html" +"/mnt/a.txt" +"/mnt/data.bin" +"/mnt/sub/" +"/mnt/link-in.txt" +"/mnt/link-out.txt" +"/mnt/link-sibling.txt" +".." +"%2e%2e" +"%2f" +"//" + +"Range: bytes=0-" +"Range: bytes=0-3,5-" +"If-None-Match: " +"W/" +"If-Modified-Since: " +"If-Range: " +"Accept-Encoding: gzip"