From cbaf751da1cc4c6d5cb65a3ecbb31cb0260824f2 Mon Sep 17 00:00:00 2001 From: yhirose Date: Mon, 12 Oct 2026 22:59:47 -0400 Subject: [PATCH] Ignore quoted-strings when parsing an Accept header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse_accept_header() split on every ',' and then on every ';', with no idea that a parameter value can be a quoted-string (RFC 9110 §5.6.6). A value such as text/html;profile="a,b" was cut in the middle, and the request was answered with 400. The parser never looks at a parameter value, so remove each quoted-string (§5.6.4) before parsing rather than teach every split about quotes. A header without a '"' takes the same path as before. A quoted-string is removed wherever it appears, so a malformed value can now be read without it instead of being rejected, and an unterminated quote hides the rest of the value. --- httplib.h | 22 ++++++++++++++++++++++ test/test.cc | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/httplib.h b/httplib.h index 526cb3fc..d7582318 100644 --- a/httplib.h +++ b/httplib.h @@ -9247,8 +9247,30 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try { } catch (...) { return false; } #endif +// Removes each quoted-string, honoring a quoted-pair (RFC 9110 Section 5.6.4) +inline std::string remove_quoted_strings(const std::string &s) { + std::string out; + auto in_quotes = false; + for (size_t i = 0; i < s.size(); i++) { + if (s[i] == '"') { + in_quotes = !in_quotes; + } else if (!in_quotes) { + out += s[i]; + } else if (s[i] == '\\') { + i++; + } + } + return out; +} + inline bool parse_accept_header(const std::string &s, std::vector &content_types) { + // Parameter values are not used here, so a quoted-string is dropped rather + // than parsed: a ',' or ';' inside one must not be read as a delimiter. + if (s.find('"') != std::string::npos) { + return parse_accept_header(remove_quoted_strings(s), content_types); + } + content_types.clear(); // Empty string is considered valid (no preference) diff --git a/test/test.cc b/test/test.cc index 38385f74..09b31296 100644 --- a/test/test.cc +++ b/test/test.cc @@ -1119,6 +1119,39 @@ TEST(ParseAcceptHeaderTest, EmptyListElementsAreIgnored) { EXPECT_FALSE(detail::parse_accept_header(",invalidtype,", result)); } +// RFC 9110 Section 5.6.6: a parameter value may be a quoted-string, and a ',' +// or ';' inside one does not end the list element or the parameter. Section +// 5.6.4: nor does a '"' that a backslash escapes end the quoted-string. +TEST(ParseAcceptHeaderTest, DelimitersInQuotedParameterValues) { + struct { + const char *value; + std::vector expected; + } cases[] = { + {"text/html;profile=\"a,b\",application/json", + {"text/html", "application/json"}}, + // A "q=" inside a quoted value is not the weight + {"text/html;profile=\"a;q=0.1\",application/json;q=0.5", + {"text/html", "application/json"}}, + // The weight after a quoted value still applies + {"text/html;profile=\"a,b;q=0.1\";q=0.8,application/json;q=0.9", + {"application/json", "text/html"}}, + {"text/html;profile=\"a\\\",b\";q=0.8,application/json;q=0.9", + {"application/json", "text/html"}}, + {"text/html;profile=\"a\\\"b\",application/json", + {"text/html", "application/json"}}, + // An escaped backslash does not escape the '"' after it + {"text/html;profile=\"a\\\\\",application/json", + {"text/html", "application/json"}}, + }; + + for (const auto &c : cases) { + std::vector result; + EXPECT_TRUE(detail::parse_accept_header(c.value, result)) + << "value: " << c.value; + EXPECT_EQ(c.expected, result) << "value: " << c.value; + } +} + TEST(ParseAcceptHeaderTest, ContentTypesPopulatedAndInvalidHeaderHandling) { Server svr;