chromium/chrome/test/fuzzing/page_load_in_process_fuzzer.cc

// 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 &&
        request.relative_url.substr(1) == network_resource.path()) {
      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;
}