Fix disallow_lone_cr_in_chunk_extension behavior. Reuse `last_char_was_slash_r_` member that was previously only used in BalsaFrame::ProcessHeaders(). PiperOrigin-RevId: 645032390
diff --git a/quiche/balsa/balsa_frame.cc b/quiche/balsa/balsa_frame.cc index 1e6080f..1272e10 100644 --- a/quiche/balsa/balsa_frame.cc +++ b/quiche/balsa/balsa_frame.cc
@@ -1111,6 +1111,7 @@ --current; parse_state_ = BalsaFrameEnums::READING_CHUNK_EXTENSION; + last_char_was_slash_r_ = false; visitor_->OnChunkLength(chunk_length_remaining_); continue; @@ -1129,11 +1130,22 @@ return current - input; } const char c = *current; - if (http_validation_policy_.disallow_lone_cr_in_chunk_extension && - c == '\r' && (current + 1 == end || *(current + 1) != '\n')) { - // We have a lone carriage return. - HandleError(BalsaFrameEnums::INVALID_CHUNK_EXTENSION); - return current - input; + if (http_validation_policy_.disallow_lone_cr_in_chunk_extension) { + // This is a CR character and the next one is not LF. + const bool cr_followed_by_non_lf = + c == '\r' && current + 1 < end && *(current + 1) != '\n'; + // The last character processed by the last ProcessInput() call was + // CR, this is the first character of the current ProcessInput() + // call, and it is not LF. + const bool previous_cr_followed_by_non_lf = + last_char_was_slash_r_ && current == input && c != '\n'; + if (cr_followed_by_non_lf || previous_cr_followed_by_non_lf) { + HandleError(BalsaFrameEnums::INVALID_CHUNK_EXTENSION); + return current - input; + } + if (current + 1 == end) { + last_char_was_slash_r_ = c == '\r'; + } } if (c == '\r' || c == '\n') { extensions_length = (extensions_start == current)
diff --git a/quiche/balsa/balsa_frame_test.cc b/quiche/balsa/balsa_frame_test.cc index 75d041f..76c02ec 100644 --- a/quiche/balsa/balsa_frame_test.cc +++ b/quiche/balsa/balsa_frame_test.cc
@@ -1728,9 +1728,10 @@ message.size()); } -// TODO(b/347710034): Fix behavior. -TEST_F(HTTPBalsaFrameTest, - ChunkExtensionLoneCarriageReturnDetectionAtBoundary) { +// Regression test for b/347710034: `disallow_lone_cr_in_chunk_extension` should +// not trigger false positive when "\r\n" terminating chunk length is separated +// into multiple calls to ProcessInput(). +TEST_F(HTTPBalsaFrameTest, ChunkExtensionCarriageReturnLineFeedAtBoundary) { balsa_frame_.set_http_validation_policy( HttpValidationPolicy{.disallow_lone_cr_in_chunk_extension = true}); EXPECT_CALL(visitor_mock_, ProcessHeaders(_)); @@ -1742,7 +1743,43 @@ balsa_frame_.ProcessInput(headers.data(), headers.size())); constexpr absl::string_view body1("3\r"); - ASSERT_EQ(1, balsa_frame_.ProcessInput(body1.data(), body1.size())); + ASSERT_EQ(body1.size(), + balsa_frame_.ProcessInput(body1.data(), body1.size())); + ASSERT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode()); + + constexpr absl::string_view body2( + "\nfoo\r\n" + "0\r\n\r\n"); + + EXPECT_CALL(visitor_mock_, OnBodyChunkInput("foo")); + EXPECT_CALL(visitor_mock_, MessageDone()); + ASSERT_EQ(body2.size(), + balsa_frame_.ProcessInput(body2.data(), body2.size())); + + EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.MessageFullyRead()); +} + +// A CR character followed by a non-LF character is detected even if separated +// into multiple calls to ProcessInput(). +TEST_F(HTTPBalsaFrameTest, ChunkExtensionLoneCarriageReturnAtBoundary) { + balsa_frame_.set_http_validation_policy( + HttpValidationPolicy{.disallow_lone_cr_in_chunk_extension = true}); + EXPECT_CALL(visitor_mock_, ProcessHeaders(_)); + EXPECT_CALL(visitor_mock_, HeaderDone()); + constexpr absl::string_view headers( + "POST / HTTP/1.1\r\n" + "transfer-encoding: chunked\r\n\r\n"); + ASSERT_EQ(headers.size(), + balsa_frame_.ProcessInput(headers.data(), headers.size())); + + constexpr absl::string_view body1("3\r"); + ASSERT_EQ(body1.size(), + balsa_frame_.ProcessInput(body1.data(), body1.size())); + ASSERT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode()); + + constexpr absl::string_view body2("a"); + EXPECT_EQ(0, balsa_frame_.ProcessInput(body2.data(), body2.size())); EXPECT_EQ(BalsaFrameEnums::INVALID_CHUNK_EXTENSION, balsa_frame_.ErrorCode()); }