瀏覽代碼

Reject Digest challenges missing realm or nonce (RFC 7616 §3.3)

parse_www_authenticate() accepted any WWW-Authenticate: Digest
challenge that carried at least one auth-param, so a server sending
e.g. Digest qop="auth" with no realm/nonce would make it through.
make_digest_authentication_header() then dereferences auth.at("realm")
and auth.at("nonce") unconditionally, throwing std::out_of_range with
no try/catch on the retry path, which terminates the client process.

Now require both realm and nonce before treating a Digest challenge as
usable, same as if no Digest challenge were present at all.
yhirose 1 天之前
父節點
當前提交
9ce15a14e5
共有 2 個文件被更改,包括 44 次插入3 次删除
  1. 6 3
      httplib.h
  2. 38 0
      test/test.cc

+ 6 - 3
httplib.h

@@ -9961,9 +9961,12 @@ inline bool parse_www_authenticate(const Response &res,
     auth[std::move(key_part)] = std::move(unquoted);
   }
 
-  // A challenge with no auth-param can't produce a usable Authorization
-  // header, so treat it the same as no Digest challenge at all.
-  return found_digest && !auth.empty();
+  // RFC 7616 Section 3.3 requires realm and nonce on every Digest challenge;
+  // make_digest_authentication_header() dereferences both unconditionally, so
+  // a challenge missing either can't produce a usable Authorization header.
+  // Treat it the same as no Digest challenge at all.
+  return found_digest && auth.find("realm") != auth.end() &&
+         auth.find("nonce") != auth.end();
 }
 
 class ContentProviderAdapter {

+ 38 - 0
test/test.cc

@@ -3180,6 +3180,44 @@ TEST(DigestAuthTest, RealmContainingCommaIsNotSplit) {
       "test,realm");
 }
 
+// RFC 7616 Section 3.3 requires realm and nonce on every Digest challenge.
+// make_digest_authentication_header() dereferences both unconditionally, so
+// a server sending a challenge missing either one must not be treated as
+// usable -- doing so used to crash the client with std::out_of_range.
+static void run_digest_challenge_missing_field_test(const char *challenge) {
+  Server svr;
+  svr.Get("/x", [&](const Request & /*req*/, Response &res) {
+    res.status = StatusCode::Unauthorized_401;
+    res.set_header("WWW-Authenticate", challenge);
+  });
+
+  auto port = svr.bind_to_any_port(HOST);
+  std::thread t([&]() { svr.listen_after_bind(); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+  });
+  svr.wait_until_ready();
+
+  Client cli(HOST, port);
+  cli.set_digest_auth("hello", "world");
+  auto res = cli.Get("/x");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+  EXPECT_EQ(StatusCode::Unauthorized_401, res->status);
+}
+
+TEST(DigestAuthTest, ChallengeMissingRealmAndNonceDoesNotCrash) {
+  run_digest_challenge_missing_field_test("Digest qop=\"auth\"");
+}
+
+TEST(DigestAuthTest, ChallengeMissingNonceDoesNotCrash) {
+  run_digest_challenge_missing_field_test("Digest realm=\"r\", qop=\"auth\"");
+}
+
+TEST(DigestAuthTest, ChallengeMissingRealmDoesNotCrash) {
+  run_digest_challenge_missing_field_test("Digest nonce=\"n\", qop=\"auth\"");
+}
+
 #endif
 
 TEST(SpecifyServerIPAddressTest, AnotherHostname_Online) {