You've already forked cpp-httplib
Merge commit from fork
* Fix HTTP Header Smuggling due to insecure trailers merge * Improve performance
This commit is contained in:
92
httplib.h
92
httplib.h
@ -450,6 +450,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
|
} // namespace case_ignore
|
||||||
|
|
||||||
// This is based on
|
// This is based on
|
||||||
@ -710,6 +714,7 @@ struct Request {
|
|||||||
std::string matched_route;
|
std::string matched_route;
|
||||||
Params params;
|
Params params;
|
||||||
Headers headers;
|
Headers headers;
|
||||||
|
Headers trailers;
|
||||||
std::string body;
|
std::string body;
|
||||||
|
|
||||||
std::string remote_addr;
|
std::string remote_addr;
|
||||||
@ -744,6 +749,10 @@ struct Request {
|
|||||||
size_t get_header_value_count(const std::string &key) const;
|
size_t get_header_value_count(const std::string &key) const;
|
||||||
void set_header(const std::string &key, const std::string &val);
|
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;
|
bool has_param(const std::string &key) const;
|
||||||
std::string get_param_value(const std::string &key, size_t id = 0) 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;
|
size_t get_param_value_count(const std::string &key) const;
|
||||||
@ -765,6 +774,7 @@ struct Response {
|
|||||||
int status = -1;
|
int status = -1;
|
||||||
std::string reason;
|
std::string reason;
|
||||||
Headers headers;
|
Headers headers;
|
||||||
|
Headers trailers;
|
||||||
std::string body;
|
std::string body;
|
||||||
std::string location; // Redirect location
|
std::string location; // Redirect location
|
||||||
|
|
||||||
@ -776,6 +786,10 @@ struct Response {
|
|||||||
size_t get_header_value_count(const std::string &key) const;
|
size_t get_header_value_count(const std::string &key) const;
|
||||||
void set_header(const std::string &key, const std::string &val);
|
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_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 char *s, size_t n, const std::string &content_type);
|
||||||
void set_content(const std::string &s, const std::string &content_type);
|
void set_content(const std::string &s, const std::string &content_type);
|
||||||
@ -4727,6 +4741,42 @@ inline ReadContentResult read_content_chunked(Stream &strm, T &x,
|
|||||||
// chunked transfer coding data without the final CRLF.
|
// chunked transfer coding data without the final CRLF.
|
||||||
if (!line_reader.getline()) { return ReadContentResult::Success; }
|
if (!line_reader.getline()) { return ReadContentResult::Success; }
|
||||||
|
|
||||||
|
// 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;
|
size_t trailer_header_count = 0;
|
||||||
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
|
while (strcmp(line_reader.ptr(), "\r\n") != 0) {
|
||||||
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) {
|
if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) {
|
||||||
@ -4744,11 +4794,12 @@ inline ReadContentResult read_content_chunked(Stream &strm, T &x,
|
|||||||
|
|
||||||
parse_header(line_reader.ptr(), end,
|
parse_header(line_reader.ptr(), end,
|
||||||
[&](const std::string &key, const std::string &val) {
|
[&](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 ReadContentResult::Error; }
|
if (!line_reader.getline()) { return ReadContentResult::Error; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6468,6 +6519,24 @@ inline void Request::set_header(const std::string &key,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 bool Request::has_param(const std::string &key) const {
|
inline bool Request::has_param(const std::string &key) const {
|
||||||
return params.find(key) != params.end();
|
return params.find(key) != params.end();
|
||||||
}
|
}
|
||||||
@ -6571,6 +6640,23 @@ inline void Response::set_header(const std::string &key,
|
|||||||
headers.emplace(key, val);
|
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) {
|
inline void Response::set_redirect(const std::string &url, int stat) {
|
||||||
if (detail::fields::is_field_value(url)) {
|
if (detail::fields::is_field_value(url)) {
|
||||||
|
69
test/test.cc
69
test/test.cc
@ -4886,8 +4886,22 @@ TEST_F(ServerTest, GetStreamedChunkedWithTrailer) {
|
|||||||
ASSERT_TRUE(res);
|
ASSERT_TRUE(res);
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
EXPECT_EQ(std::string("123456789"), res->body);
|
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) {
|
TEST_F(ServerTest, LargeChunkedPost) {
|
||||||
@ -10567,3 +10581,54 @@ TEST(ClientInThreadTest, Issue2068) {
|
|||||||
t.join();
|
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));
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user