Add HttpValidationPolicy for rejecting RFC non-compliant status codes [RFC 9110 Section 15](https://httpwg.org/specs/rfc9110.html#status.codes) clearly states that status codes should be within the range [100, 599] and we now expose a knob for enforcing that in clients. Protected by unused http validation policy. PiperOrigin-RevId: 884659559
diff --git a/quiche/balsa/balsa_enums.cc b/quiche/balsa/balsa_enums.cc index 24b2c8f..1d88760 100644 --- a/quiche/balsa/balsa_enums.cc +++ b/quiche/balsa/balsa_enums.cc
@@ -128,6 +128,8 @@ return "UNSUPPORTED_100_CONTINUE"; case INVALID_REQUEST_METHOD: return "INVALID_REQUEST_METHOD"; + case INVALID_STATUS_CODE: + return "INVALID_STATUS_CODE"; // This should be the last case in the switch statement. case NUM_ERROR_CODES: return "UNKNOWN_ERROR";
diff --git a/quiche/balsa/balsa_enums.h b/quiche/balsa/balsa_enums.h index 718fbd8..d59ba6b 100644 --- a/quiche/balsa/balsa_enums.h +++ b/quiche/balsa/balsa_enums.h
@@ -71,6 +71,7 @@ OBS_FOLD_IN_HEADERS, FAILED_CONVERTING_STATUS_CODE_TO_INT, + INVALID_STATUS_CODE, INVALID_TARGET_URI, HEADERS_TOO_LONG,
diff --git a/quiche/balsa/balsa_frame.cc b/quiche/balsa/balsa_frame.cc index ded79f7..3e64b64 100644 --- a/quiche/balsa/balsa_frame.cc +++ b/quiche/balsa/balsa_frame.cc
@@ -425,6 +425,16 @@ return; } + if (headers_->parsed_response_code_ < 100 || + headers_->parsed_response_code_ > 599) { + if (http_validation_policy().disallow_invalid_response_codes) { + parse_state_ = BalsaFrameEnums::ERROR; + last_error_ = BalsaFrameEnums::INVALID_STATUS_CODE; + HandleError(last_error_); + return; + } + } + visitor_->OnResponseFirstLineInput(line_input, part1, part2, part3); }
diff --git a/quiche/balsa/balsa_frame_test.cc b/quiche/balsa/balsa_frame_test.cc index 95e239d..d06a067 100644 --- a/quiche/balsa/balsa_frame_test.cc +++ b/quiche/balsa/balsa_frame_test.cc
@@ -474,6 +474,9 @@ EXPECT_STREQ("INVALID_HEADER_CHARACTER", BalsaFrameEnums::ErrorCodeToString( BalsaFrameEnums::INVALID_HEADER_CHARACTER)); + EXPECT_STREQ( + "INVALID_STATUS_CODE", + BalsaFrameEnums::ErrorCodeToString(BalsaFrameEnums::INVALID_STATUS_CODE)); EXPECT_STREQ("UNKNOWN_ERROR", BalsaFrameEnums::ErrorCodeToString( BalsaFrameEnums::NUM_ERROR_CODES)); @@ -935,6 +938,52 @@ FirstLineParsedCorrectlyHelper(response_tokens, 4242, false, " \t \t "); } +TEST(HTTPBalsaFrame, LargeAndSmallStatusCodesWithPolicy) { + struct TestCase { + const absl::string_view status_code; + const BalsaFrameEnums::ErrorCode expected_error; + }; + std::vector<TestCase> cases = { + {"0", BalsaFrameEnums::INVALID_STATUS_CODE}, + {"99", BalsaFrameEnums::INVALID_STATUS_CODE}, + {"100", BalsaFrameEnums::BALSA_NO_ERROR}, + {"200", BalsaFrameEnums::BALSA_NO_ERROR}, + {"599", BalsaFrameEnums::BALSA_NO_ERROR}, + {"600", BalsaFrameEnums::INVALID_STATUS_CODE}, + {"1000", BalsaFrameEnums::INVALID_STATUS_CODE}, + {"65740", BalsaFrameEnums::INVALID_STATUS_CODE}, + {"99999999999999999999999", + BalsaFrameEnums::FAILED_CONVERTING_STATUS_CODE_TO_INT}}; + HttpValidationPolicy policy; + policy.disallow_invalid_response_codes = true; + for (const TestCase& tcase : cases) { + BalsaHeaders headers; + BalsaFrame framer; + framer.set_http_validation_policy(policy); + framer.set_is_request(false); + framer.set_balsa_headers(&headers); + framer.set_http_validation_policy(policy); + 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()); + } + } +} + TEST(HTTPBalsaFrame, StatusLineSanitizedProperly) { SCOPED_TRACE("Testing that the status line is properly sanitized."); using FirstLineValidationOption =
diff --git a/quiche/balsa/balsa_fuzz_util.cc b/quiche/balsa/balsa_fuzz_util.cc index 58b15fa..f28a23f 100644 --- a/quiche/balsa/balsa_fuzz_util.cc +++ b/quiche/balsa/balsa_fuzz_util.cc
@@ -23,7 +23,8 @@ fuzztest::Arbitrary<bool>(), fuzztest::Arbitrary<bool>(), fuzztest::Arbitrary<bool>(), ArbitraryFirstLineValidationOption(), fuzztest::Arbitrary<bool>(), fuzztest::Arbitrary<bool>(), - fuzztest::Arbitrary<bool>(), fuzztest::Arbitrary<bool>()); + fuzztest::Arbitrary<bool>(), fuzztest::Arbitrary<bool>(), + fuzztest::Arbitrary<bool>()); } fuzztest::Domain<HttpValidationPolicy::FirstLineValidationOption>
diff --git a/quiche/balsa/http_validation_policy.h b/quiche/balsa/http_validation_policy.h index 7df7b7d..b1651a3 100644 --- a/quiche/balsa/http_validation_policy.h +++ b/quiche/balsa/http_validation_policy.h
@@ -124,6 +124,10 @@ // more information bool require_semicolon_delimited_chunk_extension = false; + // Status codes outside the range [100, 599] are invalid, per RFC 9110, + // Section 15 https://www.rfc-editor.org/rfc/rfc9110#section-15 + bool disallow_invalid_response_codes = false; + template <typename Sink> friend void AbslStringify(Sink& sink, FirstLineValidationOption option) { switch (option) { @@ -162,7 +166,8 @@ "sanitize_obs_fold_in_header_values=%v, " "disallow_stray_data_after_chunk=%v, " "disallow_invalid_request_methods=%v, " - "require_semicolon_delimited_chunk_extension=%v}", + "require_semicolon_delimited_chunk_extension=%v, " + "disallow_invalid_response_codes=%v}", policy.disallow_header_continuation_lines, policy.require_header_colon, policy.disallow_multiple_content_length, @@ -182,7 +187,8 @@ policy.sanitize_obs_fold_in_header_values, policy.disallow_stray_data_after_chunk, policy.disallow_invalid_request_methods, - policy.require_semicolon_delimited_chunk_extension); + policy.require_semicolon_delimited_chunk_extension, + policy.disallow_invalid_response_codes); } }; @@ -208,7 +214,8 @@ .sanitize_obs_fold_in_header_values = true, .disallow_stray_data_after_chunk = true, .disallow_invalid_request_methods = true, - .require_semicolon_delimited_chunk_extension = true}; + .require_semicolon_delimited_chunk_extension = true, + .disallow_invalid_response_codes = true}; } // namespace quiche