From 5ebd198cf7624b9f1be57c763a5bda004cf9320f Mon Sep 17 00:00:00 2001 From: starlet-dx <15929766099@163.com> Date: Wed, 5 Feb 2025 12:02:50 +0800 Subject: [PATCH] Fix CVE-2025-0825 --- CVE-2025-0825.patch | 195 ++++++++++++++++++++++++++++++++++++++++++++ cpp-httplib.spec | 6 +- 2 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 CVE-2025-0825.patch diff --git a/CVE-2025-0825.patch b/CVE-2025-0825.patch new file mode 100644 index 0000000..2e19a33 --- /dev/null +++ b/CVE-2025-0825.patch @@ -0,0 +1,195 @@ +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 7743f9fd6f..27141f0bf4 100644 +--- a/httplib.h ++++ b/httplib.h +@@ -2506,6 +2506,60 @@ class mmap { + 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 6ec4b6fc63..ebc50f6f01 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"; diff --git a/cpp-httplib.spec b/cpp-httplib.spec index f90f8a3..198961f 100644 --- a/cpp-httplib.spec +++ b/cpp-httplib.spec @@ -1,10 +1,11 @@ Name: cpp-httplib Version: 0.18.3 -Release: 1 +Release: 2 Summary: A C++ header-only HTTP/HTTPS server and client library License: MIT URL: https://github.com/yhirose/cpp-httplib Source0: https://github.com/yhirose/cpp-httplib/archive/refs/tags/v%{version}.tar.gz +Patch0: CVE-2025-0825.patch BuildRequires: gcc-c++ BuildRequires: cmake BuildRequires: pkgconfig(libcurl) @@ -59,6 +60,9 @@ rm -r $RPM_BUILD_ROOT%{_licensedir}/httplib %{_libdir}/cmake/httplib %changelog +* Wed Feb 05 2025 yaoxin <1024769339@qq.com> - 0.18.3-2 +- Fix CVE-2025-0825 + * Thu Jan 16 2025 yaoxin <1024769339@qq.com> - 0.18.3-1 - Update to 0.18.3: * Regression: Client keep-alive subsequent requests very slow #1997 -- Gitee