Browse Source

Ignore quoted-strings when parsing an Accept header

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.
yhirose 2 days ago
parent
commit
cbaf751da1
2 changed files with 55 additions and 0 deletions
  1. 22 0
      httplib.h
  2. 33 0
      test/test.cc

+ 22 - 0
httplib.h

@@ -9247,8 +9247,30 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
 } catch (...) { return false; }
 } catch (...) { return false; }
 #endif
 #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,
 inline bool parse_accept_header(const std::string &s,
                                 std::vector<std::string> &content_types) {
                                 std::vector<std::string> &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();
   content_types.clear();
 
 
   // Empty string is considered valid (no preference)
   // Empty string is considered valid (no preference)

+ 33 - 0
test/test.cc

@@ -1119,6 +1119,39 @@ TEST(ParseAcceptHeaderTest, EmptyListElementsAreIgnored) {
   EXPECT_FALSE(detail::parse_accept_header(",invalidtype,", result));
   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<std::string> 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<std::string> result;
+    EXPECT_TRUE(detail::parse_accept_header(c.value, result))
+        << "value: " << c.value;
+    EXPECT_EQ(c.expected, result) << "value: " << c.value;
+  }
+}
+
 TEST(ParseAcceptHeaderTest, ContentTypesPopulatedAndInvalidHeaderHandling) {
 TEST(ParseAcceptHeaderTest, ContentTypesPopulatedAndInvalidHeaderHandling) {
   Server svr;
   Server svr;