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