Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 89 additions & 3 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

// This is based on
Expand Down Expand Up @@ -710,6 +714,7 @@ struct Request {
std::string matched_route;
Params params;
Headers headers;
Headers trailers;
std::string body;

std::string remote_addr;
Expand Down Expand Up @@ -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;
Expand All @@ -765,6 +774,7 @@ struct Response {
int status = -1;
std::string reason;
Headers headers;
Headers trailers;
std::string body;
std::string location; // Redirect location

Expand All @@ -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);
Expand Down Expand Up @@ -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<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) {
Expand All @@ -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; }
}

Expand Down Expand Up @@ -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 {
return params.find(key) != params.end();
}
Expand Down Expand Up @@ -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<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)) {
Expand Down
69 changes: 67 additions & 2 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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));
}
Loading