What changed, and why it matters
This commit introduces a new internal helper class for parsing and building HTTP headers in Bitcoin Core. It does not change any existing behavior or fix a known bug; it is purely new infrastructure code with tests. There is no indication this commit itself creates or fixes a security issue.
No immediate action required. Treat as routine refactoring/infrastructure. Review how this class is integrated into live HTTP request handling in subsequent commits, since any new parser can introduce subtle behavioral differences.
Security signals we found
New HTTP header parsing class with input validation
Rejects CR, LF, NUL characters in header lines
Rejects whitespace in header field names
Enforces 8192-byte total header size limit
No existing parser behavior is replaced or removed in this commit
Evidence from the diff
The patch adds an http_bitcoin::HTTPHeaders class in src/httpserver.h/src/httpserver.cpp with methods to read, write, find, remove, and stringify HTTP headers. It enforces RFC-compliant header syntax: rejects missing colons, empty names, whitespace in field names, embedded CR/LF/NUL, and total header size over 8 KiB. Unit tests cover valid and invalid cases. The class is not yet wired into live request handling in this commit.
Changed components
src/httpserver.cppsrc/httpserver.hsrc/test/httpserver_tests.cppInspect captured patch +247 / −0
diff --git a/src/httpserver.cpp b/src/httpserver.cpp
index 367e29ca..45824b7a 100644
--- a/src/httpserver.cpp
+++ b/src/httpserver.cpp
@@ -28,6 +28,7 @@
#include <optional>
#include <span>
#include <string>
+#include <string_view>
#include <thread>
#include <unordered_map>
#include <vector>
@@ -714,3 +715,97 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch)
pathHandlers.erase(i);
}
}
+
+namespace http_bitcoin {
+
+std::optional<std::string> HTTPHeaders::FindFirst(const std::string_view key) const
+{
+ for (const auto& item : m_headers) {
+ if (CaseInsensitiveEqual(key, item.first)) {
+ return item.second;
+ }
+ }
+ return std::nullopt;
+}
+
+std::vector<std::string_view> HTTPHeaders::FindAll(const std::string_view key) const
+{
+ std::vector<std::string_view> ret;
+ for (const auto& item : m_headers) {
+ if (CaseInsensitiveEqual(key, item.first)) {
+ ret.push_back(item.second);
+ }
+ }
+ return ret;
+}
+
+void HTTPHeaders::Write(std::string&& key, std::string&& value)
+{
+ m_headers.emplace_back(std::move(key), std::move(value));
+}
+
+void HTTPHeaders::RemoveAll(std::string_view key)
+{
+ auto moved = std::ranges::remove_if(m_headers, [key] (auto& pair) {
+ return CaseInsensitiveEqual(key, pair.first);
+ });
+ m_headers.erase(moved.begin(), moved.end());
+}
+
+bool HTTPHeaders::Read(util::LineReader& reader)
+{
+ // Headers https://httpwg.org/specs/rfc9110.html#rfc.section.6.3
+ // A sequence of Field Lines https://httpwg.org/specs/rfc9110.html#rfc.section.5.2
+ while (auto maybe_line = reader.ReadLine()) {
+ if (reader.Consumed() > MAX_HEADERS_SIZE) throw std::runtime_error("HTTP headers exceed size limit");
+
+ const std::string_view& line = *maybe_line;
+
+ // An empty line indicates end of the headers section https://www.rfc-editor.org/rfc/rfc2616#section-4
+ if (line.empty()) return true;
+
+ // "Field values containing CR, LF, or NUL characters are invalid and dangerous"
+ // https://httpwg.org/specs/rfc9110.html#rfc.section.5.5
+ // A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF)
+ // within any protocol elements other than the content.
+ // A recipient of such a bare CR MUST consider that element to be invalid...
+ // https://httpwg.org/specs/rfc9112.html#rfc.section.2.2
+ if (line.find_first_of("\r\n\0", 0, 3) != std::string_view::npos) throw std::runtime_error("Header contains invalid character");
+
+ // Header line must have at least one ":"
+ // keys are not allowed to have delimiters like ":" but values are
+ // https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.2
+ const size_t pos{line.find(':')};
+ if (pos == std::string_view::npos) throw std::runtime_error("HTTP header missing colon (:)");
+
+ // Whitespace is strictly not allowed in the field-name (key)
+ // https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.2
+ std::string_view key = line.substr(0, pos);
+ if (key.find_first_of(" \t\n\r\f\v") != std::string_view::npos) throw std::runtime_error("Invalid header field-name contains whitespace");
+ // Whitespace is optional in the value and can be trimmed
+ std::string value = util::TrimString(std::string_view(line).substr(pos + 1));
+
+ // Header keys are Field Names: https://httpwg.org/specs/rfc9110.html#fields.names
+ // which consist of "tokens": https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.2
+ // that can not be empty.
+ if (key.empty()) throw std::runtime_error("Empty HTTP header name");
+
+ Write(std::string(key), std::move(value));
+ }
+
+ return false;
+}
+
+std::string HTTPHeaders::Stringify() const
+{
+ std::string out;
+ for (const auto& [key, value] : m_headers) {
+ out += key + ": " + value + "\r\n";
+ }
+
+ // Headers are terminated by an empty line
+ out += "\r\n";
+
+ return out;
+}
+} // namespace http_bitcoin
diff --git a/src/httpserver.h b/src/httpserver.h
index 5d6ea3c8..a888979f 100644
--- a/src/httpserver.h
+++ b/src/httpserver.h
@@ -10,6 +10,9 @@
#include <span>
#include <string>
+#include <util/strencodings.h>
+#include <util/string.h>
+
namespace util {
class SignalInterrupt;
} // namespace util
@@ -187,4 +190,50 @@ private:
struct event* ev;
};
+namespace http_bitcoin {
+
+//! Maximum size of each headers line in an HTTP request,
+//! also the maximum size of all headers total.
+//! See https://github.com/bitcoin/bitcoin/pull/6859
+//! And libevent http.c evhttp_parse_headers_()
+constexpr size_t MAX_HEADERS_SIZE{8192};
+
+class HTTPHeaders
+{
+public:
+ /**
+ * @param[in] key The field-name of the header to search for
+ * @returns The value of the first header that matches the provided key
+ * nullopt if key is not found
+ */
+ std::optional<std::string> FindFirst(std::string_view key) const;
+ /**
+ * @param[in] key The field-name of the header to search for
+ * @returns Views into all values matching the provided key (valid while this object is alive)
+ */
+ std::vector<std::string_view> FindAll(std::string_view key) const;
+ void Write(std::string&& key, std::string&& value);
+ /**
+ * @param[in] key The field-name of the header to search for and delete
+ */
+ void RemoveAll(std::string_view key);
+ /**
+ * @returns false if LineReader hits the end of the buffer before reading an
+ * \n, meaning that we are still waiting on more data from the client.
+ * true after reading an entire HTTP headers section, terminated
+ * by an empty line and \n.
+ * @throws on exceeded read limit and on bad headers syntax (e.g. no ":" in a line)
+ */
+ bool Read(util::LineReader& reader);
+ std::string Stringify() const;
+
+private:
+ /**
+ * Headers can have duplicate field names, so we use a vector of key-value pairs instead of a map.
+ * https://httpwg.org/specs/rfc9110.html#rfc.section.5.2
+ */
+ std::vector<std::pair<std::string, std::string>> m_headers;
+};
+} // namespace http_bitcoin
+
#endif // BITCOIN_HTTPSERVER_H
diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp
index 75c25caf..d2d03f8a 100644
--- a/src/test/httpserver_tests.cpp
+++ b/src/test/httpserver_tests.cpp
@@ -5,9 +5,13 @@
#include <httpserver.h>
#include <test/util/common.h>
#include <test/util/setup_common.h>
+#include <util/string.h>
#include <boost/test/unit_test.hpp>
+using http_bitcoin::HTTPHeaders;
+using http_bitcoin::MAX_HEADERS_SIZE;
+
BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup)
BOOST_AUTO_TEST_CASE(test_query_parameters)
@@ -41,4 +45,103 @@ BOOST_AUTO_TEST_CASE(test_query_parameters)
uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2%";
BOOST_CHECK_EXCEPTION(GetQueryParameterFromUri(uri.c_str(), "p1"), std::runtime_error, HasReason("URI parsing failed, it likely contained RFC 3986 invalid characters"));
}
+
+BOOST_AUTO_TEST_CASE(http_headers_tests)
+{
+ {
+ // Writing response headers
+ HTTPHeaders headers{};
+ BOOST_CHECK(!headers.FindFirst("Cache-Control"));
+ headers.Write("Cache-Control", "no-cache");
+ // Check case-insensitive key matching
+ BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
+ BOOST_CHECK_EQUAL(headers.FindFirst("cache-control"), "no-cache");
+ // Additional values are appended, compared case-insensitive
+ headers.Write("cache-control", "max-age=60");
+ BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
+ BOOST_CHECK((headers.FindAll("Cache-Control") == std::vector<std::string_view>{"no-cache", "max-age=60"}));
+ // Add a few more
+ headers.Write("Pie", "apple");
+ headers.Write("Sandwich", "ham");
+ headers.Write("Coffee", "black");
+ BOOST_CHECK_EQUAL(headers.FindFirst("Pie"), "apple");
+ // Remove
+ headers.RemoveAll("Pie");
+ BOOST_CHECK(!headers.FindFirst("Pie"));
+ // Combine for transmission
+ std::string headers_string{headers.Stringify()};
+ BOOST_CHECK_EQUAL(headers_string, "Cache-Control: no-cache\r\n"
+ "cache-control: max-age=60\r\n"
+ "Sandwich: ham\r\n"
+ "Coffee: black\r\n"
+ "\r\n");
+ }
+ {
+ // Reading request headers captured from bitcoin-cli
+ constexpr std::string_view bitcoin_cli_headers = "Host: 127.0.0.1\r\n"
+ "Connection: close\r\n"
+ "Content-Type: application/json\r\n"
+ "Authorization: Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3\r\n"
+ "Content-Length: 46\r\n";
+ util::LineReader reader(bitcoin_cli_headers, /*max_line_length=*/MAX_HEADERS_SIZE);
+ HTTPHeaders headers{};
+ headers.Read(reader);
+ BOOST_CHECK_EQUAL(headers.FindFirst("Host"), "127.0.0.1");
+ BOOST_CHECK_EQUAL(headers.FindFirst("Connection"), "close");
+ BOOST_CHECK_EQUAL(headers.FindFirst("Content-Type"), "application/json");
+ BOOST_CHECK_EQUAL(headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3");
+ BOOST_CHECK_EQUAL(headers.FindFirst("Content-Length"), "46");
+ BOOST_CHECK(!headers.FindFirst("Pizza"));
+ }
+ // Ensure invalid headers are rejected
+ {
+ // missing a colon
+ util::LineReader reader{"key value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
+ BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
+ }
+ {
+ // missing a key
+ util::LineReader reader{":value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
+ BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Empty HTTP header name"});
+ }
+ {
+ // contains NUL
+ util::LineReader reader{std::string_view{"X-Custom: foo\0bar\n", 18}, /*max_line_length=*/MAX_HEADERS_SIZE};
+ BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
+ }
+ {
+ // contains bare \r (not followed by \n)
+ util::LineReader reader{std::string_view{"X-Custom: foo\rbar\n"}, /*max_line_length=*/MAX_HEADERS_SIZE};
+ BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
+ }
+ {
+ // contains odd \r preceding the expected CRLF
+ util::LineReader reader{"X-Custom: foo\r\r\n", /*max_line_length=*/MAX_HEADERS_SIZE};
+ BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
+ }
+ {
+ // key contains whitespace
+ util::LineReader reader{"key : value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
+ BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Invalid header field-name contains whitespace"});
+ }
+ {
+ // Individual lines are below MAX_HEADERS_SIZE but the total is excessive
+ std::string lines;
+ lines.reserve(820 * 10);
+ for (int i = 0; i < 820; ++i) {
+ lines.append("key:value\n");
+ }
+ std::string_view excessive_headers{lines};
+ BOOST_CHECK_GT(excessive_headers.size(), MAX_HEADERS_SIZE);
+ util::LineReader reader{excessive_headers, /*max_line_length=*/MAX_HEADERS_SIZE};
+ BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP headers exceed size limit"});
+ }
+ {
+ // Ok
+ util::LineReader reader{"key: value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
+ HTTPHeaders headers{};
+ headers.Read(reader);
+ BOOST_CHECK_EQUAL(headers.FindFirst("key"), "value");
+ }
+}
BOOST_AUTO_TEST_SUITE_END()
Why this scored 12/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.