Skip to content

Commit 9c36aae

Browse files
committed
Fix HTTP Response Splitting Vulnerability
1 parent b766025 commit 9c36aae

File tree

2 files changed

+141
-3
lines changed

2 files changed

+141
-3
lines changed

httplib.h

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2506,6 +2506,60 @@ class mmap {
25062506
bool is_open_empty_file = false;
25072507
};
25082508

2509+
// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5
2510+
namespace fields {
2511+
2512+
inline bool is_token_char(char c) {
2513+
return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' ||
2514+
c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' ||
2515+
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
2516+
}
2517+
2518+
inline bool is_token(const std::string &s) {
2519+
if (s.empty()) { return false; }
2520+
for (auto c : s) {
2521+
if (!is_token_char(c)) { return false; }
2522+
}
2523+
return true;
2524+
}
2525+
2526+
inline bool is_field_name(const std::string &s) { return is_token(s); }
2527+
2528+
inline bool is_vchar(char c) { return c >= 33 && c <= 126; }
2529+
2530+
inline bool is_obs_text(char c) { return 128 <= static_cast<unsigned char>(c); }
2531+
2532+
inline bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); }
2533+
2534+
inline bool is_field_content(const std::string &s) {
2535+
if (s.empty()) { return false; }
2536+
2537+
if (s.size() == 1) {
2538+
return is_field_vchar(s[0]);
2539+
} else if (s.size() == 2) {
2540+
return is_field_vchar(s[0]) && is_field_vchar(s[1]);
2541+
} else {
2542+
size_t i = 0;
2543+
2544+
if (!is_field_vchar(s[i])) { return false; }
2545+
i++;
2546+
2547+
while (i < s.size() - 1) {
2548+
auto c = s[i++];
2549+
if (c == ' ' || c == '\t' || is_field_vchar(c)) {
2550+
} else {
2551+
return false;
2552+
}
2553+
}
2554+
2555+
return is_field_vchar(s[i]);
2556+
}
2557+
}
2558+
2559+
inline bool is_field_value(const std::string &s) { return is_field_content(s); }
2560+
2561+
}; // namespace fields
2562+
25092563
} // namespace detail
25102564

