http: bind Digest authentication to the request URI
What changed, and why it matters
This commit fixes a security gap in how HTTP Digest authentication was validated in Monero's built-in HTTP server. Previously, a valid authentication response for one URL could potentially be accepted for a different URL. The fix now checks that the URL in the authentication response matches the URL actually being requested. The commit message itself says the practical impact for Monero's RPC use is limited.
Treat as a low-to-moderate security hardening fix. Backport to maintained release branches if they include the RPC HTTP server. No immediate incident response is indicated, but operators should upgrade to a release containing this commit to close the authentication validation gap.
Security signals we found
Digest authentication URI binding enforced
Request target mismatch now rejected
New negative test cases for URI mismatch
Commit message explicitly calls out a validation gap
Evidence from the diff
The patch modifies contrib/epee/src/http_auth.cpp so that auth_message::verify now takes and compares the request URI (request.m_URI) against the uri field inside the parsed Digest Authorization header. The unit tests are updated to pass the URI through make_request and a new RequestTargetBinding test verifies that mismatched URIs are rejected. This addresses a violation of RFC 7616/2617 requirement that the request-target (URI) used in the digest response calculation must match the requested resource.
Changed components
contrib/epee/src/http_auth.cpptests/unit_tests/http.cppHTTP Digest authentication server-side verifierInspect captured patch +56 / −10
diff --git a/contrib/epee/src/http_auth.cpp b/contrib/epee/src/http_auth.cpp
index d9c392f..a826cd0 100644
--- a/contrib/epee/src/http_auth.cpp
+++ b/contrib/epee/src/http_auth.cpp
@@ -358,11 +358,12 @@ namespace
enum status{ kFail = 0, kStale, kPass };
//! \return Status of the `response` field from the client
- static status verify(const boost::string_ref method, const boost::string_ref request,
- const http::http_server_auth::session& user)
+ static status verify(const boost::string_ref method, const boost::string_ref uri,
+ const boost::string_ref request, const http::http_server_auth::session& user)
{
const auto parsed = parse(request);
if (parsed &&
+ boost::equals(parsed->uri, uri) &&
boost::equals(parsed->username, user.credentials.username) &&
boost::fusion::any(digest_algorithms, has_valid_response{*parsed, user, method}))
{
@@ -772,7 +773,7 @@ namespace epee
if (auth != fields.end())
{
++(user->counter);
- switch (auth_message::verify(request.m_http_method_str, auth->second, *user))
+ switch (auth_message::verify(request.m_http_method_str, request.m_URI, auth->second, *user))
{
case auth_message::kPass:
return boost::none;
diff --git a/tests/unit_tests/http.cpp b/tests/unit_tests/http.cpp
index 3a4360d..ac81fb4 100644
--- a/tests/unit_tests/http.cpp
+++ b/tests/unit_tests/http.cpp
@@ -203,13 +203,14 @@ std::string write_fields(const fields& args)
return out;
}
-http::http_request_info make_request(const fields& args)
+http::http_request_info make_request(const fields& args, const std::string& uri = {})
{
std::string out{" DIGEST "};
write_fields(out, args);
http::http_request_info request{};
request.m_http_method_str = "NOP";
+ request.m_URI = uri;
request.m_header_info.m_etc_fields.push_back(
std::make_pair(u8"authorization", std::move(out))
);
@@ -496,7 +497,7 @@ TEST(HTTP_Server_Auth, MD5)
{u8"response", quoted(auth_code)},
{u8"uri", quoted(uri)},
{u8"username", quoted(user.username)}
- });
+ }, uri);
EXPECT_FALSE(bool(auth.get_response(request)));
@@ -547,7 +548,7 @@ TEST(HTTP_Server_Auth, MD5_sess)
{u8"response", quoted(auth_code)},
{u8"uri", quoted(uri)},
{u8"username", quoted(user.username)}
- });
+ }, uri);
EXPECT_FALSE(bool(auth.get_response(request)));
@@ -608,7 +609,7 @@ TEST(HTTP_Server_Auth, MD5_auth)
{u8"username", quoted(user.username)}
};
- const auto request = make_request(args);
+ const auto request = make_request(args, uri);
EXPECT_FALSE(bool(auth.get_response(request)));
for (unsigned i = 2; i < 20; ++i)
@@ -616,7 +617,7 @@ TEST(HTTP_Server_Auth, MD5_auth)
nc = get_nc(i);
args.at(u8"nc") = nc;
args.at(u8"response") = quoted(generate_auth());
- EXPECT_FALSE(auth.get_response(make_request(args)));
+ EXPECT_FALSE(auth.get_response(make_request(args, uri)));
}
const auto replay = auth.get_response(request);
@@ -676,7 +677,7 @@ TEST(HTTP_Server_Auth, MD5_sess_auth)
{u8"username", quoted(user.username)}
};
- const auto request = make_request(args);
+ const auto request = make_request(args, uri);
EXPECT_FALSE(bool(auth.get_response(request)));
for (unsigned i = 2; i < 20; ++i)
@@ -684,7 +685,7 @@ TEST(HTTP_Server_Auth, MD5_sess_auth)
nc = get_nc(i);
args.at(u8"nc") = nc;
args.at(u8"response") = quoted(generate_auth());
- EXPECT_FALSE(auth.get_response(make_request(args)));
+ EXPECT_FALSE(auth.get_response(make_request(args, uri)));
}
const auto replay = auth.get_response(request);
@@ -700,6 +701,50 @@ TEST(HTTP_Server_Auth, MD5_sess_auth)
}
+TEST(HTTP_Auth, RequestTargetBinding)
+{
+ const struct
+ {
+ const char* digest_uri;
+ const char* request_uri;
+ bool accepted;
+ } cases[] = {
+ {"/json_rpc", "/json_rpc", true},
+ {"/json_rpc", "/stop_daemon", false},
+ {"/json_rpc?foo=1", "/json_rpc?foo=2", false}
+ };
+
+ const http::login user{"foo", "bar"};
+ for (const auto& test : cases)
+ {
+ SCOPED_TRACE(test.request_uri);
+ http::http_server_auth server{user, rng};
+ http::http_client_auth client{user};
+ http::http_request_info request{};
+ request.m_http_method_str = "POST";
+ request.m_URI = test.request_uri;
+
+ auto challenge = server.get_response(request);
+ ASSERT_TRUE(bool(challenge));
+ ASSERT_TRUE(is_unauthorized(*challenge));
+ challenge->m_header_info.m_etc_fields = challenge->m_additional_fields;
+ ASSERT_EQ(http::http_client_auth::kSuccess, client.handle_401(*challenge));
+
+ auto authorization = client.get_auth_field(request.m_http_method_str, test.digest_uri);
+ ASSERT_TRUE(bool(authorization));
+ request.m_header_info.m_etc_fields.push_back(std::move(*authorization));
+
+ const auto response = server.get_response(request);
+ if (test.accepted)
+ EXPECT_FALSE(bool(response));
+ else
+ {
+ ASSERT_TRUE(bool(response));
+ EXPECT_TRUE(is_unauthorized(*response));
+ }
+ }
+}
+
TEST(HTTP_Auth, DogFood)
{
const auto add_auth_field = [] (http::http_request_info& request, http::http_client_auth& client)
Why this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.