Compare commits

..
4 Commits
Author SHA1 Message Date
yhirose 4a1700cb5e Use fill_random_bytes in random_string
Drop the thread_local std::mt19937 so multipart boundaries and the Digest
cnonce come from the same non-deterministic source as the WebSocket
masking key. This removes the last mt19937 from httplib.h.
2026-10-14 16:13:28 -04:00
metsw24-max c03af246fc Use a strong entropy source for the WebSocket masking key (#2629)
Draw the masking key and Sec-WebSocket-Key directly from std::random_device instead of a std::mt19937 seeded once with 32 bits, as RFC 6455 Section 5.3 requires an unpredictable masking key. Taking the bytes one at a time also fixes an all-zero key on 64-bit big-endian builds, where the 4-byte memcpy from the 64-bit result_type copied the zero high half.
2026-10-14 16:09:36 -04:00
yhirose 2c918f27ee Redirect a directory request with a relative Location (#2630)
* Redirect a directory request with a relative Location

The trailing-slash redirect used the request path as-is, so a path
starting with "//" produced a Location that clients read as a
network-path reference to another host. Name only the last segment,
percent-encoded and prefixed with "./", so the target always resolves
against the request URL, whatever the mount point or any prefix a
proxy put in front of it.

* Shorten the relative redirect comment
2026-10-14 16:00:03 -04:00
mervindejong 924b5cecf0 Fix redirect for mounted folders (#2628)
A directory request without a trailing slash was redirected to the path
relative to the mount point, so /mount/dir under a mount at /mount sent
Location: //dir/ instead of /mount/dir/. Redirect to the request path
with the slash appended.
2026-10-14 15:35:27 -04:00
2 changed files with 39 additions and 23 deletions

No files matched your search

+25 -23
View File
@@ -5604,6 +5604,18 @@ inline bool is_websocket_upgrade(const Request &req) {
return true;
}
// Unpredictable bytes for masking keys (RFC 6455 Section 5.3) and nonces
inline void fill_random_bytes(unsigned char *out, size_t n) {
thread_local std::random_device rd;
size_t filled = 0;
while (filled < n) {
auto v = rd();
for (size_t b = 0; b < sizeof(v) && filled < n; ++b, ++filled) {
out[filled] = static_cast<unsigned char>((v >> (b * 8)) & 0xFFu);
}
}
}
inline bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
const char *data, size_t len, bool fin,
bool mask) {
@@ -5639,10 +5651,8 @@ inline bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
if (mask) {
// Generate random mask key
thread_local std::mt19937 rng(std::random_device{}());
uint8_t mask_key[4];
auto r = rng();
std::memcpy(mask_key, &r, 4);
fill_random_bytes(mask_key, sizeof(mask_key));
if (strm.write(reinterpret_cast<char *>(mask_key), 4) < 0) { return false; }
// Write masked payload in chunks
@@ -9665,19 +9675,10 @@ inline std::string random_string(size_t length) {
constexpr const char data[] =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
thread_local auto engine([]() {
// std::random_device might actually be deterministic on some
// platforms, but due to lack of support in the c++ standard library,
// doing better requires either some ugly hacks or breaking portability.
std::random_device seed_gen;
// Request 128 bits of entropy for initialization
std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()};
return std::mt19937(seed_sequence);
}());
std::string result;
for (size_t i = 0; i < length; i++) {
result += data[engine() % (sizeof(data) - 1)];
std::string result(length, '\0');
fill_random_bytes(reinterpret_cast<unsigned char *>(&result[0]), length);
for (auto &c : result) {
c = data[static_cast<unsigned char>(c) % (sizeof(data) - 1)];
}
return result;
}
@@ -10364,13 +10365,10 @@ inline bool is_host(const std::string &s) {
inline bool perform_websocket_handshake(Stream &strm, Request &req,
WebSocketUpgradeResponse &upgrade) {
// Generate random Sec-WebSocket-Key
thread_local std::mt19937 rng(std::random_device{}());
// Generate a random 16-byte Sec-WebSocket-Key (RFC 6455 Section 4.1)
std::string key_bytes(16, '\0');
for (size_t i = 0; i < 16; i += 4) {
auto r = rng();
std::memcpy(&key_bytes[i], &r, (std::min)(size_t(4), size_t(16 - i)));
}
fill_random_bytes(reinterpret_cast<unsigned char *>(&key_bytes[0]),
key_bytes.size());
auto client_key = base64_encode(key_bytes);
req.headers.erase("Upgrade");
@@ -13869,7 +13867,11 @@ inline bool Server::handle_file_request(Request &req, Response &res) {
detail::FileStat stat(path);
if (stat.is_dir()) {
res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301);
// Relative, so "//dir" cannot name another host; "./" keeps a ':'
// in the name from reading as a scheme.
auto name = req.path.substr(req.path.rfind('/') + 1);
res.set_redirect("./" + encode_path_component(name) + "/",
StatusCode::MovedPermanently_301);
return true;
}
+14
View File
@@ -6660,6 +6660,20 @@ TEST_F(ServerTest, GetMethodOutOfBaseDirMountWithBackslash) {
EXPECT_EQ(StatusCode::NotFound_404, res->status);
}
TEST_F(ServerTest, GetMethodDirMountWithoutTrailingSlash) {
auto res = cli_.Get("/mount/dir");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::MovedPermanently_301, res->status);
EXPECT_EQ("./dir/", res->get_header_value("Location"));
}
TEST_F(ServerTest, GetMethodDirWithLeadingDoubleSlash) {
auto res = cli_.Get("//dir");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::MovedPermanently_301, res->status);
EXPECT_EQ("./dir/", res->get_header_value("Location"));
}
TEST_F(ServerTest, PostMethod303) {
auto res = cli_.Post("/1", "body", "text/plain");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());