Add logging for various policy-controlled HTTP properties. PiperOrigin-RevId: 972596088
diff --git a/build/source_list.bzl b/build/source_list.bzl index f60d6dc..f85da00 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl
@@ -1004,6 +1004,7 @@ "balsa/framer_interface.h", "balsa/header_api.h", "balsa/header_properties.h", + "balsa/http_protocol_defects.h", "balsa/http_validation_policy.h", "balsa/noop_balsa_visitor.h", "balsa/simple_buffer.h",
diff --git a/build/source_list.gni b/build/source_list.gni index 610ae26..84571da 100644 --- a/build/source_list.gni +++ b/build/source_list.gni
@@ -1004,6 +1004,7 @@ "src/quiche/balsa/framer_interface.h", "src/quiche/balsa/header_api.h", "src/quiche/balsa/header_properties.h", + "src/quiche/balsa/http_protocol_defects.h", "src/quiche/balsa/http_validation_policy.h", "src/quiche/balsa/noop_balsa_visitor.h", "src/quiche/balsa/simple_buffer.h",
diff --git a/build/source_list.json b/build/source_list.json index 80c06ac..f504793 100644 --- a/build/source_list.json +++ b/build/source_list.json
@@ -1003,6 +1003,7 @@ "quiche/balsa/framer_interface.h", "quiche/balsa/header_api.h", "quiche/balsa/header_properties.h", + "quiche/balsa/http_protocol_defects.h", "quiche/balsa/http_validation_policy.h", "quiche/balsa/noop_balsa_visitor.h", "quiche/balsa/simple_buffer.h",
diff --git a/quiche/balsa/balsa_frame.cc b/quiche/balsa/balsa_frame.cc index f2fdba4..8f0ac4d 100644 --- a/quiche/balsa/balsa_frame.cc +++ b/quiche/balsa/balsa_frame.cc
@@ -394,6 +394,15 @@ http_validation_policy().sanitize_firstline_spaces, has_multiple_spaces, has_cr_tab); + if (has_multiple_spaces) { + QUICHE_CODE_COUNT(multiple_spaces_in_firstline_detected); + protocol_defects_.multiple_spaces_in_firstline = true; + } + if (has_cr_tab) { + QUICHE_CODE_COUNT(tab_or_cr_found_in_firstline_detected); + protocol_defects_.tab_or_cr_found_in_firstline = true; + } + if (!parse_success) { parse_state_ = BalsaFrameEnums::ERROR; HandleError(last_error_); @@ -418,6 +427,10 @@ if (is_request_) { const bool is_method_valid = header_properties::IsValidToken(part1); + if (!is_method_valid) { + QUICHE_CODE_COUNT(invalid_method_in_request_first_line_detected); + protocol_defects_.invalid_method_in_request_first_line = true; + } if (http_validation_policy().disallow_invalid_request_methods && !is_method_valid) { QUICHE_CODE_COUNT(disallow_invalid_request_methods_enforced); @@ -449,6 +462,8 @@ if (headers_->parsed_response_code_ < 100 || headers_->parsed_response_code_ > 599 || has_non_digit || has_leading_zero) { + QUICHE_CODE_COUNT(invalid_response_code_detected); + protocol_defects_.invalid_response_code = true; if (http_validation_policy().disallow_invalid_response_codes) { QUICHE_CODE_COUNT(disallow_invalid_response_codes_enforced); parse_state_ = BalsaFrameEnums::ERROR; @@ -552,6 +567,13 @@ // continuation) and continuation is allowed. HandleWarning(is_trailer ? BalsaFrameEnums::OBS_FOLD_IN_TRAILERS : BalsaFrameEnums::OBS_FOLD_IN_HEADERS); + if (is_trailer) { + QUICHE_CODE_COUNT(obs_fold_in_trailer_values_detected); + protocol_defects_.obs_fold_in_trailer_values = true; + } else { + QUICHE_CODE_COUNT(obs_fold_in_header_values_detected); + protocol_defects_.obs_fold_in_header_values = true; + } if (http_validation_policy().sanitize_obs_fold_in_header_values) { QUICHE_CODE_COUNT(sanitize_obs_fold_in_header_values_enforced); *has_continuation_lines = true; @@ -585,6 +607,8 @@ headers->header_lines_.back().has_continuation_line = header_has_continuation_line; if (current >= line_end) { + QUICHE_CODE_COUNT(header_missing_colon_detected); + protocol_defects_.header_missing_colon = true; if (http_validation_policy().require_header_colon) { QUICHE_CODE_COUNT(require_header_colon_enforced); HandleError(is_trailer ? BalsaFrameEnums::TRAILER_MISSING_COLON @@ -610,6 +634,10 @@ break; } + if (header_properties::IsInvalidHeaderKeyChar(c)) { + QUICHE_CODE_COUNT(header_name_contains_double_quote_detected); + protocol_defects_.header_name_contains_double_quote = true; + } // Generally invalid characters were found earlier. if (http_validation_policy().disallow_double_quote_in_header_name) { if (header_properties::IsInvalidHeaderKeyChar(c)) { @@ -627,6 +655,8 @@ } if (IsObsTextChar(c)) { + QUICHE_CODE_COUNT(obs_text_found_in_header_name_detected); + protocol_defects_.obs_text_found_in_header_name = true; if (http_validation_policy().disallow_obs_text_in_field_names) { QUICHE_CODE_COUNT(disallow_obs_text_in_field_names_enforced); HandleError(is_trailer @@ -645,6 +675,8 @@ // construct which is technically not allowed by the spec. // In strict mode, we do treat this invalid value-less key as an error. + QUICHE_CODE_COUNT(header_missing_colon_detected); + protocol_defects_.header_missing_colon = true; if (http_validation_policy().require_header_colon) { QUICHE_CODE_COUNT(require_header_colon_enforced); HandleError(is_trailer ? BalsaFrameEnums::TRAILER_MISSING_COLON @@ -737,6 +769,7 @@ headers_->transfer_encoding_is_chunked_ = false; return; } + protocol_defects_.unknown_transfer_encoding = true; if (http_validation_policy().validate_transfer_encoding) { HandleError(BalsaFrameEnums::UNKNOWN_TRANSFER_ENCODING); } @@ -757,6 +790,8 @@ return true; } if (*c == '\r' && c + 1 < stream_end && *(c + 1) != '\n') { + QUICHE_CODE_COUNT(lone_cr_in_request_headers_detected); + protocol_defects_.lone_cr_in_request_headers = true; if (http_validation_policy().disallow_lone_cr_in_request_headers) { QUICHE_CODE_COUNT(disallow_lone_cr_in_request_headers_enforced); return true; @@ -860,6 +895,8 @@ // Optionally, reject this per the RFC or simply keep one value. if (headers->content_length_status_ == BalsaHeadersEnums::VALID_CONTENT_LENGTH) { + QUICHE_CODE_COUNT(multiple_content_length_keys_detected); + protocol_defects_.multiple_content_length_keys = true; if (http_validation_policy().disallow_multiple_content_length) { QUICHE_CODE_COUNT(disallow_multiple_content_length_enforced); HandleError(BalsaFrameEnums::MULTIPLE_CONTENT_LENGTH_KEYS); @@ -870,6 +907,7 @@ } if (absl::EqualsIgnoreCase(key, kTransferEncoding)) { if (transfer_encoding_idx != 0) { + protocol_defects_.multiple_transfer_encoding_keys = true; if (http_validation_policy().validate_transfer_encoding) { QUICHE_CODE_COUNT(multiple_transfer_encoding_keys_rejected); HandleError(BalsaFrameEnums::MULTIPLE_TRANSFER_ENCODING_KEYS); @@ -888,6 +926,8 @@ } if (content_length_idx != 0 && transfer_encoding_idx != 0) { + QUICHE_CODE_COUNT(transfer_encoding_and_content_length_detected); + protocol_defects_.transfer_encoding_and_content_length = true; if (http_validation_policy().validate_transfer_encoding && http_validation_policy() .disallow_transfer_encoding_with_content_length) { @@ -1381,6 +1421,17 @@ HandleError(BalsaFrameEnums::INVALID_CHUNK_EXTENSION); return current - input; } + if (!found_semicolon || extension_is_empty) { + QUICHE_CODE_COUNT(missing_semicolon_in_chunk_extension_detected); + } + protocol_defects_.missing_semicolon_in_chunk_extension = + !found_semicolon || extension_is_empty; + if (found_semicolon && found_non_bws_before_semicolon) { + QUICHE_CODE_COUNT( + token_before_semicolon_in_chunk_extension_detected); + } + protocol_defects_.token_before_semicolon_in_chunk_extension = + found_semicolon && found_non_bws_before_semicolon; } chunk_length_character_extracted_ = false; @@ -1440,6 +1491,8 @@ // Right after the chunk should be a \r then a \n. if (c == '\r') { if (saw_slash_r_after_chunk_) { + QUICHE_CODE_COUNT(stray_data_after_chunk_detected); + protocol_defects_.stray_data_after_chunk = true; if (http_validation_policy().disallow_stray_data_after_chunk) { QUICHE_CODE_COUNT(disallow_stray_data_after_chunk_enforced); HandleError(BalsaFrameEnums::STRAY_DATA_AFTER_CHUNK); @@ -1453,6 +1506,8 @@ // Can't use last_char_was_slash_r_ because a \r might've been part // of the chunk data. if (!saw_slash_r_after_chunk_) { + QUICHE_CODE_COUNT(stray_data_after_chunk_detected); + protocol_defects_.stray_data_after_chunk = true; if (http_validation_policy().disallow_stray_data_after_chunk) { QUICHE_CODE_COUNT(disallow_stray_data_after_chunk_enforced); HandleError(BalsaFrameEnums::STRAY_DATA_AFTER_CHUNK); @@ -1463,6 +1518,8 @@ } break; } else { + QUICHE_CODE_COUNT(stray_data_after_chunk_detected); + protocol_defects_.stray_data_after_chunk = true; if (http_validation_policy().disallow_stray_data_after_chunk) { QUICHE_CODE_COUNT(disallow_stray_data_after_chunk_enforced); HandleError(BalsaFrameEnums::STRAY_DATA_AFTER_CHUNK); @@ -1502,6 +1559,9 @@ ++current; if (framing_found != kValidTerm1) { + QUICHE_CODE_COUNT( + chunked_body_does_not_end_with_crlf_crlf_detected); + protocol_defects_.chunked_body_does_not_end_with_crlf_crlf = true; if (http_validation_policy() .require_chunked_body_end_with_crlf_crlf) { QUICHE_CODE_COUNT(
diff --git a/quiche/balsa/balsa_frame.h b/quiche/balsa/balsa_frame.h index d71411c..99b1e2f 100644 --- a/quiche/balsa/balsa_frame.h +++ b/quiche/balsa/balsa_frame.h
@@ -21,6 +21,8 @@ #include "quiche/common/platform/api/quiche_flag_utils.h" #include "quiche/common/platform/api/quiche_logging.h" +#include "quiche/balsa/http_protocol_defects.h" + namespace quiche { namespace test { @@ -194,6 +196,10 @@ bool is_valid_target_uri() const { return is_valid_target_uri_; } + const HttpProtocolDefects& protocol_defects() const { + return protocol_defects_; + } + protected: inline BalsaHeadersEnums::ContentLengthStatus ProcessContentLengthLine( size_t line_idx, size_t* length); @@ -322,6 +328,7 @@ // Specific to parsing of chunk extensions. bool in_quote_ : 1; bool is_escaped_ : 1; + HttpProtocolDefects protocol_defects_; }; } // namespace quiche
diff --git a/quiche/balsa/balsa_frame_test.cc b/quiche/balsa/balsa_frame_test.cc index 962ec61..e8ceb16 100644 --- a/quiche/balsa/balsa_frame_test.cc +++ b/quiche/balsa/balsa_frame_test.cc
@@ -747,18 +747,26 @@ if (c >= 127) { EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::BALSA_NO_ERROR); + EXPECT_EQ(framer.protocol_defects().invalid_method_in_request_first_line, + true); EXPECT_FALSE(header_properties::IsValidToken(headers.request_method())); EXPECT_EQ(headers.request_method(), absl::StrCat("G", char_str, "ET")); } else if (absl::ascii_isspace(c)) { + EXPECT_EQ(framer.protocol_defects().invalid_method_in_request_first_line, + false); EXPECT_TRUE(header_properties::IsValidToken(headers.request_method())); EXPECT_EQ(headers.request_method(), "G"); } else if (absl::ascii_iscntrl(c)) { EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::BALSA_NO_ERROR); + EXPECT_EQ(framer.protocol_defects().invalid_method_in_request_first_line, + false); EXPECT_TRUE(header_properties::IsValidToken(headers.request_method())); EXPECT_EQ(headers.request_method(), "G"); } else if (absl::ascii_isgraph(c)) { EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::BALSA_NO_ERROR); bool char_is_tchar = header_properties::IsValidToken(char_str); + EXPECT_EQ(framer.protocol_defects().invalid_method_in_request_first_line, + !char_is_tchar); EXPECT_EQ(header_properties::IsValidToken(headers.request_method()), char_is_tchar); EXPECT_EQ(headers.request_method(), absl::StrCat("G", char_str, "ET")); @@ -879,6 +887,8 @@ framer.ProcessInput(input.data(), input.size()); EXPECT_EQ(headers.first_line(), tc.parsed); EXPECT_EQ(framer.ErrorCode(), tc.expected_error); + EXPECT_EQ(framer.protocol_defects().tab_or_cr_found_in_firstline, + tc.expected_tab_or_cr_defect); } } @@ -944,6 +954,55 @@ FirstLineParsedCorrectlyHelper(response_tokens, 4242, false, " \t \t "); } +TEST(HTTPBalsaFrame, LargeAndSmallStatusCodes) { + struct TestCase { + const absl::string_view status_code; + const BalsaFrameEnums::ErrorCode expected_error; + const bool expected_invalid_response_code; + }; + std::vector<TestCase> cases = { + {"0", BalsaFrameEnums::BALSA_NO_ERROR, true}, + {"99", BalsaFrameEnums::BALSA_NO_ERROR, true}, + {"100", BalsaFrameEnums::BALSA_NO_ERROR, false}, + {"200", BalsaFrameEnums::BALSA_NO_ERROR, false}, + {"599", BalsaFrameEnums::BALSA_NO_ERROR, false}, + {"600", BalsaFrameEnums::BALSA_NO_ERROR, true}, + {"1000", BalsaFrameEnums::BALSA_NO_ERROR, true}, + {"65740", BalsaFrameEnums::BALSA_NO_ERROR, true}, + {"0200", BalsaFrameEnums::BALSA_NO_ERROR, true}, + {"+200", BalsaFrameEnums::BALSA_NO_ERROR, true}, + {"200A", BalsaFrameEnums::FAILED_CONVERTING_STATUS_CODE_TO_INT, false}, + {"99999999999999999999999", + BalsaFrameEnums::FAILED_CONVERTING_STATUS_CODE_TO_INT, false}}; + for (const TestCase& tcase : cases) { + BalsaHeaders headers; + BalsaFrame framer; + framer.set_is_request(false); + framer.set_balsa_headers(&headers); + std::string firstline = absl::StrFormat( + "HTTP/1.1 %s OK\r\n" + "Content-Length: 0\r\n" + "\r\n", + tcase.status_code); + SCOPED_TRACE(absl::StrCat("Input: ", absl::CEscape(firstline))); + + if (tcase.expected_error == BalsaFrameEnums::BALSA_NO_ERROR) { + EXPECT_EQ(framer.ProcessInput(firstline.data(), firstline.size()), + firstline.size()); + EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::BALSA_NO_ERROR); + EXPECT_TRUE(framer.MessageFullyRead()); + } else { + EXPECT_LT(framer.ProcessInput(firstline.data(), firstline.size()), + firstline.size()); + EXPECT_EQ(framer.ErrorCode(), tcase.expected_error); + EXPECT_FALSE(framer.MessageFullyRead()); + } + + EXPECT_EQ(framer.protocol_defects().invalid_response_code, + tcase.expected_invalid_response_code); + } +} + TEST(HTTPBalsaFrame, LargeAndSmallStatusCodesWithPolicy) { struct TestCase { const absl::string_view status_code; @@ -1130,6 +1189,7 @@ CreateMessage("GET / \rHTTP/1.1\r\n", headers, 2, ":", "\r\n", ""); framer.ProcessInput(message.data(), message.size()); EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::INVALID_HEADER_CHARACTER); + EXPECT_TRUE(framer.protocol_defects().lone_cr_in_request_headers); } // Test that lone '\r' detection works correctly in the firstline @@ -1168,6 +1228,7 @@ EXPECT_EQ(message2.size(), framer.ProcessInput(message2.data(), message2.size())); EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::INVALID_HEADER_CHARACTER); + EXPECT_TRUE(framer.protocol_defects().lone_cr_in_request_headers); } TEST(HTTPBalsaFrame, CarriageReturnIllegalInHeaderKey) { @@ -1183,6 +1244,7 @@ CreateMessage("GET / HTTP/1.1\r\n", headers, 1, ":", "\r\n", ""); framer.ProcessInput(message.data(), message.size()); EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::INVALID_HEADER_NAME_CHARACTER); + EXPECT_TRUE(framer.protocol_defects().lone_cr_in_request_headers); } TEST(HTTPBalsaFrame, ResponseLinesParsedProperly) { @@ -1474,6 +1536,44 @@ EXPECT_FALSE(balsa_frame_.Error()); } +TEST_F(HTTPBalsaFrameTest, BothTransferEncodingAndContentLengthAllowed) { + std::string message = + "GET / HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Length: 5\r\n" + "\r\n"; + + balsa_frame_.ProcessInput(message.data(), message.size()); + EXPECT_FALSE(balsa_frame_.Error()); + EXPECT_TRUE( + balsa_frame_.protocol_defects().transfer_encoding_and_content_length); + // Content-Length is provided to callers when Transfer-Encoding is present. + EXPECT_EQ(headers_.content_length(), 5); +} + +TEST_F(HTTPBalsaFrameTest, BothTransferEncodingAndContentLengthDisallowed) { + HttpValidationPolicy http_validation_policy; + http_validation_policy.disallow_transfer_encoding_with_content_length = true; + balsa_frame_.set_http_validation_policy(http_validation_policy); + + std::string message = + "GET / HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Length: 5\r\n" + "\r\n"; + + EXPECT_CALL( + visitor_mock_, + HandleError(BalsaFrameEnums::BOTH_TRANSFER_ENCODING_AND_CONTENT_LENGTH)); + + balsa_frame_.ProcessInput(message.data(), message.size()); + EXPECT_TRUE(balsa_frame_.Error()); + EXPECT_EQ(balsa_frame_.ErrorCode(), + BalsaFrameEnums::BOTH_TRANSFER_ENCODING_AND_CONTENT_LENGTH); + EXPECT_TRUE( + balsa_frame_.protocol_defects().transfer_encoding_and_content_length); +} + TEST_F(HTTPBalsaFrameTest, VisitorInvokedProperlyForPermittedMissingContentLength) { std::string message = @@ -2037,6 +2137,39 @@ EXPECT_FALSE(balsa_frame_.Error()); } +TEST_F(HTTPBalsaFrameTest, EmptyChunkExtensionDetectedAsDefect) { + std::string headers = + "POST / HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"; + + // Empty extension + const absl::string_view chunks( + "8;\r\n" + "deadbeef\r\n" + "0\r\n\r\n"); + + ASSERT_EQ(headers.size(), + balsa_frame_.ProcessInput(headers.data(), headers.size())); + + balsa_frame_.set_balsa_visitor(&visitor_mock_); + { + InSequence s1; + EXPECT_CALL(visitor_mock_, OnChunkLength(8)); + EXPECT_CALL(visitor_mock_, OnChunkExtensionInput(";")); + EXPECT_CALL(visitor_mock_, OnBodyChunkInput("deadbeef")); + EXPECT_CALL(visitor_mock_, OnChunkLength(0)); + EXPECT_CALL(visitor_mock_, OnChunkExtensionInput("")); + } + + EXPECT_EQ(chunks.size(), + balsa_frame_.ProcessInput(chunks.data(), chunks.size())); + + EXPECT_FALSE(balsa_frame_.Error()); + EXPECT_TRUE( + balsa_frame_.protocol_defects().missing_semicolon_in_chunk_extension); +} + TEST_F(HTTPBalsaFrameTest, InvalidChunkExtensionWithCarriageReturn) { balsa_frame_.set_http_validation_policy( HttpValidationPolicy{.disallow_lone_cr_in_chunk_extension = true}); @@ -2247,6 +2380,7 @@ EXPECT_TRUE(balsa_frame_.MessageFullyRead()); EXPECT_FALSE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::STRAY_DATA_AFTER_CHUNK, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().stray_data_after_chunk); EXPECT_EQ(message_body, body_input); EXPECT_EQ(message_body_data, body_data); @@ -2269,6 +2403,7 @@ balsa_frame_.ProcessInput(body1.data(), body1.size())); EXPECT_TRUE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::STRAY_DATA_AFTER_CHUNK, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().stray_data_after_chunk); } // A LF character preceded by CR is allowed even if separated into multiple @@ -2331,6 +2466,8 @@ EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode()); // According to the RFC, this should be false! EXPECT_TRUE(balsa_frame_.MessageFullyRead()); + EXPECT_TRUE( + balsa_frame_.protocol_defects().chunked_body_does_not_end_with_crlf_crlf); } TEST_F( @@ -2368,6 +2505,8 @@ EXPECT_EQ(BalsaFrameEnums::INVALID_CHUNK_FRAMING, balsa_frame_.ErrorCode()); EXPECT_FALSE(balsa_frame_.MessageFullyRead()); + EXPECT_TRUE( + balsa_frame_.protocol_defects().chunked_body_does_not_end_with_crlf_crlf); } TEST_F(HTTPBalsaFrameTest, FirstlinesWithMultipleSpacesAllowed) { @@ -2387,6 +2526,7 @@ << BalsaFrameEnums::ErrorCodeToString(balsa_frame_.ErrorCode()); EXPECT_EQ("GET / HTTP/1.1", headers_.first_line()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_spaces_in_firstline); } TEST_F(HTTPBalsaFrameTest, FirstlinesWithMultipleSpacesRejected) { @@ -2405,6 +2545,7 @@ EXPECT_TRUE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::MULTIPLE_SPACES_IN_REQUEST_LINE, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_spaces_in_firstline); } TEST_F(HTTPBalsaFrameTest, FirstlinesWithMultipleSpacesSanitized) { @@ -2426,6 +2567,7 @@ << BalsaFrameEnums::ErrorCodeToString(balsa_frame_.ErrorCode()); EXPECT_EQ("GET / HTTP/1.1", headers_.first_line()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_spaces_in_firstline); } TEST_F(HTTPBalsaFrameTest, ResponseFirstlinesWithMultipleSpacesAllowed) { @@ -2446,6 +2588,7 @@ << BalsaFrameEnums::ErrorCodeToString(balsa_frame_.ErrorCode()); EXPECT_EQ("HTTP/1.1 200 OK", headers_.first_line()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_spaces_in_firstline); } TEST_F(HTTPBalsaFrameTest, ResponseFirstlinesWithMultipleSpacesRejected) { @@ -2465,6 +2608,7 @@ EXPECT_TRUE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::MULTIPLE_SPACES_IN_STATUS_LINE, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_spaces_in_firstline); } TEST_F(HTTPBalsaFrameTest, ResponseFirstlinesWithMultipleSpacesSanitized) { @@ -2487,6 +2631,7 @@ << BalsaFrameEnums::ErrorCodeToString(balsa_frame_.ErrorCode()); EXPECT_EQ("HTTP/1.1 200 OK", headers_.first_line()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_spaces_in_firstline); } TEST_F(HTTPBalsaFrameTest, @@ -3694,6 +3839,7 @@ EXPECT_TRUE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::MULTIPLE_CONTENT_LENGTH_KEYS, balsa_frame_.ErrorCode()); + EXPECT_FALSE(balsa_frame_.protocol_defects().multiple_content_length_keys); } TEST_F(HTTPBalsaFrameTest, TwoDifferentContentLengthHeadersIsAnError) { @@ -3707,6 +3853,7 @@ EXPECT_TRUE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::MULTIPLE_CONTENT_LENGTH_KEYS, balsa_frame_.ErrorCode()); + EXPECT_FALSE(balsa_frame_.protocol_defects().multiple_content_length_keys); } TEST_F(HTTPBalsaFrameTest, TwoSameContentLengthHeadersIsNotAnError) { @@ -3718,6 +3865,7 @@ "1"; balsa_frame_.ProcessInput(header.data(), header.size()); EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_content_length_keys); EXPECT_FALSE(balsa_frame_.Error()); balsa_frame_.ProcessInput(header.data(), header.size()); EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode()); @@ -3741,6 +3889,7 @@ EXPECT_TRUE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::MULTIPLE_CONTENT_LENGTH_KEYS, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_content_length_keys); } TEST_F(HTTPBalsaFrameTest, ChunkedTransferEncodingWithContentLength) { @@ -3775,6 +3924,7 @@ EXPECT_TRUE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::MULTIPLE_TRANSFER_ENCODING_KEYS, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_transfer_encoding_keys); } TEST_F(HTTPBalsaFrameTest, AcceptTwoTransferEncodingHeaders) { @@ -3793,6 +3943,7 @@ EXPECT_FALSE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().multiple_transfer_encoding_keys); } TEST_F(HTTPBalsaFrameTest, TwoTransferEncodingTokensIsAnError) { @@ -3806,6 +3957,7 @@ EXPECT_TRUE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::UNKNOWN_TRANSFER_ENCODING, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().unknown_transfer_encoding); } TEST_F(HTTPBalsaFrameTest, AcceptTwoTransferEncodingTokens) { @@ -3823,6 +3975,7 @@ EXPECT_FALSE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().unknown_transfer_encoding); } TEST_F(HTTPBalsaFrameTest, UnknownTransferEncodingTokenIsAnError) { @@ -3836,6 +3989,7 @@ EXPECT_TRUE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::UNKNOWN_TRANSFER_ENCODING, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().unknown_transfer_encoding); } TEST_F(HTTPBalsaFrameTest, AcceptUnknownTransferEncodingToken) { @@ -3853,6 +4007,7 @@ EXPECT_FALSE(balsa_frame_.Error()); EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode()); + EXPECT_TRUE(balsa_frame_.protocol_defects().unknown_transfer_encoding); } TEST_F(HTTPBalsaFrameTest, MissingContentLength) { @@ -4966,6 +5121,7 @@ EXPECT_EQ(message.size(), balsa_frame_.ProcessInput(message.data(), message.size())); EXPECT_FALSE(balsa_frame_.Error()); + EXPECT_TRUE(balsa_frame_.protocol_defects().obs_fold_in_header_values); } // A.k.a., ObsFoldDisallowed. @@ -5090,6 +5246,7 @@ EXPECT_EQ(message.size(), balsa_frame_.ProcessInput(message.data(), message.size())); EXPECT_FALSE(balsa_frame_.Error()); + EXPECT_FALSE(balsa_frame_.protocol_defects().obs_text_found_in_header_name); } TEST_F(HTTPBalsaFrameTest, HeaderFieldNameWithObsTextButPolicyDisabled) { @@ -5112,6 +5269,7 @@ EXPECT_EQ(message.size(), balsa_frame_.ProcessInput(message.data(), message.size())); EXPECT_FALSE(balsa_frame_.Error()); + EXPECT_TRUE(balsa_frame_.protocol_defects().obs_text_found_in_header_name); } TEST_F(HTTPBalsaFrameTest, HeaderFieldNameWithObsTextAndPolicyEnabled) {
diff --git a/quiche/balsa/http_protocol_defects.h b/quiche/balsa/http_protocol_defects.h new file mode 100644 index 0000000..cfbdab6 --- /dev/null +++ b/quiche/balsa/http_protocol_defects.h
@@ -0,0 +1,184 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef QUICHE_BALSA_HTTP_PROTOCOL_DEFECTS_H_ +#define QUICHE_BALSA_HTTP_PROTOCOL_DEFECTS_H_ + +#include "absl/strings/string_view.h" +#include "quiche/common/platform/api/quiche_export.h" +#include "quiche/common/quiche_callbacks.h" + +namespace quiche { + +class QUICHE_EXPORT HttpProtocolDefects { + public: + bool invalid_method_in_request_first_line = false; + bool multiple_content_length_keys = false; + bool missing_semicolon_in_chunk_extension = false; + bool token_before_semicolon_in_chunk_extension = false; + bool invalid_response_code = false; + bool multiple_spaces_in_firstline = false; + bool obs_fold_in_header_values = false; + bool obs_fold_in_trailer_values = false; + bool chunked_body_does_not_end_with_crlf_crlf = false; + bool stray_data_after_chunk = false; + bool lone_cr_in_request_headers = false; + bool header_name_contains_double_quote = false; + bool header_missing_colon = false; + bool tab_or_cr_found_in_firstline = false; + bool obs_text_found_in_header_name = false; + bool transfer_encoding_and_content_length = false; + bool unknown_transfer_encoding = false; + bool multiple_transfer_encoding_keys = false; + + void Merge(const HttpProtocolDefects& other) { + invalid_method_in_request_first_line |= + other.invalid_method_in_request_first_line; + multiple_content_length_keys |= other.multiple_content_length_keys; + missing_semicolon_in_chunk_extension |= + other.missing_semicolon_in_chunk_extension; + token_before_semicolon_in_chunk_extension |= + other.token_before_semicolon_in_chunk_extension; + invalid_response_code |= other.invalid_response_code; + multiple_spaces_in_firstline |= other.multiple_spaces_in_firstline; + obs_fold_in_header_values |= other.obs_fold_in_header_values; + obs_fold_in_trailer_values |= other.obs_fold_in_trailer_values; + chunked_body_does_not_end_with_crlf_crlf |= + other.chunked_body_does_not_end_with_crlf_crlf; + stray_data_after_chunk |= other.stray_data_after_chunk; + lone_cr_in_request_headers |= other.lone_cr_in_request_headers; + header_name_contains_double_quote |= + other.header_name_contains_double_quote; + header_missing_colon |= other.header_missing_colon; + tab_or_cr_found_in_firstline |= other.tab_or_cr_found_in_firstline; + obs_text_found_in_header_name |= other.obs_text_found_in_header_name; + transfer_encoding_and_content_length |= + other.transfer_encoding_and_content_length; + unknown_transfer_encoding |= other.unknown_transfer_encoding; + multiple_transfer_encoding_keys |= other.multiple_transfer_encoding_keys; + } + + void ForEachDefect( + UnretainedCallback<void(absl::string_view)> callback) const { + if (invalid_method_in_request_first_line) { + callback("invalid_method_in_request_first_line"); + } + if (multiple_content_length_keys) { + callback("multiple_content_length_keys"); + } + if (missing_semicolon_in_chunk_extension) { + callback("missing_semicolon_in_chunk_extension"); + } + if (token_before_semicolon_in_chunk_extension) { + callback("token_before_semicolon_in_chunk_extension"); + } + if (invalid_response_code) { + callback("invalid_response_code"); + } + if (multiple_spaces_in_firstline) { + callback("multiple_spaces_in_firstline"); + } + if (obs_fold_in_header_values) { + callback("obs_fold_in_header_values"); + } + if (obs_fold_in_trailer_values) { + callback("obs_fold_in_trailer_values"); + } + if (chunked_body_does_not_end_with_crlf_crlf) { + callback("chunked_body_does_not_end_with_crlf_crlf"); + } + if (stray_data_after_chunk) { + callback("stray_data_after_chunk"); + } + if (lone_cr_in_request_headers) { + callback("lone_cr_in_request_headers"); + } + if (header_name_contains_double_quote) { + callback("header_name_contains_double_quote"); + } + if (header_missing_colon) { + callback("header_missing_colon"); + } + if (tab_or_cr_found_in_firstline) { + callback("tab_or_cr_found_in_firstline"); + } + if (obs_text_found_in_header_name) { + callback("obs_text_found_in_header_name"); + } + if (transfer_encoding_and_content_length) { + callback("transfer_encoding_and_content_length"); + } + if (unknown_transfer_encoding) { + callback("unknown_transfer_encoding"); + } + if (multiple_transfer_encoding_keys) { + callback("multiple_transfer_encoding_keys"); + } + } + + // T should ideally be strongly typed as + // logs::gfe::GfeLog::HttpProtocolDefects but since Quiche can only depend on + // targets that are compatible with non_prod, a template is used instead. + template <typename T> + void PopulateLogDefects(T& log_defects) const { + if (invalid_method_in_request_first_line) { + log_defects.set_invalid_method_in_request_first_line(true); + } + if (multiple_content_length_keys) { + log_defects.set_multiple_content_length_keys_detected(true); + } + if (missing_semicolon_in_chunk_extension) { + log_defects.set_missing_semicolon_in_chunk_extension(true); + } + if (token_before_semicolon_in_chunk_extension) { + log_defects.set_token_before_semicolon_in_chunk_extension(true); + } + if (invalid_response_code) { + log_defects.set_invalid_response_code(true); + } + if (multiple_spaces_in_firstline) { + log_defects.set_multiple_spaces_found_in_first_line(true); + } + if (obs_fold_in_header_values) { + log_defects.set_obs_fold_in_header_value(true); + } + if (obs_fold_in_trailer_values) { + log_defects.set_obs_fold_in_trailer_value(true); + } + if (chunked_body_does_not_end_with_crlf_crlf) { + log_defects.set_chunked_request_ends_with_crlflf(true); + } + if (stray_data_after_chunk) { + log_defects.set_stray_data_after_chunk(true); + } + if (lone_cr_in_request_headers) { + log_defects.set_cr_found_in_header_value(true); + } + if (header_name_contains_double_quote) { + log_defects.set_header_name_contains_double_quote(true); + } + if (header_missing_colon) { + log_defects.set_header_missing_colon(true); + } + if (transfer_encoding_and_content_length) { + log_defects.set_transfer_encoding_and_content_length(true); + } + if (unknown_transfer_encoding) { + log_defects.set_unknown_transfer_encoding(true); + } + if (multiple_transfer_encoding_keys) { + log_defects.set_multiple_transfer_encoding_keys(true); + } + if (tab_or_cr_found_in_firstline) { + log_defects.set_tab_or_cr_found_in_firstline(true); + } + if (obs_text_found_in_header_name) { + log_defects.set_obs_text_found_in_header_name(true); + } + } +}; + +} // namespace quiche + +#endif // QUICHE_BALSA_HTTP_PROTOCOL_DEFECTS_H_