Add new validation policy rejecting chunked requests framed with anything other than CRLFCRLF
According to the [RFC](https://datatracker.ietf.org/doc/html/rfc9112#name-chunked-transfer-coding), the ABNF for chunked coding states that both `last-chunk` _and_ `chunked_body` must end with `CR_LF`. This implies that `kValidTerm2` should not allowed in this case.
Protected by code is unused by any caller.
PiperOrigin-RevId: 789764991
diff --git a/quiche/balsa/balsa_enums.cc b/quiche/balsa/balsa_enums.cc
index 0567386..42b9e41 100644
--- a/quiche/balsa/balsa_enums.cc
+++ b/quiche/balsa/balsa_enums.cc
@@ -84,6 +84,8 @@
return "CHUNK_LENGTH_OVERFLOW";
case INVALID_CHUNK_EXTENSION:
return "INVALID_CHUNK_EXTENSION";
+ case INVALID_CHUNK_FRAMING:
+ return "INVALID_CHUNK_FRAMING";
case CALLED_BYTES_SPLICED_WHEN_UNSAFE_TO_DO_SO:
return "CALLED_BYTES_SPLICED_WHEN_UNSAFE_TO_DO_SO";
case CALLED_BYTES_SPLICED_AND_EXCEEDED_SAFE_SPLICE_AMOUNT:
diff --git a/quiche/balsa/balsa_enums.h b/quiche/balsa/balsa_enums.h
index 00e081a..f6cce3f 100644
--- a/quiche/balsa/balsa_enums.h
+++ b/quiche/balsa/balsa_enums.h
@@ -86,6 +86,7 @@
INVALID_CHUNK_LENGTH,
CHUNK_LENGTH_OVERFLOW,
INVALID_CHUNK_EXTENSION,
+ INVALID_CHUNK_FRAMING,
// Other errors.
CALLED_BYTES_SPLICED_WHEN_UNSAFE_TO_DO_SO,
diff --git a/quiche/balsa/balsa_frame.cc b/quiche/balsa/balsa_frame.cc
index 7d63d51..9c35642 100644
--- a/quiche/balsa/balsa_frame.cc
+++ b/quiche/balsa/balsa_frame.cc
@@ -1343,7 +1343,7 @@
}
const char c = *current;
- int32_t framing_found = HeaderFramingFound(c);
+ const int32_t framing_found = HeaderFramingFound(c);
if (framing_found != 0) {
// TODO(b/433557986) remove these code counts
if (framing_found == kValidTerm1 && is_request_) {
@@ -1356,9 +1356,20 @@
QUICHE_CODE_COUNT(balsa_frame_framing_found_valid_term2_response);
}
- // If we've found a "\r\n\r\n", then the message
- // is done.
+ // If we've found the end of the chunk, then we're done.
++current;
+
+ if (http_validation_policy()
+ .require_chunked_body_end_with_crlf_crlf &&
+ framing_found != kValidTerm1) {
+ // https://datatracker.ietf.org/doc/html/rfc9112#name-chunked-transfer-coding
+ // The ABNF for chunked coding states that both `last-chunk` _and_
+ // `chunked_body` must end with CR_LF, i.e. kValidTerm2 is not
+ // allowed.
+ HandleError(BalsaFrameEnums::INVALID_CHUNK_FRAMING);
+ return current - input;
+ }
+
parse_state_ = BalsaFrameEnums::MESSAGE_FULLY_READ;
visitor_->OnRawBodyInput(
absl::string_view(on_entry, current - on_entry));
diff --git a/quiche/balsa/balsa_frame_test.cc b/quiche/balsa/balsa_frame_test.cc
index 7caf5c4..b38827a 100644
--- a/quiche/balsa/balsa_frame_test.cc
+++ b/quiche/balsa/balsa_frame_test.cc
@@ -2048,6 +2048,82 @@
EXPECT_EQ(message_body, body_input);
EXPECT_EQ(message_body_data, body_data);
}
+
+// Validates that chunked requests terminated by \r\n\n are accepted.
+// Note that this does not comply with
+// https://datatracker.ietf.org/doc/html/rfc9112#name-chunked-transfer-coding
+// which states that both `last-chunk` _and_ `chunked_body` must end with CR_LF.
+TEST_F(HTTPBalsaFrameTest,
+ TransferEncodingChunkedFramesMessagesEndingWithCR_LF_LF) {
+ EXPECT_CALL(visitor_mock_, ProcessHeaders(_));
+ EXPECT_CALL(visitor_mock_, HeaderDone());
+ const std::string message1 =
+ "POST / HTTP/1.1\r\n"
+ "Host: 1.1.1.1\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n";
+ EXPECT_EQ(message1.size(),
+ balsa_frame_.ProcessInput(message1.data(), message1.size()));
+ EXPECT_FALSE(balsa_frame_.Error())
+ << BalsaFrameEnums::ErrorCodeToString(balsa_frame_.ErrorCode());
+
+ const std::string chunk_size = "2\r\n";
+ EXPECT_EQ(chunk_size.size(),
+ balsa_frame_.ProcessInput(chunk_size.data(), chunk_size.size()));
+ EXPECT_FALSE(balsa_frame_.Error())
+ << BalsaFrameEnums::ErrorCodeToString(balsa_frame_.ErrorCode());
+
+ EXPECT_CALL(visitor_mock_, OnBodyChunkInput("AA"));
+ EXPECT_CALL(visitor_mock_, MessageDone());
+ const std::string chunks =
+ "AA\r\n"
+ "0\r\n"
+ "\n";
+ ASSERT_EQ(chunks.size(),
+ balsa_frame_.ProcessInput(chunks.data(), chunks.size()));
+
+ EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode());
+ // According to the RFC, this should be false!
+ EXPECT_TRUE(balsa_frame_.MessageFullyRead());
+}
+
+TEST_F(
+ HTTPBalsaFrameTest,
+ TransferEncodingChunkedFramesMessagesEndingWithCR_LF_LFFailsWhenPolicySet) {
+ HttpValidationPolicy http_validation_policy{
+ .require_chunked_body_end_with_crlf_crlf = true};
+ balsa_frame_.set_http_validation_policy(http_validation_policy);
+ EXPECT_CALL(visitor_mock_, ProcessHeaders(_));
+ EXPECT_CALL(visitor_mock_, HeaderDone());
+ const std::string message1 =
+ "POST / HTTP/1.1\r\n"
+ "Host: 1.1.1.1\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "\r\n";
+ EXPECT_EQ(message1.size(),
+ balsa_frame_.ProcessInput(message1.data(), message1.size()));
+ EXPECT_FALSE(balsa_frame_.Error())
+ << BalsaFrameEnums::ErrorCodeToString(balsa_frame_.ErrorCode());
+
+ const std::string chunk_size = "2\r\n";
+ EXPECT_EQ(chunk_size.size(),
+ balsa_frame_.ProcessInput(chunk_size.data(), chunk_size.size()));
+ EXPECT_FALSE(balsa_frame_.Error())
+ << BalsaFrameEnums::ErrorCodeToString(balsa_frame_.ErrorCode());
+
+ EXPECT_CALL(visitor_mock_, OnBodyChunkInput("AA"));
+ EXPECT_CALL(visitor_mock_, MessageDone()).Times(0);
+ const std::string chunks =
+ "AA\r\n"
+ "0\r\n"
+ "\n";
+ ASSERT_EQ(chunks.size(),
+ balsa_frame_.ProcessInput(chunks.data(), chunks.size()));
+
+ EXPECT_EQ(BalsaFrameEnums::INVALID_CHUNK_FRAMING, balsa_frame_.ErrorCode());
+ EXPECT_FALSE(balsa_frame_.MessageFullyRead());
+}
+
TEST_F(HTTPBalsaFrameTest,
VisitorInvokedProperlyForRequestWithTransferEncodingAndTrailers) {
std::string message_headers =
diff --git a/quiche/balsa/http_validation_policy.h b/quiche/balsa/http_validation_policy.h
index 5c80f5c..80e0aec 100644
--- a/quiche/balsa/http_validation_policy.h
+++ b/quiche/balsa/http_validation_policy.h
@@ -87,6 +87,10 @@
// If true, the parser rejects messages where there is a lone LF not preceded
// by CR.
bool disallow_lone_lf_in_chunk_extension = true;
+
+ // If true, the parser rejects chunked messages that don't end with
+ // CR_LF_CR_LF.
+ bool require_chunked_body_end_with_crlf_crlf = false;
};
} // namespace quiche