25112565
// ----------------------------------------------------------------------------
@@ -5699,7 +5753,8 @@ inline size_t Request::get_header_value_count(const std::string &key) const {
56995753

57005754
inline void Request::set_header(const std::string &key,
57015755
const std::string &val) {
5702-
if (!detail::has_crlf(key) && !detail::has_crlf(val)) {
5756+
if (detail::fields::is_field_name(key) &&
5757+
detail::fields::is_field_value(val)) {
57035758
headers.emplace(key, val);
57045759
}
57055760
}
@@ -5765,13 +5820,14 @@ inline size_t Response::get_header_value_count(const std::string &key) const {
57655820

57665821
inline void Response::set_header(const std::string &key,
57675822
const std::string &val) {
5768-
if (!detail::has_crlf(key) && !detail::has_crlf(val)) {
5823+
if (detail::fields::is_field_name(key) &&
5824+
detail::fields::is_field_value(val)) {
57695825
headers.emplace(key, val);
57705826
}
57715827
}
57725828

57735829
inline void Response::set_redirect(const std::string &url, int stat) {
5774-
if (!detail::has_crlf(url)) {
5830+
if (detail::fields::is_field_value(url)) {
57755831
set_header("Location", url);
57765832
if (300 <= stat && stat < 400) {
57775833
this->status = stat;

test/test.cc

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7925,6 +7925,88 @@ TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) {
79257925
cli.Get("/test", {{"Test", "_\n\r_\n\r_"}});
79267926
}
79277927

7928+
TEST(InvalidHeaderCharsTest, is_field_name) {
7929+
EXPECT_TRUE(detail::fields::is_field_name("exampleToken"));
7930+
EXPECT_TRUE(detail::fields::is_field_name("token123"));
7931+
EXPECT_TRUE(detail::fields::is_field_name("!#$%&'*+-.^_`|~"));
7932+
7933+
EXPECT_FALSE(detail::fields::is_field_name("example token"));
7934+
EXPECT_FALSE(detail::fields::is_field_name(" example_token"));
7935+
EXPECT_FALSE(detail::fields::is_field_name("example_token "));
7936+
EXPECT_FALSE(detail::fields::is_field_name("token@123"));
7937+
EXPECT_FALSE(detail::fields::is_field_name(""));
7938+
EXPECT_FALSE(detail::fields::is_field_name("example\rtoken"));
7939+
EXPECT_FALSE(detail::fields::is_field_name("example\ntoken"));
7940+
EXPECT_FALSE(detail::fields::is_field_name(std::string("\0", 1)));
7941+
EXPECT_FALSE(detail::fields::is_field_name("example\ttoken"));
7942+
}
7943+
7944+
TEST(InvalidHeaderCharsTest, is_field_value) {
7945+
EXPECT_TRUE(detail::fields::is_field_value("exampleToken"));
7946+
EXPECT_TRUE(detail::fields::is_field_value("token123"));
7947+
EXPECT_TRUE(detail::fields::is_field_value("!#$%&'*+-.^_`|~"));
7948+
7949+
EXPECT_TRUE(detail::fields::is_field_value("example token"));
7950+
EXPECT_FALSE(detail::fields::is_field_value(" example_token"));
7951+
EXPECT_FALSE(detail::fields::is_field_value("example_token "));
7952+
EXPECT_TRUE(detail::fields::is_field_value("token@123"));
7953+
EXPECT_FALSE(detail::fields::is_field_value(""));
7954+
EXPECT_FALSE(detail::fields::is_field_value("example\rtoken"));
7955+
EXPECT_FALSE(detail::fields::is_field_value("example\ntoken"));
7956+
EXPECT_FALSE(detail::fields::is_field_value(std::string("\0", 1)));
7957+
EXPECT_TRUE(detail::fields::is_field_value("example\ttoken"));
7958+
7959+
EXPECT_TRUE(detail::fields::is_field_value("0"));
7960+
}
7961+
7962+
TEST(InvalidHeaderCharsTest, OnServer) {
7963+
Server svr;
7964+
7965+
svr.Get("/test_name", [&](const Request &req, Response &res) {
7966+
std::string header = "Not Set";
7967+
if (req.has_param("header")) { header = req.get_param_value("header"); }
7968+
7969+
res.set_header(header, "value");
7970+
res.set_content("Page Content Page Content", "text/plain");
7971+
});
7972+
7973+
svr.Get("/test_value", [&](const Request &req, Response &res) {
7974+
std::string header = "Not Set";
7975+
if (req.has_param("header")) { header = req.get_param_value("header"); }
7976+
7977+
res.set_header("X-Test", header);
7978+
res.set_content("Page Content Page Content", "text/plain");
7979+
});
7980+
7981+
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
7982+
7983+
auto se = detail::scope_exit([&] {
7984+
svr.stop();
7985+
thread.join();
7986+
ASSERT_FALSE(svr.is_running());
7987+
});
7988+
7989+
svr.wait_until_ready();
7990+
7991+
Client cli(HOST, PORT);
7992+
{
7993+
auto res = cli.Get(
7994+
R"(/test_name?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)");
7995+
7996+
ASSERT_TRUE(res);
7997+
EXPECT_EQ("Page Content Page Content", res->body);
7998+
EXPECT_FALSE(res->has_header("HEADER_KEY"));
7999+
}
8000+
{
8001+
auto res = cli.Get(
8002+
R"(/test_value?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)");
8003+
8004+
ASSERT_TRUE(res);
8005+
EXPECT_EQ("Page Content Page Content", res->body);
8006+
EXPECT_FALSE(res->has_header("HEADER_KEY"));
8007+
}
8008+
}
8009+
79288010
#ifndef _WIN32
79298011
TEST(Expect100ContinueTest, ServerClosesConnection) {
79308012
static constexpr char reject[] = "Unauthorized";

0 commit comments

Comments
 (0)