Skip to content

skip header fields containing CR/LF in http serialization#3387

Open
ubeddulla wants to merge 2 commits into
apache:masterfrom
ubeddulla:http-header-crlf
Open

skip header fields containing CR/LF in http serialization#3387
ubeddulla wants to merge 2 commits into
apache:masterfrom
ubeddulla:http-header-crlf

Conversation

@ubeddulla

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number:

Problem Summary:
MakeRawHttpRequest and MakeRawHttpResponse write every header field-name and field-value straight onto the wire (os << it->first << ": " << it->second << CRLF), and emit Content-Type from content_type() the same way. When one of those values carries a raw CR or LF (for example a value that an application forwards from an untrusted request), it closes the current header line and the following bytes are parsed as extra header fields or a body, i.e. HTTP request/response splitting. The inbound http_parser already refuses these control bytes in header fields, but the outbound serializers trusted them.

What is changed and the side effects?

Changed:

  1. Add HeaderHasCRLF() and, in both serializers, skip any header whose name or value contains CR or LF (a warning is logged).
  2. Apply the same check to the Content-Type line in both functions.

Side effects:

  • Performance effects: one find_first_of scan per emitted header; negligible.
  • Breaking backward compatibility: none for well-formed messages. A header field carrying a raw CR/LF is already malformed under RFC 7230 and is now dropped instead of being split onto the wire.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens outbound HTTP serialization in brpc to prevent HTTP request/response splitting by ensuring headers containing raw CR/LF bytes are not written onto the wire.

Changes:

  • Added a helper (HeaderHasCRLF) and used it to skip header fields whose name/value contains \r or \n during request/response serialization.
  • Applied the same CR/LF check to outbound Content-Type serialization.
  • Added a unit test to validate CR/LF injection is not emitted in serialized HTTP messages.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
test/brpc_http_message_unittest.cpp Adds a regression test ensuring CR/LF in header fields cannot inject additional serialized headers.
src/brpc/details/http_message.cpp Drops outbound header fields (and Content-Type) containing CR/LF to prevent HTTP splitting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/brpc/details/http_message.cpp Outdated
Comment on lines 657 to 660
if (!h->content_type().empty() && !HeaderHasCRLF(h->content_type())) {
os << "Content-Type: " << h->content_type()
<< BRPC_CRLF;
}
Comment thread src/brpc/details/http_message.cpp Outdated
Comment on lines 751 to 755
if (!is_invalid_content && !h->content_type().empty() &&
!HeaderHasCRLF(h->content_type())) {
os << "Content-Type: " << h->content_type()
<< BRPC_CRLF;
}
Comment on lines +663 to +667
if (HeaderHasCRLF(it->first) || HeaderHasCRLF(it->second)) {
LOG(ERROR) << "Skip header `" << it->first
<< "' containing CR/LF to avoid injection";
continue;
}
Comment on lines +758 to +762
if (HeaderHasCRLF(it->first) || HeaderHasCRLF(it->second)) {
LOG(ERROR) << "Skip header `" << it->first
<< "' containing CR/LF to avoid injection";
continue;
}
Comment on lines +706 to +729
TEST(HttpMessageTest, serialize_header_with_crlf_is_not_injected) {
// A header value carrying CR/LF must not terminate the current line and
// introduce extra header fields (HTTP request/response splitting).
butil::EndPoint ep;
ASSERT_EQ(0, butil::str2endpoint("127.0.0.1:1234", &ep));
butil::IOBuf content;
content.append("data");

brpc::HttpHeader req_header;
req_header.set_method(brpc::HTTP_METHOD_POST);
req_header.SetHeader("X-Evil", "a\r\nInjected: 1");
butil::IOBuf request;
MakeRawHttpRequest(&request, &req_header, ep, &content);
std::string request_str = request.to_string();
ASSERT_EQ(std::string::npos, request_str.find("Injected: 1")) << request_str;

brpc::HttpHeader res_header;
res_header.SetHeader("X-Evil", "a\r\nInjected: 1");
butil::IOBuf response;
content.append("data");
MakeRawHttpResponse(&response, &res_header, &content);
std::string response_str = response.to_string();
ASSERT_EQ(std::string::npos, response_str.find("Injected: 1")) << response_str;
}
Comment thread src/brpc/details/http_message.cpp Outdated
for (HttpHeader::HeaderIterator it = h->HeaderBegin();
it != h->HeaderEnd(); ++it) {
if (HeaderHasCRLF(it->first) || HeaderHasCRLF(it->second)) {
LOG(ERROR) << "Skip header `" << it->first

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be warning, not error

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, switched both to LOG(WARNING). Also wrapped the logged name in butil::ToPrintable so a CR/LF in the name can't inject newlines into the log.

Comment thread src/brpc/details/http_message.cpp Outdated
// such fields so a value forwarded from an untrusted source can't smuggle
// headers.
static bool HeaderHasCRLF(const std::string& s) {
return s.find_first_of("\r\n") != std::string::npos;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may affect performance, since MakeRawHttpRequest/Response are hot paths. You'd better add a flag to switch this on/off.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I put it behind -http_check_outbound_header_crlf (default on). The scan is a single find_first_of over the header name/value, so it's cheap, but anyone who never forwards untrusted header values can turn it off.

…name

Signed-off-by: ubeddulla khan <ubed@bugqore.com>
@ubeddulla

Copy link
Copy Markdown
Contributor Author

Pushed an update covering the review:

  • Gated the CR/LF check behind -http_check_outbound_header_crlf (default on) so it can be turned off on hot paths that never forward untrusted header values.
  • Changed the skip logs from ERROR to WARNING, and run the header name through butil::ToPrintable so a CR/LF in the name can't inject newlines into the log.
  • Content-Type now logs the same warning when skipped instead of dropping silently, in both request and response paths.
  • Added an explicit Content-Type CR/LF test for request and response, each with its own IOBuf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants