epee: prevent log injection from malformed HTTP headers
What changed, and why it matters
This commit removes attacker-controlled HTTP header text from error and debug log messages in Monero's epee networking code. Before the change, a malicious peer or server could put fake log lines, terminal escape codes, or misleading text into HTTP headers, and those characters would be written verbatim into application logs. That could trick administrators, hide real alerts, or in some terminal setups manipulate display output. The patch replaces the raw header content in log messages with just the byte size of the offending data, so the malicious bytes are no longer echoed into logs.
Treat as a low-to-moderate defensive hardening fix. Review whether other log sites in epee and Monero still emit raw network input, and consider centralizing log sanitization or structured logging to prevent similar issues elsewhere. No emergency response is indicated, but operators should update to include the fix in normal release cycles.
Security signals we found
Commit title explicitly states 'prevent log injection from malformed HTTP headers'
Multiple log statements changed from printing raw header buffers to printing byte counts
Both client-side response parsing and server-side request parsing paths are touched
No CVE, advisory, or researcher attribution present in commit or supplied references
Evidence from the diff
The diff changes LOG_ERROR / CHECK_AND_ASSERT_MES / MERROR / LOG_PRINT_L3 calls in contrib/epee/include/net/http_client.h and contrib/epee/include/net/http_protocol_handler.inl so that they log the size of untrusted HTTP header buffers (e.g., m_cache_to_process.size(), m_header_cache.size(), pos) instead of the buffers themselves. This is a log-injection / log-forging mitigation: prior code interpolated raw request/response headers and values directly into log strings, allowing newline, carriage-return, ANSI escape sequences, or forged severity markers to be injected into log output. The patch is defensive and partial—it does not sanitize the data, it simply stops emitting it in these particular log paths. It does not change parsing logic or add input validation.
Changed components
contrib/epee/include/net/http_client.hcontrib/epee/include/net/http_protocol_handler.inlepee HTTP stream filter (client response handling)epee simple HTTP connection handler (server request handling)Inspect captured patch +23 / −23
diff --git a/contrib/epee/include/net/http_client.h b/contrib/epee/include/net/http_client.h
index 6e49f5f..fc1360a 100644
--- a/contrib/epee/include/net/http_client.h
+++ b/contrib/epee/include/net/http_client.h
@@ -541,7 +541,7 @@ namespace net_utils
}
if(!get_chunk_head(m_chunked_cache, m_len_in_remain, is_matched))
{
- LOG_ERROR("http_stream_filter::handle_chunked(*) Failed to get length from chunked head:" << m_chunked_cache);
+ LOG_ERROR("http_stream_filter::handle_chunked(*) Failed to get length from chunked head (" << m_chunked_cache.size() << " bytes)");
m_state = reciev_machine_state_error;
return false;
}
@@ -607,7 +607,7 @@ namespace net_utils
while(cur < m_cache_to_process.size())
{
const size_t line_end = m_cache_to_process.find('\n', cur);
- CHECK_AND_ASSERT_MES(line_end != std::string::npos, false, "http_stream_filter::parse_cached_header() invalid header in: " << m_cache_to_process);
+ CHECK_AND_ASSERT_MES(line_end != std::string::npos, false, "http_stream_filter::parse_cached_header() invalid header (" << m_cache_to_process.size() << " bytes)");
boost::string_view line(m_cache_to_process.data() + cur, line_end - cur);
cur = line_end + 1;
@@ -617,7 +617,7 @@ namespace net_utils
boost::string_view name;
boost::string_view value;
- CHECK_AND_ASSERT_MES(detail::parse_header_line(line, name, value), false, "http_stream_filter::parse_cached_header() invalid header in: " << m_cache_to_process);
+ CHECK_AND_ASSERT_MES(detail::parse_header_line(line, name, value), false, "http_stream_filter::parse_cached_header() invalid header (" << m_cache_to_process.size() << " bytes)");
std::string key(name.data(), name.size());
std::string val(value.data(), value.size());
@@ -651,25 +651,25 @@ namespace net_utils
{
//First line response, look like this: "HTTP/1.1 200 OK"
const char *ptr = m_header_cache.c_str();
- CHECK_AND_ASSERT_MES(!memcmp(ptr, "HTTP/", 5), false, "Invalid first response line: " + m_header_cache);
+ CHECK_AND_ASSERT_MES(!memcmp(ptr, "HTTP/", 5), false, "Invalid first response line (" << m_header_cache.size() << " header bytes)");
ptr += 5;
- CHECK_AND_ASSERT_MES(epee::misc_utils::parse::isdigit(*ptr), false, "Invalid first response line: " + m_header_cache);
+ CHECK_AND_ASSERT_MES(epee::misc_utils::parse::isdigit(*ptr), false, "Invalid first response line (" << m_header_cache.size() << " header bytes)");
unsigned long ul;
char *end;
ul = strtoul(ptr, &end, 10);
- CHECK_AND_ASSERT_MES(ul <= INT_MAX && *end =='.', false, "Invalid first response line: " + m_header_cache);
+ CHECK_AND_ASSERT_MES(ul <= INT_MAX && *end =='.', false, "Invalid first response line (" << m_header_cache.size() << " header bytes)");
m_response_info.m_http_ver_hi = ul;
ptr = end + 1;
- CHECK_AND_ASSERT_MES(epee::misc_utils::parse::isdigit(*ptr), false, "Invalid first response line: " + m_header_cache + ", ptr: " << ptr);
+ CHECK_AND_ASSERT_MES(epee::misc_utils::parse::isdigit(*ptr), false, "Invalid first response line (" << m_header_cache.size() << " header bytes)");
ul = strtoul(ptr, &end, 10);
- CHECK_AND_ASSERT_MES(ul <= INT_MAX && isblank(static_cast<unsigned char>(*end)), false, "Invalid first response line: " + m_header_cache + ", ptr: " << ptr);
+ CHECK_AND_ASSERT_MES(ul <= INT_MAX && isblank(static_cast<unsigned char>(*end)), false, "Invalid first response line (" << m_header_cache.size() << " header bytes)");
m_response_info.m_http_ver_lo = ul;
ptr = end + 1;
while (isblank(static_cast<unsigned char>(*ptr)))
++ptr;
- CHECK_AND_ASSERT_MES(epee::misc_utils::parse::isdigit(*ptr), false, "Invalid first response line: " + m_header_cache);
+ CHECK_AND_ASSERT_MES(epee::misc_utils::parse::isdigit(*ptr), false, "Invalid first response line (" << m_header_cache.size() << " header bytes)");
ul = strtoul(ptr, &end, 10);
- CHECK_AND_ASSERT_MES(ul >= 100 && ul <= 999 && isspace(static_cast<unsigned char>(*end)), false, "Invalid first response line: " + m_header_cache);
+ CHECK_AND_ASSERT_MES(ul >= 100 && ul <= 999 && isspace(static_cast<unsigned char>(*end)), false, "Invalid first response line (" << m_header_cache.size() << " header bytes)");
m_response_info.m_response_code = ul;
ptr = end;
// ignore the optional text, till the end
@@ -677,7 +677,7 @@ namespace net_utils
++ptr;
if (*ptr == '\r')
++ptr;
- CHECK_AND_ASSERT_MES(*ptr == '\n', false, "Invalid first response line: " << m_header_cache);
+ CHECK_AND_ASSERT_MES(*ptr == '\n', false, "Invalid first response line (" << m_header_cache.size() << " header bytes)");
++ptr;
m_header_cache.erase(0, ptr - m_header_cache.c_str());
@@ -709,7 +709,7 @@ namespace net_utils
std::string fake_str; //gcc error workaround
bool res = parse_header(m_response_info.m_header_info, m_header_cache);
- CHECK_AND_ASSERT_MES(res, false, "http_stream_filter::analize_cached_reply_header_and_invoke_state(): failed to anilize reply header: " << m_header_cache);
+ CHECK_AND_ASSERT_MES(res, false, "http_stream_filter::analize_cached_reply_header_and_invoke_state(): failed to anilize reply header (" << m_header_cache.size() << " bytes)");
set_reply_content_encoder();
@@ -731,7 +731,7 @@ namespace net_utils
string_tools::trim(m_response_info.m_header_info.m_transfer_encoding);
if(string_tools::compare_no_case(m_response_info.m_header_info.m_transfer_encoding, "chunked"))
{
- LOG_ERROR("Wrong Transfer-Encoding:" << m_response_info.m_header_info.m_transfer_encoding);
+ LOG_ERROR("Wrong Transfer-Encoding (" << m_response_info.m_header_info.m_transfer_encoding.size() << " bytes)");
m_state = reciev_machine_state_error;
return false;
}
@@ -744,7 +744,7 @@ namespace net_utils
//In the response header the length was specified
if(!content_len_valid)
{
- LOG_ERROR("http_stream_filter::analize_cached_reply_header_and_invoke_state(): Failed to get_len_from_content_lenght();, m_query_info.m_content_length="<<m_response_info.m_header_info.m_content_length);
+ LOG_ERROR("http_stream_filter::analize_cached_reply_header_and_invoke_state(): Failed to get_len_from_content_lenght() (" << m_response_info.m_header_info.m_content_length.size() << " bytes)");
m_state = reciev_machine_state_error;
return false;
}
@@ -770,7 +770,7 @@ namespace net_utils
}else
{ //Apparently there are no signs of the form of transfer, will receive data until the connection is closed
m_state = reciev_machine_state_error;
- MERROR("Undefined transfer type, consider http_body_transfer_connection_close method. header: " << m_header_cache);
+ MERROR("Undefined transfer type, consider http_body_transfer_connection_close method (" << m_header_cache.size() << " header bytes)");
return false;
}
return false;
@@ -801,7 +801,7 @@ namespace net_utils
boundary = result[7];
else
{
- LOG_ERROR("Failed to match boundary in content-type=" << head_info.m_content_type);
+ LOG_ERROR("Failed to match boundary in content-type (" << head_info.m_content_type.size() << " bytes)");
return false;
}
return true;
diff --git a/contrib/epee/include/net/http_protocol_handler.inl b/contrib/epee/include/net/http_protocol_handler.inl
index cf0d729..bf981e7 100644
--- a/contrib/epee/include/net/http_protocol_handler.inl
+++ b/contrib/epee/include/net/http_protocol_handler.inl
@@ -101,7 +101,7 @@ namespace net_utils
entry.m_etc_header_fields.push_back(std::pair<std::string, std::string>(result[field_etc_name], result[field_val]));
else
{
- LOG_ERROR("simple_http_connection_handler::parse_header() not matched last entry in:"<<std::string(it_current_bound, it_end));
+ LOG_ERROR("simple_http_connection_handler::parse_header() not matched last entry (" << std::distance(it_current_bound, it_end) << " bytes)");
}
it_current_bound = result[(int)result.size()-1].first;
@@ -122,7 +122,7 @@ namespace net_utils
if(!parse_header(it_begin, end_header_it+4, entry))
{
- LOG_ERROR("Failed to parse header:" << std::string(it_begin, end_header_it+2));
+ LOG_ERROR("Failed to parse header (" << std::distance(it_begin, end_header_it + 2) << " bytes)");
return false;
}
@@ -139,7 +139,7 @@ namespace net_utils
std::string boundary;
if(!match_boundary(content_type, boundary))
{
- MERROR("Failed to match boundary in content type: " << content_type);
+ MERROR("Failed to match boundary in content type (" << content_type.size() << " bytes)");
return false;
}
@@ -430,7 +430,7 @@ namespace net_utils
}else
{
m_state = http_state_error;
- LOG_ERROR_CC(m_conn_context, "simple_http_connection_handler<t_connection_context>::handle_invoke_query_line(): Failed to match first line: " << m_cache);
+ LOG_ERROR_CC(m_conn_context, "simple_http_connection_handler<t_connection_context>::handle_invoke_query_line(): Failed to match first line (" << m_cache.size() << " bytes)");
return false;
}
@@ -454,14 +454,14 @@ namespace net_utils
template<class t_connection_context>
bool simple_http_connection_handler<t_connection_context>::analize_cached_request_header_and_invoke_state(size_t pos)
{
- LOG_PRINT_L3("HTTP HEAD:\r\n" << m_cache.substr(0, pos));
+ LOG_PRINT_L3("HTTP HEAD: " << pos << " bytes");
m_query_info.m_full_request_buf_size = pos;
m_query_info.m_request_head.assign(m_cache.begin(), m_cache.begin()+pos);
if(!parse_cached_header(m_query_info.m_header_info, m_cache, pos))
{
- LOG_ERROR_CC(m_conn_context, "simple_http_connection_handler<t_connection_context>::analize_cached_request_header_and_invoke_state(): failed to anilize request header: " << m_cache);
+ LOG_ERROR_CC(m_conn_context, "simple_http_connection_handler<t_connection_context>::analize_cached_request_header_and_invoke_state(): failed to anilize request header (" << pos << " bytes)");
m_state = http_state_error;
return false;
}
@@ -477,7 +477,7 @@ namespace net_utils
m_body_transfer_type = http_body_transfer_measure;
if(!get_len_from_content_lenght(m_query_info.m_header_info.m_content_length, m_len_summary))
{
- LOG_ERROR_CC(m_conn_context, "simple_http_connection_handler<t_connection_context>::analize_cached_request_header_and_invoke_state(): Failed to get_len_from_content_lenght();, m_query_info.m_content_length="<<m_query_info.m_header_info.m_content_length);
+ LOG_ERROR_CC(m_conn_context, "simple_http_connection_handler<t_connection_context>::analize_cached_request_header_and_invoke_state(): Failed to get_len_from_content_lenght() (" << m_query_info.m_header_info.m_content_length.size() << " bytes)");
m_state = http_state_error;
return false;
}
Why this scored 57/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.