From 9c36aae4b73e2b6e493f4133e4173103c9266289 Mon Sep 17 00:00:00 2001 From: yhirose Date: Thu, 16 Jan 2025 00:04:17 -0500 Subject: [PATCH] Fix HTTP Response Splitting Vulnerability --- httplib.h | 62 +++++++++++++++++++++++++++++++++++++-- test/test.cc | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/httplib.h b/httplib.h index 7743f9f..27141f0 100644 --- a/httplib.h +++ b/httplib.h @@ -2506,6 +2506,60 @@ private: bool is_open_empty_file = false; }; +// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5 +namespace fields { + +inline bool is_token_char(char c) { + return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' || + c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || + c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; +} + +inline bool is_token(const std::string &s) { + if (s.empty()) { return false; } + for (auto c : s) { + if (!is_token_char(c)) { return false; } + } + return true; +} + +inline bool is_field_name(const std::string &s) { return is_token(s); } + +inline bool is_vchar(char c) { return c >= 33 && c <= 126; } + +inline bool is_obs_text(char c) { return 128 <= static_cast(c); } + +inline bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); } + +inline bool is_field_content(const std::string &s) { + if (s.empty()) { return false; } + + if (s.size() == 1) { + return is_field_vchar(s[0]); + } else if (s.size() == 2) { + return is_field_vchar(s[0]) && is_field_vchar(s[1]); + } else { + size_t i = 0; + + if (!is_field_vchar(s[i])) { return false; } + i++; + + while (i < s.size() - 1) { + auto c = s[i++]; + if (c == ' ' || c == '\t' || is_field_vchar(c)) { + } else { + return false; + } + } + + return is_field_vchar(s[i]); + } +} + +inline bool is_field_value(const std::string &s) { return is_field_content(s); } + +}; // namespace fields + } // namespace detail // ---------------------------------------------------------------------------- @@ -5699,7 +5753,8 @@ inline size_t Request::get_header_value_count(const std::string &key) const { inline void Request::set_header(const std::string &key, const std::string &val) { - if (!detail::has_crlf(key) && !detail::has_crlf(val)) { + if (detail::fields::is_field_name(key) && + detail::fields::is_field_value(val)) { headers.emplace(key, val); } } @@ -5765,13 +5820,14 @@ inline size_t Response::get_header_value_count(const std::string &key) const { inline void Response::set_header(const std::string &key, const std::string &val) { - if (!detail::has_crlf(key) && !detail::has_crlf(val)) { + if (detail::fields::is_field_name(key) && + detail::fields::is_field_value(val)) { headers.emplace(key, val); } } inline void Response::set_redirect(const std::string &url, int stat) { - if (!detail::has_crlf(url)) { + if (detail::fields::is_field_value(url)) { set_header("Location", url); if (300 <= stat && stat < 400) { this->status = stat; diff --git a/test/test.cc b/test/test.cc index 6ec4b6f..ebc50f6 100644 --- a/test/test.cc +++ b/test/test.cc @@ -7925,6 +7925,88 @@ TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) { cli.Get("/test", {{"Test", "_\n\r_\n\r_"}}); } +TEST(InvalidHeaderCharsTest, is_field_name) { + EXPECT_TRUE(detail::fields::is_field_name("exampleToken")); + EXPECT_TRUE(detail::fields::is_field_name("token123")); + EXPECT_TRUE(detail::fields::is_field_name("!#$%&'*+-.^_`|~")); + + EXPECT_FALSE(detail::fields::is_field_name("example token")); + EXPECT_FALSE(detail::fields::is_field_name(" example_token")); + EXPECT_FALSE(detail::fields::is_field_name("example_token ")); + EXPECT_FALSE(detail::fields::is_field_name("token@123")); + EXPECT_FALSE(detail::fields::is_field_name("")); + EXPECT_FALSE(detail::fields::is_field_name("example\rtoken")); + EXPECT_FALSE(detail::fields::is_field_name("example\ntoken")); + EXPECT_FALSE(detail::fields::is_field_name(std::string("\0", 1))); + EXPECT_FALSE(detail::fields::is_field_name("example\ttoken")); +} + +TEST(InvalidHeaderCharsTest, is_field_value) { + EXPECT_TRUE(detail::fields::is_field_value("exampleToken")); + EXPECT_TRUE(detail::fields::is_field_value("token123")); + EXPECT_TRUE(detail::fields::is_field_value("!#$%&'*+-.^_`|~")); + + EXPECT_TRUE(detail::fields::is_field_value("example token")); + EXPECT_FALSE(detail::fields::is_field_value(" example_token")); + EXPECT_FALSE(detail::fields::is_field_value("example_token ")); + EXPECT_TRUE(detail::fields::is_field_value("token@123")); + EXPECT_FALSE(detail::fields::is_field_value("")); + EXPECT_FALSE(detail::fields::is_field_value("example\rtoken")); + EXPECT_FALSE(detail::fields::is_field_value("example\ntoken")); + EXPECT_FALSE(detail::fields::is_field_value(std::string("\0", 1))); + EXPECT_TRUE(detail::fields::is_field_value("example\ttoken")); + + EXPECT_TRUE(detail::fields::is_field_value("0")); +} + +TEST(InvalidHeaderCharsTest, OnServer) { + Server svr; + + svr.Get("/test_name", [&](const Request &req, Response &res) { + std::string header = "Not Set"; + if (req.has_param("header")) { header = req.get_param_value("header"); } + + res.set_header(header, "value"); + res.set_content("Page Content Page Content", "text/plain"); + }); + + svr.Get("/test_value", [&](const Request &req, Response &res) { + std::string header = "Not Set"; + if (req.has_param("header")) { header = req.get_param_value("header"); } + + res.set_header("X-Test", header); + res.set_content("Page Content Page Content", "text/plain"); + }); + + auto thread = std::thread([&]() { svr.listen(HOST, PORT); }); + + auto se = detail::scope_exit([&] { + svr.stop(); + thread.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + Client cli(HOST, PORT); + { + auto res = cli.Get( + R"(/test_name?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)"); + + ASSERT_TRUE(res); + EXPECT_EQ("Page Content Page Content", res->body); + EXPECT_FALSE(res->has_header("HEADER_KEY")); + } + { + auto res = cli.Get( + R"(/test_value?header=Value%00%0d%0aHEADER_KEY%3aHEADER_VALUE%0d%0a%0d%0aBODY_BODY_BODY)"); + + ASSERT_TRUE(res); + EXPECT_EQ("Page Content Page Content", res->body); + EXPECT_FALSE(res->has_header("HEADER_KEY")); + } +} + #ifndef _WIN32 TEST(Expect100ContinueTest, ServerClosesConnection) { static constexpr char reject[] = "Unauthorized";