From 9ce15a14e5bbe0bc135b25236339c53ba1e28923 Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 28 Aug 2026 23:36:40 -0400 Subject: [PATCH] =?UTF-8?q?Reject=20Digest=20challenges=20missing=20realm?= =?UTF-8?q?=20or=20nonce=20(RFC=207616=20=C2=A73.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- httplib.h | 9 ++++++--- test/test.cc | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/httplib.h b/httplib.h index ba028018..4ec92afb 100644 --- a/httplib.h +++ b/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 { diff --git a/test/test.cc b/test/test.cc index 95aadb1b..2a2988d8 100644 --- a/test/test.cc +++ b/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) {