File CVE-2025-53628-53629.patch of Package cpp-httplib.19233
From 17ba303889b8d4d719be3879a70639ab653efb99 Mon Sep 17 00:00:00 2001
From: yhirose <yhirose@users.noreply.github.com>
Date: Wed, 9 Jul 2025 07:10:09 -0400
Subject: [PATCH] Merge commit from fork
* Fix HTTP Header Smuggling due to insecure trailers merge
* Improve performance
---
httplib.h | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++--
test/test.cc | 69 +++++++++++++++++++++++++++++++++++++--
2 files changed, 156 insertions(+), 5 deletions(-)
Index: cpp-httplib-0.20.1/httplib.h
===================================================================
--- cpp-httplib-0.20.1.orig/httplib.h
+++ cpp-httplib-0.20.1/httplib.h
@@ -410,6 +410,10 @@ struct hash {
}
};
+template <typename T>
+using unordered_set = std::unordered_set<T, detail::case_ignore::hash,
+ detail::case_ignore::equal_to>;
+
} // namespace case_ignore
// This is based on
@@ -637,6 +641,7 @@ struct Request {
std::string path;
Params params;
Headers headers;
+ Headers trailers;
std::string body;
std::string remote_addr;
@@ -669,6 +674,10 @@ struct Request {
size_t get_header_value_count(const std::string &key) const;
void set_header(const std::string &key, const std::string &val);
+ bool has_trailer(const std::string &key) const;
+ std::string get_trailer_value(const std::string &key, size_t id = 0) const;
+ size_t get_trailer_value_count(const std::string &key) const;
+
bool has_param(const std::string &key) const;
std::string get_param_value(const std::string &key, size_t id = 0) const;
size_t get_param_value_count(const std::string &key) const;
@@ -694,6 +703,7 @@ struct Response {
int status = -1;
std::string reason;
Headers headers;
+ Headers trailers;
std::string body;
std::string location; // Redirect location
@@ -705,6 +715,10 @@ struct Response {
size_t get_header_value_count(const std::string &key) const;
void set_header(const std::string &key, const std::string &val);
+ bool has_trailer(const std::string &key) const;
+ std::string get_trailer_value(const std::string &key, size_t id = 0) const;
+ size_t get_trailer_value_count(const std::string &key) const;
+
void set_redirect(const std::string &url, int status = StatusCode::Found_302);
void set_content(const char *s, size_t n, const std::string &content_type);
void set_content(const std::string &s, const std::string &content_type);
@@ -4476,6 +4490,42 @@ inline bool read_content_chunked(Stream
// chunked transfer coding data without the final CRLF.
if (!line_reader.getline()) { return true; }
+ // RFC 7230 Section 4.1.2 - Headers prohibited in trailers
+ thread_local case_ignore::unordered_set<std::string> prohibited_trailers = {
+ // Message framing
+ "transfer-encoding", "content-length",
+
+ // Routing
+ "host",
+
+ // Authentication
+ "authorization", "www-authenticate", "proxy-authenticate",
+ "proxy-authorization", "cookie", "set-cookie",
+
+ // Request modifiers
+ "cache-control", "expect", "max-forwards", "pragma", "range", "te",
+
+ // Response control
+ "age", "expires", "date", "location", "retry-after", "vary", "warning",
+
+ // Payload processing
+ "content-encoding", "content-type", "content-range", "trailer"};
+
+ // Parse declared trailer headers once for performance
+ case_ignore::unordered_set<std::string> declared_trailers;
+ if (has_header(x.headers, "Trailer")) {
+ auto trailer_header = get_header_value(x.headers, "Trailer", "", 0);
+ auto len = std::strlen(trailer_header);
+
+ split(trailer_header, trailer_header + len, ',',
+ [&](const char *b, const char *e) {
+ std::string key(b, e);
+ if (prohibited_trailers.find(key) == prohibited_trailers.end()) {
+ declared_trailers.insert(key);
+ }
+ });
+ }
+
size_t trailer_header_count = 0;
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; }
@@ -4489,11 +4539,12 @@ inline bool read_content_chunked(Stream
parse_header(line_reader.ptr(), end,
[&](const std::string &key, const std::string &val) {
- x.headers.emplace(key, val);
+ if (declared_trailers.find(key) != declared_trailers.end()) {
+ x.trailers.emplace(key, val);
+ trailer_header_count++;
+ }
});
- trailer_header_count++;
-
if (!line_reader.getline()) { return false; }
}
Index: cpp-httplib-0.20.1/httplib.h
===================================================================
--- cpp-httplib-0.20.1.orig/httplib.h
+++ cpp-httplib-0.20.1/httplib.h
@@ -5946,6 +5946,24 @@ inline size_t Request::get_header_value_
return static_cast<size_t>(std::distance(r.first, r.second));
}
+inline bool Request::has_trailer(const std::string &key) const {
+ return trailers.find(key) != trailers.end();
+}
+
+inline std::string Request::get_trailer_value(const std::string &key,
+ size_t id) const {
+ auto rng = trailers.equal_range(key);
+ auto it = rng.first;
+ std::advance(it, static_cast<ssize_t>(id));
+ if (it != rng.second) { return it->second; }
+ return std::string();
+}
+
+inline size_t Request::get_trailer_value_count(const std::string &key) const {
+ auto r = trailers.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
+}
+
inline void Request::set_header(const std::string &key,
const std::string &val) {
if (detail::fields::is_field_name(key) &&
@@ -6020,6 +6038,23 @@ inline void Response::set_header(const s
headers.emplace(key, val);
}
}
+inline bool Response::has_trailer(const std::string &key) const {
+ return trailers.find(key) != trailers.end();
+}
+
+inline std::string Response::get_trailer_value(const std::string &key,
+ size_t id) const {
+ auto rng = trailers.equal_range(key);
+ auto it = rng.first;
+ std::advance(it, static_cast<ssize_t>(id));
+ if (it != rng.second) { return it->second; }
+ return std::string();
+}
+
+inline size_t Response::get_trailer_value_count(const std::string &key) const {
+ auto r = trailers.equal_range(key);
+ return static_cast<size_t>(std::distance(r.first, r.second));
+}
inline void Response::set_redirect(const std::string &url, int stat) {
if (detail::fields::is_field_value(url)) {
Index: cpp-httplib-0.20.1/test/test.cc
===================================================================
--- cpp-httplib-0.20.1.orig/test/test.cc
+++ cpp-httplib-0.20.1/test/test.cc
@@ -4213,8 +4213,22 @@ TEST_F(ServerTest, GetStreamedChunkedWit
ASSERT_TRUE(res);
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(std::string("123456789"), res->body);
- EXPECT_EQ(std::string("DummyVal1"), res->get_header_value("Dummy1"));
- EXPECT_EQ(std::string("DummyVal2"), res->get_header_value("Dummy2"));
+
+ EXPECT_TRUE(res->has_header("Trailer"));
+ EXPECT_EQ(1U, res->get_header_value_count("Trailer"));
+ EXPECT_EQ(std::string("Dummy1, Dummy2"), res->get_header_value("Trailer"));
+
+ // Trailers are now stored separately from headers (security fix)
+ EXPECT_EQ(2U, res->trailers.size());
+ EXPECT_TRUE(res->has_trailer("Dummy1"));
+ EXPECT_TRUE(res->has_trailer("Dummy2"));
+ EXPECT_FALSE(res->has_trailer("Dummy3"));
+ EXPECT_EQ(std::string("DummyVal1"), res->get_trailer_value("Dummy1"));
+ EXPECT_EQ(std::string("DummyVal2"), res->get_trailer_value("Dummy2"));
+
+ // Verify trailers are NOT in headers (security verification)
+ EXPECT_EQ(std::string(""), res->get_header_value("Dummy1"));
+ EXPECT_EQ(std::string(""), res->get_header_value("Dummy2"));
}
TEST_F(ServerTest, LargeChunkedPost) {
@@ -8806,3 +8820,54 @@ TEST(ClientInThreadTest, Issue2068) {
t.join();
}
}
+
+TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) {
+ Server svr;
+
+ svr.Get("/", [](const Request &req, Response &res) {
+ EXPECT_EQ(2U, req.trailers.size());
+
+ EXPECT_FALSE(req.has_trailer("[invalid key...]"));
+
+ // Denied
+ EXPECT_FALSE(req.has_trailer("Content-Length"));
+ EXPECT_FALSE(req.has_trailer("X-Forwarded-For"));
+
+ // Accepted
+ EXPECT_TRUE(req.has_trailer("X-Hello"));
+ EXPECT_EQ(req.get_trailer_value("X-Hello"), "hello");
+
+ EXPECT_TRUE(req.has_trailer("X-World"));
+ EXPECT_EQ(req.get_trailer_value("X-World"), "world");
+
+ res.set_content("ok", "text/plain");
+ });
+
+ thread t = thread([&]() { svr.listen(HOST, PORT); });
+ auto se = detail::scope_exit([&] {
+ svr.stop();
+ t.join();
+ ASSERT_FALSE(svr.is_running());
+ });
+
+ svr.wait_until_ready();
+
+ const std::string req = "GET / HTTP/1.1\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Trailer: X-Hello, X-World, X-AAA, X-BBB\r\n"
+ "\r\n"
+ "0\r\n"
+ "Content-Length: 10\r\n"
+ "Host: internal.local\r\n"
+ "Content-Type: malicious/content\r\n"
+ "Cookie: any\r\n"
+ "Set-Cookie: any\r\n"
+ "X-Forwarded-For: attacker.com\r\n"
+ "X-Real-Ip: 1.1.1.1\r\n"
+ "X-Hello: hello\r\n"
+ "X-World: world\r\n"
+ "\r\n";
+
+ std::string res;
+ ASSERT_TRUE(send_request(1, req, &res));
+}