| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| #include <fuzzer/FuzzedDataProvider.h> |
| #include <google/protobuf/descriptor.h> |
| #include <memory> |
| #include "base/functional/bind.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_util.h" |
| #include "base/test/bind.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "chrome/test/fuzzing/in_process_proto_fuzzer.h" |
| #include "chrome/test/fuzzing/page_load_in_process_fuzzer.pb.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| |
| // A fuzzer which can test the interaction of HTTP response parameters |
| // and HTML content. This is a large search space and it's unlikely that |
| // this fuzzer will presently find interesting results, but future |
| // technologies that can better explore a search space like this may |
| // successfully do so. Meanwhile, it may be useful to aid reproduction |
| // of human-crafted test cases. |
| // |
| // In the future we might want to extend this fuzzer to: |
| // * support different HTTPS parameters too |
| // * support multiple, different, HTTP(S) responses in order to |
| // handle iframes or other types of navigation. |
| // (We'd need to provide a corpus designed to exercise these). |
| // * run servers on 3+ different ports to support cross-origin navigations |
| |
| class PageLoadInProcessFuzzer |
| : public InProcessProtoFuzzer<test::fuzzing::page_load_fuzzing::FuzzCase> { |
| public: |
| using WhichServer = test::fuzzing::page_load_fuzzing::WhichServer; |
| PageLoadInProcessFuzzer(); |
| |
| void SetUpOnMainThread() override; |
| int Fuzz( |
| const test::fuzzing::page_load_fuzzing::FuzzCase& fuzz_case) override; |
| |
| private: |
| static std::unique_ptr<net::test_server::HttpResponse> HandleHTTPRequest( |
| base::WeakPtr<PageLoadInProcessFuzzer> fuzzer_weak, |
| |
| WhichServer which_server, |
| const net::test_server::HttpRequest& request); |
| std::unique_ptr<net::test_server::BasicHttpResponse> DoHandleHTTPRequest( |
| WhichServer which_server, |
| const net::test_server::HttpRequest& request); |
| std::string SubstituteServersInBody(const std::string& body); |
| static void SubstituteServerPattern(std::string* haystack, |
| const std::string& pattern, |
| const net::EmbeddedTestServer& server); |
| |
| private: |
| // To test cross-origin cases, we have four servers listening |
| // on different ports. |
| net::EmbeddedTestServer http_test_server1_; |
| net::EmbeddedTestServer http_test_server2_; |
| net::EmbeddedTestServer https_test_server1_; |
| net::EmbeddedTestServer https_test_server2_; |
| test::fuzzing::page_load_fuzzing::FuzzCase fuzz_case_; |
| base::WeakPtrFactory<PageLoadInProcessFuzzer> weak_ptr_factory_{this}; |
| }; |
| |
| REGISTER_TEXT_PROTO_IN_PROCESS_FUZZER(PageLoadInProcessFuzzer) |
| |
| PageLoadInProcessFuzzer::PageLoadInProcessFuzzer() |
| : InProcessProtoFuzzer({ |
| RunLoopTimeoutBehavior::kDeclareInfiniteLoop, |
| base::Seconds(180), |
| }), |
| http_test_server1_(net::EmbeddedTestServer::TYPE_HTTP), |
| http_test_server2_(net::EmbeddedTestServer::TYPE_HTTP), |
| https_test_server1_(net::EmbeddedTestServer::TYPE_HTTPS), |
| https_test_server2_(net::EmbeddedTestServer::TYPE_HTTPS) { |
| https_test_server1_.SetSSLConfig(net::EmbeddedTestServer::CERT_OK); |
| https_test_server2_.SetSSLConfig(net::EmbeddedTestServer::CERT_OK); |
| http_test_server1_.RegisterRequestHandler(base::BindRepeating( |
| &PageLoadInProcessFuzzer::HandleHTTPRequest, |
| weak_ptr_factory_.GetWeakPtr(), WhichServer::HTTP_ORIGIN1)); |
| https_test_server1_.RegisterRequestHandler(base::BindRepeating( |
| &PageLoadInProcessFuzzer::HandleHTTPRequest, |
| weak_ptr_factory_.GetWeakPtr(), WhichServer::HTTPS_ORIGIN1)); |
| http_test_server2_.RegisterRequestHandler(base::BindRepeating( |
| &PageLoadInProcessFuzzer::HandleHTTPRequest, |
| weak_ptr_factory_.GetWeakPtr(), WhichServer::HTTP_ORIGIN2)); |
| https_test_server2_.RegisterRequestHandler(base::BindRepeating( |
| &PageLoadInProcessFuzzer::HandleHTTPRequest, |
| weak_ptr_factory_.GetWeakPtr(), WhichServer::HTTPS_ORIGIN2)); |
| } |
| |
| void PageLoadInProcessFuzzer::SetUpOnMainThread() { |
| InProcessFuzzer::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| CHECK(http_test_server1_.Start()); |
| CHECK(http_test_server2_.Start()); |
| CHECK(https_test_server1_.Start()); |
| CHECK(https_test_server2_.Start()); |
| } |
| |
| std::unique_ptr<net::test_server::HttpResponse> |
| PageLoadInProcessFuzzer::HandleHTTPRequest( |
| base::WeakPtr<PageLoadInProcessFuzzer> fuzzer_weak, |
| WhichServer which_server, |
| const net::test_server::HttpRequest& request) { |
| std::unique_ptr<net::test_server::BasicHttpResponse> response; |
| // We are running on the embedded test server's thread. |
| // We want to ask the fuzzer thread for the fuzz case. |
| // We use a weak pointer, but we have to dereference that on the originating |
| // thread. |
| base::RunLoop run_loop; |
| base::RepeatingCallback<void()> get_payload_lambda = |
| base::BindLambdaForTesting([&]() { |
| PageLoadInProcessFuzzer* fuzzer = fuzzer_weak.get(); |
| if (fuzzer) { |
| response = fuzzer->DoHandleHTTPRequest(which_server, request); |
| } |
| run_loop.Quit(); |
| }); |
| content::GetUIThreadTaskRunner()->PostTask(FROM_HERE, get_payload_lambda); |
| run_loop.Run(); |
| return response; |
| } |
| |
| std::unique_ptr<net::test_server::BasicHttpResponse> |
| PageLoadInProcessFuzzer::DoHandleHTTPRequest( |
| WhichServer which_server, |
| const net::test_server::HttpRequest& request) { |
| // Look through all the network resources given in the fuzz case and build |
| // a response if we find one. |
| LOG(INFO) << "Got request at " << which_server << " path " |
| << request.relative_url; |
| for (const auto& network_resource : fuzz_case_.network_resource()) { |
| if (network_resource.which_server() == which_server && |
| (network_resource.path() == request.relative_url || |
| (request.relative_url == "/" && network_resource.path().empty()))) { |
| std::unique_ptr<net::test_server::BasicHttpResponse> response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code( |
| static_cast<net::HttpStatusCode>(network_resource.http_status())); |
| response->set_content_type(network_resource.content_type()); |
| for (const auto& header : network_resource.custom_headers()) { |
| response->AddCustomHeader(header.key(), header.value()); |
| } |
| response->set_reason(network_resource.reason()); |
| if (network_resource.has_body()) { |
| response->set_content(SubstituteServersInBody(network_resource.body())); |
| } |
| LOG(INFO) << "Returning valid response for " << which_server << " path " |
| << request.relative_url; |
| return response; |
| } |
| } |
| return nullptr; |
| } |
| |
| int PageLoadInProcessFuzzer::Fuzz( |
| const test::fuzzing::page_load_fuzzing::FuzzCase& fuzz_case) { |
| fuzz_case_ = fuzz_case; |
| |
| GURL test_url; |
| |
| if (fuzz_case_.has_data_uri_navigation()) { |
| const auto& data_uri_navigation = fuzz_case_.data_uri_navigation(); |
| std::string content_type = data_uri_navigation.content_type(); |
| std::string body = SubstituteServersInBody(data_uri_navigation.body()); |
| // Request via a data: URI which should be quickest. |
| test_url = GURL(base::StrCat({"data:", content_type, ";charset=utf-8,", |
| base::EscapeQueryParamValue(body, false)})); |
| } else { |
| // We navigate to the first server resource listed. |
| if (fuzz_case_.network_resource_size() < 1) { |
| return -1; // invalid fuzz case. |
| } |
| const auto& network_resource = fuzz_case_.network_resource(0); |
| std::string path = network_resource.path(); |
| switch (network_resource.which_server()) { |
| case WhichServer::HTTP_ORIGIN1: |
| test_url = http_test_server1_.GetURL(path); |
| break; |
| case WhichServer::HTTP_ORIGIN2: |
| test_url = http_test_server2_.GetURL(path); |
| break; |
| case WhichServer::HTTPS_ORIGIN1: |
| test_url = https_test_server1_.GetURL(path); |
| break; |
| case WhichServer::HTTPS_ORIGIN2: |
| test_url = https_test_server2_.GetURL(path); |
| break; |
| default: |
| LOG(FATAL) << "Unexpected proto value for which server"; |
| } |
| } |
| |
| LOG(INFO) << "Navigating to " << test_url; |
| base::IgnoreResult(ui_test_utils::NavigateToURL(browser(), test_url)); |
| return 0; |
| } |
| |
| void PageLoadInProcessFuzzer::SubstituteServerPattern( |
| std::string* body, |
| const std::string& pattern, |
| const net::EmbeddedTestServer& server) { |
| std::string url = server.GetURL("").spec(); |
| url.pop_back(); // remove trailing / |
| base::ReplaceSubstringsAfterOffset(body, 0, pattern, url); |
| } |
| |
| std::string PageLoadInProcessFuzzer::SubstituteServersInBody( |
| const std::string& body) { |
| std::string result = body; |
| SubstituteServerPattern(&result, "$HTTP_ORIGIN1", http_test_server1_); |
| SubstituteServerPattern(&result, "$HTTP_ORIGIN2", http_test_server2_); |
| SubstituteServerPattern(&result, "$HTTPS_ORIGIN1", https_test_server1_); |
| SubstituteServerPattern(&result, "$HTTPS_ORIGIN2", https_test_server2_); |
| return result; |
| } |