mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-11-13 17:00:40 +00:00
Add server_mount_fuzzer for static file serving (#2666)
Fuzz Server::handle_file_request through a mount point backed by a temporary directory that holds symlinks escaping the mount root, one of them into a sibling directory sharing its name prefix. Abort if a token stored outside the root appears in a response. The seeds are based on those of #2661. Move FuzzedStream into fuzzed_stream.h so that both server fuzzers share it, and add FuzzableServer::MarkAsRunning(), without which a server that is not listening writes no file.
This commit is contained in:
1 parent
53a7f4964e
commit
546e821c65
17 files changed
+272
-78
No files matched your search
@@ -13,14 +13,15 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
|||||||
BROTLI_DIR = /usr/local/opt/brotli
|
BROTLI_DIR = /usr/local/opt/brotli
|
||||||
# BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
# 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.
|
# Runs all the tests and also fuzz tests against seed corpus.
|
||||||
all : $(FUZZERS)
|
all : $(FUZZERS)
|
||||||
./server_fuzzer corpus/*
|
./server_fuzzer corpus/*
|
||||||
|
./server_mount_fuzzer corpus_mount/*
|
||||||
|
|
||||||
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
# 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 $@ $< -Wl,-Bstatic $(OPENSSL_SUPPORT) -Wl,-Bdynamic -ldl $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread
|
||||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||||
zip -q -r server_fuzzer_seed_corpus.zip corpus
|
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
|
sse_parser_fuzzer : sse_parser_fuzzer.cc ../../httplib.h
|
||||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
$(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:
|
clean:
|
||||||
rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip
|
rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
HEAD /mnt/a.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
GET /mnt/a.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/index.html HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
GET /mnt/index.html HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
If-None-Match: W/"bogusetag"
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/link-in.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/link-out.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/link-sibling.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/missing.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
GET /mnt/a.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
Range: bytes=0-3
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/sub/ HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/../../../../etc/passwd HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/%2e%2e/secret.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <httplib.h>
|
||||||
|
|
||||||
|
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<ssize_t>(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *buffered_data(size_t &size) const override {
|
||||||
|
size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0;
|
||||||
|
return size ? reinterpret_cast<const char *>(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<int>(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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,79 +1,4 @@
|
|||||||
#include <cstdint>
|
#include "fuzzed_stream.h"
|
||||||
|
|
||||||
#include <httplib.h>
|
|
||||||
|
|
||||||
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<ssize_t>(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *buffered_data(size_t &size) const override {
|
|
||||||
size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0;
|
|
||||||
return size ? reinterpret_cast<const char *>(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<int>(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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static FuzzableServer g_server;
|
static FuzzableServer g_server;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <utime.h>
|
||||||
|
|
||||||
|
#include "fuzzed_stream.h"
|
||||||
|
|
||||||
|
// Fixture layout, with <token> 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 <token>.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
FuzzableServer g_server;
|
||||||
|
std::string g_root;
|
||||||
|
std::string g_token;
|
||||||
|
std::vector<std::string> 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<const uint8_t *>(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", "<html>index</html>");
|
||||||
|
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", "<html>sub</html>");
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
Reference in new issue
Block a user