http: parse server Content-Length strictly
What changed, and why it matters
This commit tightens how Monero's built-in HTTP server reads the 'Content-Length' header. Previously, the server would accept malformed values like '5abc' or '0x10' and silently use only the leading digits as the body length. Now it rejects the whole value unless it is a plain number. This kind of lenient parsing can cause request smuggling or desynchronization between the server and other HTTP components, which attackers can sometimes exploit to bypass security controls or poison caches.
Treat as a security-hardening fix and include in release notes. Users running Monero nodes or RPC services should upgrade to a release containing this commit. Operators should also ensure any reverse proxies or load balancers in front of Monero RPC enforce strict Content-Length parsing to avoid mixed-leniency request smuggling.
Security signals we found
HTTP request smuggling / desynchronization risk from lenient Content-Length parsing
Strict parsing now matches client-side behavior
RFC 7230 compliance improvement
No explicit CVE or security advisory referenced in commit
Evidence from the diff
The patch replaces a boost::regex-based extraction of leading digits with string_tools::get_xtype_from_string, which parses the entire Content-Length value strictly. The old code used a regex matching ‘\d+’ and lexical_cast on the first match, so trailing characters, hex prefixes, plus signs, spaces, or non-numeric values were ignored or accepted partially. The new behavior rejects any value that is not a pure decimal integer, aligning with RFC 7230 section 3.3.2 (‘1*DIGIT’). Unit tests confirm rejection of ‘5abc’, ‘0x10’, ‘+10’, ‘1 0’, and ‘abc’.
Changed components
contrib/epee/include/net/http_protocol_handler.inlMonero HTTP server / RPC listenerContent-Length header parserInspect captured patch +24 / −9
diff --git a/contrib/epee/include/net/http_protocol_handler.inl b/contrib/epee/include/net/http_protocol_handler.inl
index bd6e404..4b8c1f4 100644
--- a/contrib/epee/include/net/http_protocol_handler.inl
+++ b/contrib/epee/include/net/http_protocol_handler.inl
@@ -30,6 +30,7 @@
#include <boost/regex.hpp>
#include "http_protocol_handler.h"
#include "string_tools.h"
+#include "string_tools_lexical.h"
#include "file_io_utils.h"
#include "net_parse_helpers.h"
#include "time_helper.h"
@@ -599,15 +600,11 @@ namespace net_utils
template<class t_connection_context>
bool simple_http_connection_handler<t_connection_context>::get_len_from_content_lenght(const std::string& str, size_t& OUT len)
{
- static const boost::regex rexp_mach_field("\\d+", boost::regex::normal);
- std::string res;
- boost::smatch result;
- if(!(boost::regex_search( str, result, rexp_mach_field, boost::match_default) && result[0].matched))
- return false;
-
- try { len = boost::lexical_cast<size_t>(result[0]); }
- catch(...) { return false; }
- return true;
+ // Content-Length must be 1*DIGIT (RFC 7230 3.3.2). Parse the whole value
+ // strictly, matching the client side, so a field such as "5abc" or "0x10"
+ // is rejected rather than silently yielding a body length from its leading
+ // digits.
+ return string_tools::get_xtype_from_string(len, str);
}
//-----------------------------------------------------------------------------------
template<class t_connection_context>
diff --git a/tests/unit_tests/http.cpp b/tests/unit_tests/http.cpp
index 74fe412..5ecdd84 100644
--- a/tests/unit_tests/http.cpp
+++ b/tests/unit_tests/http.cpp
@@ -875,6 +875,24 @@ TEST(HTTP, Server_Parses_First_Header_After_Split_Request_Line)
EXPECT_EQ(body, capture.requests.front().m_body);
}
+TEST(HTTP, Server_Rejects_Malformed_Content_Length)
+{
+ const std::string body = "0123456789";
+ for (const char* len : {"5abc", "0x10", "+10", "1 0", "abc"})
+ {
+ const auto capture = feed_http_request(
+ "POST /json_rpc HTTP/1.1\r\n"
+ "Content-Length: " + std::string(len) + "\r\n"
+ "Host: example.com\r\n"
+ "\r\n" + body
+ );
+
+ ASSERT_EQ(1u, capture.results.size());
+ EXPECT_FALSE(capture.results.front()) << "accepted Content-Length: " << len;
+ EXPECT_TRUE(capture.requests.empty()) << "accepted Content-Length: " << len;
+ }
+}
+
TEST(HTTP, Server_Rejects_Malformed_First_Header)
{
const auto capture = feed_http_request(
Why this scored 51/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.