Respect quoted-strings when splitting header parameters (Fix #2568) (#2573)

parse_disposition_params() and extract_media_type() both split on every
';' and then on every '=', with no idea that a parameter value can be a
quoted-string. RFC 9110 5.6.6 allows ';' and '=' inside one, so
filename="report=v2.pdf" came out as v2.pdf", and filename="a;b.txt" was
truncated at the semicolon and left a bogus parameter behind.

The same defect reached the boundary. RFC 2046 5.1.1 allows '=' in a
boundary, which forces a sender to quote it, so the common MIME form
boundary="----=_NextPart_000_0000_01D9" parsed as
_NextPart_000_0000_01D9".

Add split_unquoted(), which is split() with the one extra rule that a
delimiter inside a quoted-string is not a delimiter, and route both
parameter parsers through it. The key/value split, duplicated verbatim
in the two of them, moves into divide_param_pair(). That one divides at
the first '=' without tracking quotes: 5.6.6 makes the key a token, so
no quote can precede the separator, and reusing divide() keeps this off
the per-byte scan.

A backslash stays an ordinary character here. Both browsers and
httplib's own sender percent-encode '"' rather than escaping it, and
recognizing a quoted-pair without also unescaping it would just trade
one wrong value for another.
This commit is contained in:
yhirose authored and GitHub committed 2026-08-27 17:18:29 -04:00
1 parent c58061ea81
commit b4ec1bb1de
2 files changed
+180 -33

No files matched your search

+74 -33
View File
@@ -3619,6 +3619,8 @@ bool parse_range_header(const std::string &s, Ranges &ranges);
bool parse_accept_header(const std::string &s,
std::vector<std::string> &content_types);
void parse_disposition_params(const std::string &s, Params &params);
ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags);
ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags);
@@ -5885,6 +5887,55 @@ inline void split(const char *b, const char *e, char d, size_t m,
}
}
// Same contract as split(), except that a delimiter inside a quoted-string is
// not a delimiter. RFC 9110 Section 5.6.6 lets a parameter value be a
// quoted-string, and ';' and '=' are legal characters inside one.
inline void split_unquoted(const char *b, const char *e, char d, size_t m,
std::function<void(const char *, const char *)> fn) {
size_t i = 0;
size_t beg = 0;
size_t count = 1;
auto in_quotes = false;
while (e ? (b + i < e) : (b[i] != '\0')) {
if (b[i] == '"') {
in_quotes = !in_quotes;
} else if (b[i] == d && !in_quotes && count < m) {
auto r = trim(b, e, beg, i);
if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }
beg = i + 1;
count++;
}
i++;
}
if (i) {
auto r = trim(b, e, beg, i);
if (r.first < r.second) { fn(&b[r.first], &b[r.second]); }
}
}
inline void split_unquoted(const char *b, const char *e, char d,
std::function<void(const char *, const char *)> fn) {
return split_unquoted(b, e, d, (std::numeric_limits<size_t>::max)(),
std::move(fn));
}
// Divide a header parameter at its first '='. RFC 9110 Section 5.6.6 makes the
// key a token, so the first '=' is the separator even when the value is a
// quoted-string carrying more of them.
inline void divide_param_pair(const char *b, const char *e, std::string &key,
std::string &val) {
divide(
b, static_cast<std::size_t>(e - b), '=',
[&](const char *kb, std::size_t klen, const char *vb, std::size_t vlen) {
const auto kr = trim(kb, kb + klen, 0, klen);
key.assign(kb + kr.first, kb + kr.second);
const auto vr = trim(vb, vb + vlen, 0, vlen);
val.assign(vb + vr.first, vb + vr.second);
});
}
inline bool split_find(const char *b, const char *e, char d, size_t m,
std::function<bool(const char *, const char *)> fn) {
size_t i = 0;
@@ -7303,21 +7354,16 @@ extract_media_type(const std::string &content_type,
if (params) {
// Parse parameters: key=value pairs separated by ';'
split(param_str.data(), param_str.data() + param_str.size(), ';',
[&](const char *b, const char *e) {
std::string key;
std::string val;
split(b, e, '=', [&](const char *b2, const char *e2) {
if (key.empty()) {
key.assign(b2, e2);
} else {
val.assign(b2, e2);
}
});
if (!key.empty()) {
params->emplace(trim_copy(key), trim_double_quotes_copy(val));
}
});
split_unquoted(param_str.data(), param_str.data() + param_str.size(), ';',
[&](const char *b, const char *e) {
std::string key;
std::string val;
divide_param_pair(b, e, key, val);
if (!key.empty()) {
params->emplace(trim_copy(key),
trim_double_quotes_copy(val));
}
});
}
}
@@ -8760,26 +8806,21 @@ inline bool parse_multipart_boundary(const std::string &content_type,
inline void parse_disposition_params(const std::string &s, Params &params) {
std::set<std::string> cache;
split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) {
std::string kv(b, e);
if (cache.find(kv) != cache.end()) { return; }
cache.insert(kv);
split_unquoted(s.data(), s.data() + s.size(), ';',
[&](const char *b, const char *e) {
std::string kv(b, e);
if (cache.find(kv) != cache.end()) { return; }
cache.insert(kv);
std::string key;
std::string val;
split(b, e, '=', [&](const char *b2, const char *e2) {
if (key.empty()) {
key.assign(b2, e2);
} else {
val.assign(b2, e2);
}
});
std::string key;
std::string val;
divide_param_pair(b, e, key, val);
if (!key.empty()) {
params.emplace(trim_double_quotes_copy((key)),
trim_double_quotes_copy((val)));
}
});
if (!key.empty()) {
params.emplace(trim_double_quotes_copy(key),
trim_double_quotes_copy(val));
}
});
}
#ifdef CPPHTTPLIB_NO_EXCEPTIONS