mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2025-05-10 01:33:53 +00:00
Fix HTTP Response Splitting Vulnerability
This commit is contained in:
parent
b766025a83
commit
9c36aae4b7
62
httplib.h
62
httplib.h
@ -2506,6 +2506,60 @@ private:
|
|||||||
bool is_open_empty_file = false;
|
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<unsigned char>(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
|
} // 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,
|
inline void Request::set_header(const std::string &key,
|
||||||
const std::string &val) {
|
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);
|
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,
|
inline void Response::set_header(const std::string &key,
|
||||||
const std::string &val) {
|
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);
|
headers.emplace(key, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void Response::set_redirect(const std::string &url, int stat) {
|
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);
|
set_header("Location", url);
|
||||||
if (300 <= stat && stat < 400) {
|
if (300 <= stat && stat < 400) {
|
||||||
this->status = stat;
|
this->status = stat;
|
||||||
|
82
test/test.cc
82
test/test.cc
@ -7925,6 +7925,88 @@ TEST(DirtyDataRequestTest, HeadFieldValueContains_CR_LF_NUL) {
|
|||||||
cli.Get("/test", {{"Test", "_\n\r_\n\r_"}});
|
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
|
#ifndef _WIN32
|
||||||
TEST(Expect100ContinueTest, ServerClosesConnection) {
|
TEST(Expect100ContinueTest, ServerClosesConnection) {
|
||||||
static constexpr char reject[] = "Unauthorized";
|
static constexpr char reject[] = "Unauthorized";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user