diff --git a/httplib.h b/httplib.h index 3495500..e6c5534 100644 --- a/httplib.h +++ b/httplib.h @@ -450,6 +450,10 @@ struct hash { } }; +template +using unordered_set = std::unordered_set; + } // namespace case_ignore // This is based on @@ -710,6 +714,7 @@ struct Request { std::string matched_route; Params params; Headers headers; + Headers trailers; std::string body; std::string remote_addr; @@ -744,6 +749,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; @@ -765,6 +774,7 @@ struct Response { int status = -1; std::string reason; Headers headers; + Headers trailers; std::string body; std::string location; // Redirect location @@ -776,6 +786,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); @@ -4727,6 +4741,42 @@ inline ReadContentResult read_content_chunked(Stream &strm, T &x, // chunked transfer coding data without the final CRLF. if (!line_reader.getline()) { return ReadContentResult::Success; } + // RFC 7230 Section 4.1.2 - Headers prohibited in trailers + thread_local case_ignore::unordered_set 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 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) { @@ -4744,11 +4794,12 @@ inline ReadContentResult read_content_chunked(Stream &strm, T &x, 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 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(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(std::distance(r.first, r.second)); +} + inline bool Request::has_param(const std::string &key) const { return params.find(key) != params.end(); } @@ -6571,6 +6640,23 @@ inline void Response::set_header(const std::string &key, 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(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(std::distance(r.first, r.second)); +} inline void Response::set_redirect(const std::string &url, int stat) { if (detail::fields::is_field_value(url)) { diff --git a/test/test.cc b/test/test.cc index 023e926..7e397de 100644 --- a/test/test.cc +++ b/test/test.cc @@ -4886,8 +4886,22 @@ TEST_F(ServerTest, GetStreamedChunkedWithTrailer) { 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) { @@ -10567,3 +10581,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)); +}