From 64bebd3b2b8f99ff183d1e5c9eb29f08b1dca0a0 Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 16 Oct 2026 16:39:00 -0400 Subject: [PATCH] Honor quoted-pairs in parameter values split_unquoted ended a quoted-string at an escaped quote, so name="a\"; filename=evil.php" came apart into a name and a filename that a parser in front following RFC 9110 would not see. Skip quoted-pairs when splitting, and unescape \" and \\ in Content-Disposition values, leaving other backslashes, such as those of a Windows path, as they are. --- httplib.h | 26 +++++++++++++++++++++++--- test/test.cc | 6 ++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/httplib.h b/httplib.h index 07fadc70..9d15c9f8 100644 --- a/httplib.h +++ b/httplib.h @@ -6196,8 +6196,12 @@ inline void split_unquoted(const char *b, const char *e, char d, size_t m, size_t count = 1; auto in_quotes = false; - while (e ? (b + i < e) : (b[i] != '\0')) { - if (b[i] == '"') { + auto has = [&](size_t j) { return e ? (b + j < e) : (b[j] != '\0'); }; + while (has(i)) { + if (in_quotes && b[i] == '\\' && has(i + 1)) { + // A quoted-pair, whose second character does not end the string. + i++; + } else if (b[i] == '"') { in_quotes = !in_quotes; } else if (b[i] == d && !in_quotes && count < m) { auto r = trim(b, e, beg, i); @@ -9241,6 +9245,22 @@ inline bool parse_multipart_boundary(const std::string &content_type, 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) { std::set cache; split_unquoted(s.data(), s.data() + s.size(), ';', @@ -9255,7 +9275,7 @@ inline void parse_disposition_params(const std::string &s, Params ¶ms) { if (!key.empty()) { params.emplace(trim_double_quotes_copy(key), - trim_double_quotes_copy(val)); + unquote_disposition_param(val)); } }); } diff --git a/test/test.cc b/test/test.cc index 626a0d8d..e5acf7f4 100644 --- a/test/test.cc +++ b/test/test.cc @@ -1857,6 +1857,12 @@ TEST(ParseDispositionParamsTest, QuotedValues) { // A parameter with no key is dropped rather than turned into one whose // key is the value {"=nokey; name=\"file5\"", {{"name", "file5"}}}, + // An escaped quote does not end the value + {"name=\"a\\\"; filename=evil.php\"", + {{"name", "a\"; filename=evil.php"}}}, + // Other backslashes, as in a Windows path, are kept + {"name=\"f\"; filename=\"C:\\dir\\a.txt\"", + {{"name", "f"}, {"filename", "C:\\dir\\a.txt"}}}, }; for (const auto &c : cases) {