Browse Source

Match certificate names as X509_check_host does

The Mbed TLS and wolfSSL backends matched *.com, a bare *, vic*.test and
names with empty labels, which OpenSSL rejects. Require two labels after
a wildcard, none in an IDNA label and no empty label, and ignore a
trailing dot on the host, so every backend accepts the same names.
yhirose 1 ngày trước cách đây
mục cha
commit
314e5577b8
2 tập tin đã thay đổi với 60 bổ sung và 17 xóa
  1. 37 17
      httplib.h
  2. 23 0
      test/test.cc

+ 37 - 17
httplib.h

@@ -10902,31 +10902,51 @@ inline std::pair<std::string, std::string> make_digest_authentication_header(
   return std::make_pair(key, field);
 }
 
+// Follows X509_check_host, which the OpenSSL backend uses, so that every
+// backend accepts the same names.
 inline bool match_hostname(const std::string &pattern,
                            const std::string &hostname) {
-  // Exact match (case-insensitive)
-  if (detail::case_ignore::equal(hostname, pattern)) { return true; }
-
-  // Split both pattern and hostname into components by '.'
-  std::vector<std::string> pattern_components;
-  if (!pattern.empty()) {
-    split(pattern.data(), pattern.data() + pattern.size(), '.',
-          [&](const char *b, const char *e) {
-            pattern_components.emplace_back(b, e);
-          });
-  }
+  // A trailing dot on the host is ignored
+  auto host = hostname;
+  if (host.size() > 1 && host.back() == '.') { host.pop_back(); }
 
-  std::vector<std::string> host_components;
-  if (!hostname.empty()) {
-    split(hostname.data(), hostname.data() + hostname.size(), '.',
-          [&](const char *b, const char *e) {
-            host_components.emplace_back(b, e);
-          });
+  // Exact match (case-insensitive)
+  if (detail::case_ignore::equal(host, pattern)) { return true; }
+
+  // Split both pattern and hostname into components by '.', keeping any empty
+  // one, which no name may have
+  auto labels = [](const std::string &s) {
+    std::vector<std::string> v;
+    size_t b = 0;
+    while (true) {
+      auto e = s.find('.', b);
+      v.push_back(s.substr(b, e - b));
+      if (e == std::string::npos) { return v; }
+      b = e + 1;
+    }
+  };
+  auto pattern_components = labels(pattern);
+  auto host_components = labels(host);
+  auto has_empty_label = [](const std::vector<std::string> &v) {
+    return std::any_of(v.begin(), v.end(),
+                       [](const std::string &l) { return l.empty(); });
+  };
+  if (has_empty_label(pattern_components) || has_empty_label(host_components)) {
+    return false;
   }
 
   // Component count must match
   if (host_components.size() != pattern_components.size()) { return false; }
 
+  // A wildcard needs two labels after it, so that "*.com" matches nothing,
+  // and an IDNA label may not have one
+  const auto &leftmost = pattern_components.front();
+  if (leftmost.find('*') != std::string::npos &&
+      (pattern_components.size() < 3 ||
+       case_ignore::equal(leftmost.substr(0, 4), "xn--"))) {
+    return false;
+  }
+
   // Compare each component with wildcard support
   // Supports: "*" (full wildcard), "prefix*" (partial wildcard)
   // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484

+ 23 - 0
test/test.cc

@@ -562,6 +562,9 @@ find_content_type(const std::string &path,
                   const std::string &default_content_type);
 bool can_compress_content_type(const std::string &content_type);
 bool is_prohibited_header_name(const std::string &name);
+#ifdef CPPHTTPLIB_SSL_ENABLED
+bool match_hostname(const std::string &pattern, const std::string &hostname);
+#endif
 } // namespace detail
 } // namespace httplib
 
@@ -645,6 +648,26 @@ TEST(FileExtensionTest, TakesTheAlphanumericSuffix) {
   EXPECT_EQ(long_ext, detail::file_extension("a." + long_ext));
 }
 
+#ifdef CPPHTTPLIB_SSL_ENABLED
+TEST(MatchHostnameTest, FollowsX509CheckHost) {
+  EXPECT_TRUE(detail::match_hostname("*.example.com", "www.example.com"));
+  EXPECT_TRUE(detail::match_hostname("www*.example.com", "www1.example.com"));
+  EXPECT_TRUE(detail::match_hostname("www.example.com", "WWW.example.com."));
+
+  // A wildcard needs two labels after it, in the leftmost label only
+  EXPECT_FALSE(detail::match_hostname("*.com", "example.com"));
+  EXPECT_FALSE(detail::match_hostname("*", "localhost"));
+  EXPECT_FALSE(detail::match_hostname("vic*.test", "victim.test"));
+  EXPECT_FALSE(detail::match_hostname("www.*.com", "www.a.com"));
+  EXPECT_FALSE(
+      detail::match_hostname("xn--*.example.com", "xn--a.example.com"));
+
+  // No name may have an empty label
+  EXPECT_FALSE(detail::match_hostname("*.example.com", "a..example.com"));
+  EXPECT_FALSE(detail::match_hostname("a..example.com", "a.example.com"));
+}
+#endif
+
 TEST(CaseIgnoreHashTest, IgnoresCaseAndTakesLongKeys) {
   detail::case_ignore::hash hash;
   EXPECT_EQ(hash("Content-Type"), hash("content-TYPE"));