mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-19 11:19:36 +00:00
Unescape quoted Content-Type parameters and escape backslashes
A quoted boundary kept its quoted-pairs, as only the quotes were taken off, and the multipart writer sent a backslash as it was, so a name ending in one escaped the closing quote for this library's own parser. Unescape \" and \\ in media type parameters as in Content-Disposition, and write a backslash as \\.
This commit is contained in:
1 parent
aa0a588ed5
commit
a10338a6ee
2 files changed
+82
-36
No files matched your search
@@ -6185,6 +6185,22 @@ inline std::string trim_double_quotes_copy(const std::string &s) {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Takes the quotes off a parameter value. Only \" and \\ are quoted-pairs
|
||||||
|
// here, as browsers send any other backslash, such as one in a Windows path,
|
||||||
|
// as it is.
|
||||||
|
inline std::string unquote_param(const std::string &s) {
|
||||||
|
if (s.size() < 2 || s.front() != '"' || s.back() != '"') { return s; }
|
||||||
|
std::string out;
|
||||||
|
for (size_t i = 1; i + 1 < s.size(); i++) {
|
||||||
|
if (s[i] == '\\' && i + 2 < s.size() &&
|
||||||
|
(s[i + 1] == '"' || s[i + 1] == '\\')) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
out += s[i];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
inline void
|
inline void
|
||||||
divide(const char *data, std::size_t size, char d,
|
divide(const char *data, std::size_t size, char d,
|
||||||
std::function<void(const char *, std::size_t, const char *, std::size_t)>
|
std::function<void(const char *, std::size_t, const char *, std::size_t)>
|
||||||
@@ -7742,7 +7758,7 @@ extract_media_type(const std::string &content_type,
|
|||||||
// §5.6.6)
|
// §5.6.6)
|
||||||
if (!key.empty()) {
|
if (!key.empty()) {
|
||||||
params->emplace(case_ignore::to_lower(trim_copy(key)),
|
params->emplace(case_ignore::to_lower(trim_copy(key)),
|
||||||
trim_double_quotes_copy(val));
|
unquote_param(val));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -9328,39 +9344,22 @@ inline bool parse_multipart_boundary(const std::string &content_type,
|
|||||||
return !boundary.empty() && boundary.size() <= 70;
|
return !boundary.empty() && boundary.size() <= 70;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Takes the quotes off a parameter value. Only \" and \\ are quoted-pairs
|
|
||||||
// here, as browsers send any other backslash, such as one in a Windows path,
|
|
||||||
// as it is.
|
|
||||||
inline std::string unquote_disposition_param(const std::string &s) {
|
|
||||||
if (s.size() < 2 || s.front() != '"' || s.back() != '"') { return s; }
|
|
||||||
std::string out;
|
|
||||||
for (size_t i = 1; i + 1 < s.size(); i++) {
|
|
||||||
if (s[i] == '\\' && i + 2 < s.size() &&
|
|
||||||
(s[i + 1] == '"' || s[i + 1] == '\\')) {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
out += s[i];
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void parse_disposition_params(const std::string &s, Params ¶ms) {
|
inline void parse_disposition_params(const std::string &s, Params ¶ms) {
|
||||||
std::set<std::string> cache;
|
std::set<std::string> cache;
|
||||||
split_unquoted(s.data(), s.data() + s.size(), ';',
|
split_unquoted(
|
||||||
[&](const char *b, const char *e) {
|
s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) {
|
||||||
std::string kv(b, e);
|
std::string kv(b, e);
|
||||||
if (cache.find(kv) != cache.end()) { return; }
|
if (cache.find(kv) != cache.end()) { return; }
|
||||||
cache.insert(kv);
|
cache.insert(kv);
|
||||||
|
|
||||||
std::string key;
|
std::string key;
|
||||||
std::string val;
|
std::string val;
|
||||||
divide_param_pair(b, e, key, val);
|
divide_param_pair(b, e, key, val);
|
||||||
|
|
||||||
if (!key.empty()) {
|
if (!key.empty()) {
|
||||||
params.emplace(trim_double_quotes_copy(key),
|
params.emplace(trim_double_quotes_copy(key), unquote_param(val));
|
||||||
unquote_disposition_param(val));
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
|
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
|
||||||
@@ -9877,23 +9876,29 @@ inline bool is_multipart_boundary_chars_valid(const std::string &boundary) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Escape a multipart field name/filename following the WHATWG HTML standard
|
// Escape a multipart field name/filename following the WHATWG HTML standard
|
||||||
// ("escape a multipart form-data name"), which is what browsers send:
|
// ("escape a multipart form-data name"): '"' -> %22, CR -> %0D, LF -> %0A.
|
||||||
// '"' -> %22, CR -> %0D, LF -> %0A
|
// As the value is a quoted-string, '\' -> '\\' too, which browsers leave out.
|
||||||
// With escape_quote = false, only CR and LF are escaped; this is for header
|
// With quoted = false, only CR and LF are escaped; this is for header values
|
||||||
// values outside a quoted-string (e.g. Content-Type), where '"' is legal.
|
// outside a quoted-string (e.g. Content-Type), where '"' and '\' are legal.
|
||||||
inline std::string escape_multipart_field(const std::string &s,
|
inline std::string escape_multipart_field(const std::string &s,
|
||||||
bool escape_quote = true) {
|
bool quoted = true) {
|
||||||
std::string result;
|
std::string result;
|
||||||
result.reserve(s.size());
|
result.reserve(s.size());
|
||||||
for (auto c : s) {
|
for (auto c : s) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case '"':
|
case '"':
|
||||||
if (escape_quote) {
|
if (quoted) {
|
||||||
result += "%22";
|
result += "%22";
|
||||||
} else {
|
} else {
|
||||||
result += c;
|
result += c;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case '\\':
|
||||||
|
// A lone backslash would escape the character after it (RFC 9110
|
||||||
|
// 5.6.4).
|
||||||
|
if (quoted) { result += '\\'; }
|
||||||
|
result += c;
|
||||||
|
break;
|
||||||
case '\r': result += "%0D"; break;
|
case '\r': result += "%0D"; break;
|
||||||
case '\n': result += "%0A"; break;
|
case '\n': result += "%0A"; break;
|
||||||
default: result += c; break;
|
default: result += c; break;
|
||||||
|
|||||||
@@ -1739,6 +1739,37 @@ TEST(MultipartOrderTest, RepeatedNamesKeepTheirOrder) {
|
|||||||
EXPECT_EQ("", out_of_range);
|
EXPECT_EQ("", out_of_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(MultipartFormDataTest, BackslashesInNamesSurvive) {
|
||||||
|
Server svr;
|
||||||
|
std::string file_names;
|
||||||
|
svr.Post("/upload", [&](const Request &req, Response &res) {
|
||||||
|
for (const auto &file : req.form.files) {
|
||||||
|
file_names += file.first + "|" + file.second.filename + " ";
|
||||||
|
}
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
thread t = thread([&] { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
t.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
// A trailing backslash would otherwise escape the closing quote.
|
||||||
|
UploadFormDataItems items = {
|
||||||
|
{"dir\\", "x", "a.txt", "text/plain"},
|
||||||
|
{"b", "y", "a\\\\b.txt", "text/plain"},
|
||||||
|
};
|
||||||
|
|
||||||
|
Client cli(HOST, port);
|
||||||
|
auto res = cli.Post("/upload", items);
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ("dir\\|a.txt b|a\\\\b.txt ", file_names);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(MultipartOrderTest, ContentSurvivesContainerGrowth) {
|
TEST(MultipartOrderTest, ContentSurvivesContainerGrowth) {
|
||||||
// Server::read_content() keeps a FormFields::iterator alive across the
|
// Server::read_content() keeps a FormFields::iterator alive across the
|
||||||
// content callbacks that fill the part it points at. The container is
|
// content callbacks that fill the part it points at. The container is
|
||||||
@@ -1872,6 +1903,16 @@ TEST(ParseMultipartBoundaryTest, QuotedValueWithSemicolon) {
|
|||||||
EXPECT_EQ(parsed, "a;b");
|
EXPECT_EQ(parsed, "a;b");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ParseMultipartBoundaryTest, QuotedPairsAreUnescaped) {
|
||||||
|
string parsed;
|
||||||
|
EXPECT_TRUE(detail::parse_multipart_boundary(
|
||||||
|
"multipart/form-data; boundary=\"a\\\\b\"", parsed));
|
||||||
|
EXPECT_EQ("a\\b", parsed);
|
||||||
|
EXPECT_TRUE(detail::parse_multipart_boundary(
|
||||||
|
"multipart/form-data; boundary=\"a\\\"b\"", parsed));
|
||||||
|
EXPECT_EQ("a\"b", parsed);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(ParseDispositionParamsTest, QuotedValues) {
|
TEST(ParseDispositionParamsTest, QuotedValues) {
|
||||||
// RFC 9110 Section 5.6.6: a parameter value may be a quoted-string, and
|
// RFC 9110 Section 5.6.6: a parameter value may be a quoted-string, and
|
||||||
// ';' and '=' are ordinary characters inside one.
|
// ';' and '=' are ordinary characters inside one.
|
||||||
|
|||||||
Reference in new issue
Block a user