// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>

#include <atomic>
#include <optional>
#include <tuple>

#include "base/command_line.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/test/bind.h"
#include "base/test/gtest_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "content/browser/bad_message.h"
#include "content/browser/browser_interface_broker_impl.h"
#include "content/browser/dom_storage/dom_storage_context_wrapper.h"
#include "content/browser/dom_storage/session_storage_namespace_impl.h"
#include "content/browser/fenced_frame/fenced_frame.h"
#include "content/browser/permissions/permission_service_context.h"
#include "content/browser/renderer_host/file_utilities_host_impl.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/navigator.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_frame_proxy_host.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_view_host_factory.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/screen_orientation/screen_orientation_provider.h"
#include "content/browser/security/cpsp/child_process_security_policy_impl.h"
#include "content/browser/web_contents/file_chooser_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/features.h"
#include "content/common/frame.mojom.h"
#include "content/common/frame_messages.mojom.h"
#include "content/common/render_message_filter.mojom.h"
#include "content/public/browser/blob_handle.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/file_select_listener.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/bindings_policy.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/navigation_handle_observer.h"
#include "content/public/test/test_content_browser_client.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/did_commit_navigation_interceptor.h"
#include "content/test/frame_host_interceptor.h"
#include "device/gamepad/public/mojom/gamepad.mojom.h"
#include "ipc/constants.mojom.h"
#include "mojo/core/embedder/embedder.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/base/features.h"
#include "net/base/filename_util.h"
#include "net/base/net_errors.h"
#include "net/base/network_isolation_key.h"
#include "net/dns/mock_host_resolver.h"
#include "net/storage_access_api/status.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/device/public/mojom/battery_monitor.mojom.h"
#include "services/device/public/mojom/screen_orientation.mojom.h"
#include "services/device/public/mojom/vibration_manager.mojom.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/resource_request_body.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
#include "services/network/public/mojom/trust_tokens.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/test/test_url_loader_client.h"
#include "storage/browser/blob/blob_registry_impl.h"
#include "storage/browser/blob/blob_url_registry.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/blob/blob_utils.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h"
#include "third_party/blink/public/common/frame/fenced_frame_sandbox_flags.h"
#include "third_party/blink/public/common/navigation/navigation_policy.h"
#include "third_party/blink/public/common/page_state/page_state_serialization.h"
#include "third_party/blink/public/mojom/blob/blob.mojom.h"
#include "third_party/blink/public/mojom/blob/blob_url_store.mojom.h"
#include "third_party/blink/public/mojom/blob/data_element.mojom.h"
#include "third_party/blink/public/mojom/blob/file_backed_blob_factory.mojom.h"
#include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h"
#include "third_party/blink/public/mojom/content_extraction/script_tools.mojom.h"
#include "third_party/blink/public/mojom/fenced_frame/fenced_frame.mojom.h"
#include "third_party/blink/public/mojom/file/file_utilities.mojom.h"
#include "third_party/blink/public/mojom/frame/frame.mojom-test-utils.h"
#include "third_party/blink/public/mojom/frame/frame.mojom.h"
#include "third_party/blink/public/mojom/frame/frame_replication_state.mojom.h"
#include "third_party/blink/public/mojom/frame/fullscreen.mojom.h"
#include "third_party/blink/public/mojom/frame/remote_frame.mojom-test-utils.h"
#include "third_party/blink/public/mojom/loader/mixed_content.mojom.h"
#include "third_party/blink/public/mojom/permissions/permission.mojom.h"

using ::testing::HasSubstr;
using ::testing::Optional;

namespace content {

namespace {

// This is a helper function for the tests which attempt to create a
// duplicate RenderViewHost or RenderWidgetHost. It tries to create two objects
// with the same process and routing ids, which causes a collision.
// It creates a couple of windows in process 1, which causes a few routing ids
// to be allocated. Then a cross-process navigation is initiated, which causes a
// new process 2 to be created and have a pending RenderViewHost for it. The
// routing id of the RenderViewHost which is target for a duplicate is set
// into |target_routing_id| and the pending RenderFrameHost which is used for
// the attempt is the return value.
RenderFrameHostImpl* PrepareToDuplicateHosts(Shell* shell,
                                             net::EmbeddedTestServer* server,
                                             int* target_routing_id) {
  GURL foo("http://foo.com/simple_page.html");

  if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
    // Isolate "bar.com" so we are guaranteed to get a different process
    // for navigations to this origin.
    IsolateOriginsForTesting(server, shell->web_contents(), {"bar.com"});
  }

  // Start off with initial navigation, so we get the first process allocated.
  EXPECT_TRUE(NavigateToURL(shell, foo));
  EXPECT_EQ(u"OK", shell->web_contents()->GetTitle());

  // Open another window, so we generate some more routing ids.
  ShellAddedObserver shell2_observer;
  EXPECT_TRUE(ExecJs(shell, "window.open(document.URL + '#2');"));
  Shell* shell2 = shell2_observer.GetShell();

  // The new window must be in the same process, but have a new routing id.
  EXPECT_EQ(shell->web_contents()
                ->GetPrimaryMainFrame()
                ->GetProcess()
                ->GetDeprecatedID(),
            shell2->web_contents()
                ->GetPrimaryMainFrame()
                ->GetProcess()
                ->GetDeprecatedID());
  *target_routing_id = shell2->web_contents()
                           ->GetPrimaryMainFrame()
                           ->GetRenderViewHost()
                           ->GetRoutingID();
  EXPECT_NE(*target_routing_id, shell->web_contents()
                                    ->GetPrimaryMainFrame()
                                    ->GetRenderViewHost()
                                    ->GetRoutingID());

  // Now, simulate a link click coming from the renderer.
  GURL extension_url("http://bar.com/simple_page.html");
  WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell->web_contents());
  TestNavigationManager navigation_manager(wc, extension_url);
  wc->GetPrimaryFrameTree().root()->navigator().RequestOpenURL(
      wc->GetPrimaryFrameTree().root()->current_frame_host(), extension_url,
      nullptr /* initiator_frame_token */,
      ChildProcessHost::kInvalidUniqueID /* initiator_process_id */,
      url::Origin::Create(foo), /* initiator_base_url= */ std::nullopt,
      /* initiator_navigation_state= */ nullptr, nullptr, std::string(),
      Referrer(), WindowOpenDisposition::CURRENT_TAB,
      false /* should_replace_current_entry */, true /* user_gesture */,
      blink::mojom::TriggeringEventInfo::kFromTrustedEvent, std::string(),
      nullptr /* blob_url_loader_factory */, false /* has_rel_opener */,
      false /* started_by_ad */);
  navigation_manager.WaitForSpeculativeRenderFrameHostCreation();

  // Since the navigation above requires a cross-process swap, there will be a
  // speculative/pending RenderFrameHost. Ensure it exists and is in a different
  // process than the initial page.
  RenderFrameHostImpl* next_rfh = wc->GetPrimaryFrameTree()
                                      .root()
                                      ->render_manager()
                                      ->speculative_frame_host();

  EXPECT_TRUE(next_rfh);
  EXPECT_NE(shell->web_contents()
                ->GetPrimaryMainFrame()
                ->GetProcess()
                ->GetDeprecatedID(),
            next_rfh->GetProcess()->GetDeprecatedID());

  return next_rfh;
}

blink::mojom::OpenURLParamsPtr CreateOpenURLParams(const GURL& url) {
  auto params = blink::mojom::OpenURLParams::New();
  params->url = url;
  params->disposition = WindowOpenDisposition::CURRENT_TAB;
  params->should_replace_current_entry = false;
  params->user_gesture = true;
  return params;
}

std::unique_ptr<content::BlobHandle> CreateMemoryBackedBlob(
    BrowserContext* browser_context,
    const std::string& contents,
    const std::string& content_type) {
  std::unique_ptr<content::BlobHandle> result;
  base::RunLoop loop;
  browser_context->CreateMemoryBackedBlob(
      base::as_byte_span(contents), content_type,
      base::BindOnce(
          [](std::unique_ptr<content::BlobHandle>* out_blob,
             base::OnceClosure done,
             std::unique_ptr<content::BlobHandle> blob) {
            *out_blob = std::move(blob);
            std::move(done).Run();
          },
          &result, loop.QuitClosure()));
  loop.Run();
  EXPECT_TRUE(result);
  return result;
}

// Constructs a WebContentsDelegate that mocks a file dialog.
// Unlike content::FileChooserDelegate, this class doesn't make a response in
// RunFileChooser(), and a user needs to call Choose().
class DelayedFileChooserDelegate : public WebContentsDelegate {
 public:
  void Choose(const base::FilePath& file) {
    auto file_info = blink::mojom::FileChooserFileInfo::NewNativeFile(
        blink::mojom::NativeFileInfo::New(file, std::u16string(),
                                          std::vector<std::u16string>()));
    std::vector<blink::mojom::FileChooserFileInfoPtr> files;
    files.push_back(std::move(file_info));
    listener_->FileSelected(std::move(files), base::FilePath(),
                            blink::mojom::FileChooserParams::Mode::kOpen);
    listener_.reset();
  }

  // WebContentsDelegate overrides
  void RunFileChooser(RenderFrameHost* render_frame_host,
                      scoped_refptr<FileSelectListener> listener,
                      const blink::mojom::FileChooserParams& params) override {
    listener_ = std::move(listener);
  }

  void EnumerateDirectory(WebContents* web_contents,
                          scoped_refptr<FileSelectListener> listener,
                          const base::FilePath& directory_path) override {
    listener->FileSelectionCanceled();
  }

 private:
  scoped_refptr<FileSelectListener> listener_;
};

void FileChooserCallback(base::RunLoop* run_loop,
                         blink::mojom::FileChooserResultPtr result) {
  run_loop->Quit();
}

}  // namespace

// The goal of these tests will be to "simulate" exploited renderer processes,
// which can send arbitrary IPC messages and confuse browser process internal
// state, leading to security bugs. We are trying to verify that the browser
// doesn't perform any dangerous operations in such cases.
class SecurityExploitBrowserTest : public ContentBrowserTest {
 public:
  SecurityExploitBrowserTest() {
    feature_list_.InitWithFeatures({features::kKillOnUnexpectedOriginHeader},
                                   {});
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    // EmbeddedTestServer::InitializeAndListen() initializes its |base_url_|
    // which is required below. This cannot invoke Start() however as that kicks
    // off the "EmbeddedTestServer IO Thread" which then races with
    // initialization in ContentBrowserTest::SetUp(), http://crbug.com/674545.
    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());

    // Add a host resolver rule to map all outgoing requests to the test server.
    // This allows us to use "real" hostnames in URLs, which we can use to
    // create arbitrary SiteInstances.
    command_line->AppendSwitchASCII(
        network::switches::kHostResolverRules,
        "MAP * " +
            net::HostPortPair::FromURL(embedded_test_server()->base_url())
                .ToString() +
            ",EXCLUDE localhost");
  }

  void SetUpOnMainThread() override {
    // Complete the manual Start() after ContentBrowserTest's own
    // initialization, ref. comment on InitializeAndListen() above.
    embedded_test_server()->StartAcceptingConnections();
  }

 protected:
  // Tests that a given file path sent in a FrameHostMsg_RunFileChooser will
  // cause renderer to be killed.
  void TestFileChooserWithPath(const base::FilePath& path);

  void IsolateOrigin(const std::string& hostname) {
    IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
                             {hostname});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

class SecurityExploitBrowserTestWebMCPDisabled
    : public SecurityExploitBrowserTest {
 public:
  SecurityExploitBrowserTestWebMCPDisabled() {
    scoped_feature_list_.InitAndDisableFeature(blink::features::kWebMCP);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

class SecurityExploitBrowserTestWebMCPEnabled
    : public SecurityExploitBrowserTest {
 public:
  SecurityExploitBrowserTestWebMCPEnabled() {
    scoped_feature_list_.InitAndEnableFeature(blink::features::kWebMCP);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

void SecurityExploitBrowserTest::TestFileChooserWithPath(
    const base::FilePath& path) {
  GURL foo("http://foo.com/simple_page.html");
  EXPECT_TRUE(NavigateToURL(shell(), foo));
  EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle());

  RenderFrameHost* compromised_renderer =
      shell()->web_contents()->GetPrimaryMainFrame();
  blink::mojom::FileChooserParamsPtr params =
      blink::mojom::FileChooserParams::New();
  params->default_file_name = path;

  mojo::test::BadMessageObserver bad_message_observer;
  mojo::Remote<blink::mojom::FileChooser> chooser =
      FileChooserImpl::CreateBoundForTesting(
          static_cast<RenderFrameHostImpl*>(compromised_renderer));
  chooser->OpenFileChooser(
      std::move(params), blink::mojom::FileChooser::OpenFileChooserCallback());
  chooser.FlushForTesting();
  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              ::testing::StartsWith("FileChooser: The default file name"));
}

// Ensure that we kill the renderer process if we try to give it WebUI
// properties and it doesn't have enabled WebUI bindings.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, SetWebUIProperty) {
  GURL foo("http://foo.com/simple_page.html");

  EXPECT_TRUE(NavigateToURL(shell(), foo));
  EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle());
  EXPECT_TRUE(shell()
                  ->web_contents()
                  ->GetPrimaryMainFrame()
                  ->GetEnabledBindings()
                  .empty());

  RenderFrameHost* compromised_renderer =
      shell()->web_contents()->GetPrimaryMainFrame();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      compromised_renderer->GetProcess());
  compromised_renderer->SetWebUIProperty("toolkit", "views");
  EXPECT_EQ(bad_message::RVH_WEB_UI_BINDINGS_MISMATCH, kill_waiter.Wait());
}

// This is a test for crbug.com/312016 attempting to create duplicate
// RenderViewHosts. SetupForDuplicateHosts sets up this test case and leaves
// it in a state with pending RenderViewHost. Before the commit of the new
// pending RenderViewHost, this test case creates a new window through the new
// process.
//
// TODO(crbug.com/485081313): Flaky. Reenable it.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       DISABLED_AttemptDuplicateRenderViewHost) {
  int32_t duplicate_routing_id = IPC::mojom::kRoutingIdNone;
  RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts(
      shell(), embedded_test_server(), &duplicate_routing_id);
  EXPECT_NE(IPC::mojom::kRoutingIdNone, duplicate_routing_id);

  mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
  params->target_url = GURL("about:blank");
  pending_rfh->CreateNewWindow(
      std::move(params), base::BindOnce([](mojom::CreateNewWindowStatus,
                                           mojom::CreateNewWindowReplyPtr) {}));
  // If the above operation doesn't cause a crash, the test has succeeded!
}

// This is a test for crbug.com/444198. It tries to send a
// FrameHostMsg_RunFileChooser containing an invalid path. The browser should
// correctly terminate the renderer in these cases.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, AttemptRunFileChoosers) {
  TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("../../*.txt")));
  TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("/etc/*.conf")));
#if BUILDFLAG(IS_WIN)
  TestFileChooserWithPath(
      base::FilePath(FILE_PATH_LITERAL("\\\\evilserver\\evilshare\\*.txt")));
  TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("c:\\*.txt")));
  TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("..\\..\\*.txt")));
#endif
}

// Ensure that we kill the renderer process if it tries to open a file chooser
// with Mode::kSave, which is not allowed for renderer-initiated choosers.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, FileChooserWithSaveMode) {
  GURL foo("http://foo.com/simple_page.html");
  EXPECT_TRUE(NavigateToURL(shell(), foo));

  RenderFrameHost* compromised_renderer =
      shell()->web_contents()->GetPrimaryMainFrame();
  blink::mojom::FileChooserParamsPtr params =
      blink::mojom::FileChooserParams::New();
  params->mode = blink::mojom::FileChooserParams::Mode::kSave;
  params->default_file_name = base::FilePath(FILE_PATH_LITERAL("file.txt"));

  mojo::test::BadMessageObserver bad_message_observer;
  mojo::Remote<blink::mojom::FileChooser> chooser =
      FileChooserImpl::CreateBoundForTesting(
          static_cast<RenderFrameHostImpl*>(compromised_renderer));
  chooser->OpenFileChooser(
      std::move(params), blink::mojom::FileChooser::OpenFileChooserCallback());
  chooser.FlushForTesting();
  EXPECT_EQ("FileChooser: Save mode is not allowed.",
            bad_message_observer.WaitForBadMessage());
}

// A test for crbug.com/941008.
// Calling OpenFileChooser() and EnumerateChosenDirectory() for a single
// FileChooser instance had a problem.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, UnexpectedMethodsSequence) {
  EXPECT_TRUE(NavigateToURL(shell(), GURL("http://foo.com/simple_page.html")));
  RenderFrameHost* compromised_renderer =
      shell()->web_contents()->GetPrimaryMainFrame();
  auto delegate = std::make_unique<DelayedFileChooserDelegate>();
  shell()->web_contents()->SetDelegate(delegate.get());

  mojo::Remote<blink::mojom::FileChooser> chooser =
      FileChooserImpl::CreateBoundForTesting(
          static_cast<RenderFrameHostImpl*>(compromised_renderer));
  base::RunLoop run_loop1;
  base::RunLoop run_loop2;
  chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(),
                           base::BindOnce(FileChooserCallback, &run_loop2));
  // The following EnumerateChosenDirectory() runs the specified callback
  // immediately regardless of the content of the first argument FilePath.
  chooser->EnumerateChosenDirectory(
      base::FilePath(FILE_PATH_LITERAL(":*?\"<>|")),
      base::BindOnce(FileChooserCallback, &run_loop1));
  run_loop1.Run();

  delegate->Choose(base::FilePath(FILE_PATH_LITERAL("foo.txt")));
  run_loop2.Run();

  // The test passes if it doesn't crash.
}

// Test that calling `RegisterScriptTool()` when the `tools` permission policy
// is not enabled properly terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       RegisterScriptToolPermissionPolicyDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create a cross-origin iframe to b.com. `tools` permission is enabled for
  // 'self' by default, so it will be disabled for the cross-origin frame.
  GURL subframe_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  RenderFrameHostImpl* subframe = static_cast<RenderFrameHostImpl*>(
      CreateSubframe(static_cast<WebContentsImpl*>(shell()->web_contents()),
                     "child1", subframe_url, true /* wait_for_navigation */));

  EXPECT_FALSE(subframe->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());

  auto tool = blink::mojom::ScriptTool::New();
  tool->name = "test-tool";
  tool->description = "test description";

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  subframe->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing());
  EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait());
}

// Test that calling `RegisterScriptTool()` when the `WebMCP` base::Feature is
// disabled (but permissions policy is enabled) properly terminates the
// renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPDisabled,
                       RegisterScriptToolWebMCPDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  auto tool = blink::mojom::ScriptTool::New();
  tool->name = "test-tool";
  tool->description = "test description";

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing());
  EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait());
}

// Test that calling `UnregisterScriptTool()` when the `WebMCP` base::Feature is
// disabled (but permissions policy is enabled) properly terminates the
// renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPDisabled,
                       UnregisterScriptToolWebMCPDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->UnregisterScriptTool("some-tool");
  EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait());
}

// Test that calling `GetScriptTools()` when the `WebMCP` base::Feature is
// disabled (but permissions policy is enabled) properly terminates the
// renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPDisabled,
                       GetScriptToolsWebMCPDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->GetScriptTools({}, base::DoNothing());
  EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait());
}

// Test that calling `ExecuteRemoteScriptTool()` when the `WebMCP` base::Feature
// is disabled (but permissions policy is enabled) properly terminates the
// renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPDisabled,
                       ExecuteRemoteScriptToolWebMCPDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  blink::FrameToken tool_owner_frame_token = rfh->GetFrameToken();

  script_tool_host->ExecuteRemoteScriptTool(
      base::UnguessableToken::Create(), tool_owner_frame_token,
      rfh->GetLastCommittedOrigin(), "test-tool", "[]",
      base::BindOnce(
          [](const std::optional<std::string>& result, bool success) {}));
  EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait());
}

// Test that calling `RegisterScriptTool()` with an invalid
// `tool_owner_frame_token` properly terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       RegisterScriptToolInvalidToolOwnerToken) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  auto tool = blink::mojom::ScriptTool::New();
  tool->name = "test-tool";
  tool->description = "test description";
  tool->tool_owner_frame_token = blink::LocalFrameToken();
  tool->origin = rfh->GetLastCommittedOrigin();

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing());
  EXPECT_EQ(bad_message::RFHI_WEBMCP_INVALID_TOOL_OWNER, kill_waiter.Wait());
}

// Test that calling `RegisterScriptTool()` with an invalid `origin`
// properly terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       RegisterScriptToolInvalidToolOwnerOrigin) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  auto tool = blink::mojom::ScriptTool::New();
  tool->name = "test-tool";
  tool->description = "test description";
  tool->tool_owner_frame_token = rfh->GetFrameToken();
  tool->origin = url::Origin::Create(GURL("https://b.com"));

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing());
  EXPECT_EQ(bad_message::RFHI_WEBMCP_INVALID_TOOL_OWNER, kill_waiter.Wait());
}

// Test that calling `UnregisterScriptTool()` when the `tools` permission policy
// is not enabled properly terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       UnregisterScriptToolPermissionPolicyDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  GURL subframe_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  RenderFrameHostImpl* subframe = static_cast<RenderFrameHostImpl*>(
      CreateSubframe(static_cast<WebContentsImpl*>(shell()->web_contents()),
                     "child1", subframe_url, true /* wait_for_navigation */));

  EXPECT_FALSE(subframe->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  subframe->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->UnregisterScriptTool("test-tool");
  EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait());
}

// Test that binding `ModelContextHost` twice for the same document properly
// terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       ModelContextHostDuplicateBind) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  mojo::Remote<blink::mojom::ModelContextHost> host1;
  rfh->BindModelContextHost(host1.BindNewPipeAndPassReceiver());

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  mojo::Remote<blink::mojom::ModelContextHost> host2;
  rfh->BindModelContextHost(host2.BindNewPipeAndPassReceiver());

  EXPECT_EQ(bad_message::RFHI_WEBMCP_DUPLICATE_BIND, kill_waiter.Wait());
}

// Test that calling `BindModelContext()` twice for the same document properly
// terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       ModelContextHostDuplicateBindModelContext) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  mojo::PendingRemote<blink::mojom::ModelContext> remote1;
  auto receiver1 = remote1.InitWithNewPipeAndPassReceiver();
  script_tool_host->BindModelContext(std::move(remote1));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  mojo::PendingRemote<blink::mojom::ModelContext> remote2;
  auto receiver2 = remote2.InitWithNewPipeAndPassReceiver();
  script_tool_host->BindModelContext(std::move(remote2));

  EXPECT_EQ(bad_message::RFHI_WEBMCP_DUPLICATE_SET_RECEIVER,
            kill_waiter.Wait());
}

// Test that calling `GetScriptTools()` when the `tools` permission policy
// is not enabled properly terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       GetScriptToolsPermissionPolicyDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  GURL subframe_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  RenderFrameHostImpl* subframe = static_cast<RenderFrameHostImpl*>(
      CreateSubframe(static_cast<WebContentsImpl*>(shell()->web_contents()),
                     "child1", subframe_url, true /* wait_for_navigation */));

  EXPECT_FALSE(subframe->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  subframe->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->GetScriptTools({}, base::DoNothing());
  EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait());
}

// Test that calling `UnregisterScriptTool()` with an unknown name properly
// terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled,
                       UnregisterUnknownTool) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->UnregisterScriptTool("unknown-tool");
  EXPECT_EQ(bad_message::RFHI_WEBMCP_UNKNOWN_TOOL_NAME, kill_waiter.Wait());
}

// Test that calling `RegisterScriptTool()` with a non-HTTPS origin in
// `exposedTo` properly terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled,
                       ModelContextHostExposedNonHttpsOrigin) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  auto tool = blink::mojom::ScriptTool::New();
  tool->name = "test-tool";
  tool->description = "test description";
  tool->exposed_origins.push_back(url::Origin::Create(GURL("http://b.com")));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing());
  EXPECT_EQ(bad_message::RFHI_WEBMCP_EXPOSED_UNTRUSTWORTHY_ORIGIN,
            kill_waiter.Wait());
}

// Test that calling `RegisterScriptTool()` with a duplicate name properly
// terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled,
                       RegisterDuplicateScriptTool) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  auto tool1 = blink::mojom::ScriptTool::New();
  tool1->name = "test-tool";
  tool1->description = "test description";
  tool1->tool_owner_frame_token = rfh->GetFrameToken();
  tool1->origin = rfh->GetLastCommittedOrigin();

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  script_tool_host->RegisterScriptTool(std::move(tool1), base::DoNothing());

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  auto tool2 = blink::mojom::ScriptTool::New();
  tool2->name = "test-tool";  // Duplicate name
  tool2->description = "test description";
  tool2->tool_owner_frame_token = rfh->GetFrameToken();
  tool2->origin = rfh->GetLastCommittedOrigin();

  script_tool_host->RegisterScriptTool(std::move(tool2), base::DoNothing());
  EXPECT_EQ(bad_message::RFHI_WEBMCP_REGISTER_DUPLICATE_TOOL_NAME,
            kill_waiter.Wait());
}

// Test that calling `ExecuteRemoteScriptTool()` with an opaque target origin
// properly terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled,
                       ExecuteRemoteScriptToolOpaqueTargetOrigin) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  blink::FrameToken tool_owner_frame_token = rfh->GetFrameToken();

  script_tool_host->ExecuteRemoteScriptTool(
      base::UnguessableToken::Create(), tool_owner_frame_token, url::Origin(),
      "test-tool", "[]",
      base::BindOnce(
          [](const std::optional<std::string>& result, bool success) {}));
  EXPECT_EQ(bad_message::RFHI_WEBMCP_OPAQUE_TARGET_ORIGIN, kill_waiter.Wait());
}

// Test that `GetScriptTools()` clears `exposed_origins` so that the full ACL
// list is not leaked to caller renderers.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled,
                       GetScriptToolsExposedOriginsRedacted) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  EXPECT_TRUE(
      rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools));

  auto tool = blink::mojom::ScriptTool::New();
  tool->name = "test-tool";
  tool->description = "test description";
  tool->tool_owner_frame_token = rfh->GetFrameToken();
  tool->origin = rfh->GetLastCommittedOrigin();
  tool->exposed_origins.push_back(
      url::Origin::Create(GURL("https://friend.example")));
  tool->exposed_origins.push_back(
      url::Origin::Create(GURL("https://attacker.example")));

  mojo::Remote<blink::mojom::ModelContextHost> script_tool_host;
  rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver());

  base::RunLoop register_loop;
  script_tool_host->RegisterScriptTool(std::move(tool),
                                       register_loop.QuitClosure());
  register_loop.Run();

  base::RunLoop get_tools_loop;
  std::vector<blink::mojom::ScriptToolPtr> result_tools;
  script_tool_host->GetScriptTools(
      {}, base::BindLambdaForTesting(
              [&](std::vector<blink::mojom::ScriptToolPtr> tools) {
                result_tools = std::move(tools);
                get_tools_loop.Quit();
              }));
  get_tools_loop.Run();

  ASSERT_EQ(1u, result_tools.size());
  EXPECT_EQ("test-tool", result_tools[0]->name);
  EXPECT_TRUE(result_tools[0]->exposed_origins.empty());
}

class CorsExploitBrowserTest : public ContentBrowserTest {
 public:
  CorsExploitBrowserTest() = default;

  CorsExploitBrowserTest(const CorsExploitBrowserTest&) = delete;
  CorsExploitBrowserTest& operator=(const CorsExploitBrowserTest&) = delete;

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    SetupCrossSiteRedirector(embedded_test_server());
  }
};

// Test that receiving a commit with incorrect origin properly terminates the
// renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginOnCommit) {
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Navigate to a new URL, with an interceptor that replaces the origin with
  // one that does not match params.url.
  GURL url(embedded_test_server()->GetURL("/title2.html"));
  PwnCommitIPC(shell()->web_contents(), url, url,
               url::Origin::Create(GURL("http://bar.com/")));

  // Use LoadURL, as the test shouldn't wait for navigation commit.
  NavigationController& controller = shell()->web_contents()->GetController();
  controller.LoadURL(url, Referrer(), ui::PAGE_TRANSITION_LINK, std::string());
  EXPECT_NE(nullptr, controller.GetPendingEntry());
  EXPECT_EQ(url, controller.GetPendingEntry()->GetURL());

  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      root->current_frame_host()->GetProcess());

  // When the IPC message is received and validation fails, the process is
  // terminated. However, the notification for that should be processed in a
  // separate task of the message loop, so ensure that the process is still
  // considered alive.
  EXPECT_TRUE(
      root->current_frame_host()->GetProcess()->IsInitializedAndNotDead());

  EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait());
}

// Test that receiving a document.open() URL update with incorrect origin
// properly terminates the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       MismatchedOriginOnDocumentOpenURLUpdate) {
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  // Simulate a document.open() URL update with incorrect origin.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
  static_cast<mojom::FrameHost*>(rfh)->DidOpenDocumentInputStream(
      embedded_test_server()->GetURL("evil.com", "/title1.html"));

  // Ensure that the renderer process gets killed.
  EXPECT_EQ(AreAllSitesIsolatedForTesting()
                ? bad_message::RFH_CAN_COMMIT_URL_BLOCKED
                : bad_message::RFH_INVALID_ORIGIN_ON_COMMIT,
            kill_waiter.Wait());
}

// Test that same-document navigations cannot go cross-origin (even within the
// same site).
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       CrossOriginSameDocumentCommit) {
  GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Do a same-document navigation to a cross-origin URL/Origin (which match
  // each other, unlike the MismatchedOriginOnCommit), using an interceptor that
  // replaces the origin and URL. This intentionally uses a cross-origin but
  // same-site destination, to avoid failing Site Isolation checks.
  GURL dest_url(embedded_test_server()->GetURL("bar.foo.com", "/title2.html"));
  PwnCommitIPC(shell()->web_contents(), start_url, dest_url,
               url::Origin::Create(dest_url));
  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess());
  // ExecJs will sometimes finish before the renderer gets killed, so we must
  // ignore the result.
  std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
                       "history.pushState({}, '', location.href);");
  EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait());
}

// Test that same-document navigations cannot go cross-origin from about:blank
// (even within the same site). Uses a subframe to inherit an existing origin.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       CrossOriginSameDocumentCommitFromAboutBlank) {
  GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Create an about:blank iframe that inherits the origin.
  RenderFrameHost* subframe =
      CreateSubframe(static_cast<WebContentsImpl*>(shell()->web_contents()),
                     "child1", GURL(), false /* wait_for_navigation */);
  EXPECT_EQ(url::Origin::Create(start_url), subframe->GetLastCommittedOrigin());

  // Do a same-document navigation to another about:blank URL, but using a
  // different origin. This intentionally uses a cross-origin but same-site
  // origin to avoid triggering Site Isolation checks.
  GURL blank_url("about:blank#foo");
  GURL fake_url(embedded_test_server()->GetURL("bar.foo.com", "/"));
  PwnCommitIPC(shell()->web_contents(), blank_url, blank_url,
               url::Origin::Create(fake_url));
  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  // ExecJs will sometimes finish before the renderer gets killed, so we must
  // ignore the result.
  std::ignore = ExecJs(subframe, "location.hash='foo';");
  EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait());
}

// Test that same-document navigations cannot go cross-origin (even within the
// same site), in the case that allow_universal_access_from_file_urls is enabled
// but the last committed origin is not a file URL.  See also
// RenderFrameHostManagerTest.EnsureUniversalAccessFromFileSchemeSucceeds for
// the intended case that file URLs are allowed to go cross-origin.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       CrossOriginSameDocumentCommitUniversalAccessNonFile) {
  auto prefs = shell()->web_contents()->GetOrCreateWebPreferences();
  prefs.allow_universal_access_from_file_urls = true;
  shell()->web_contents()->SetWebPreferences(prefs);

  GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Do a same-document navigation to a cross-origin URL, using an interceptor
  // that replaces the URL but not the origin (to simulate the universal access
  // case, but for a non-file committed origin). This intentionally uses a
  // cross-origin but same-site destination, to avoid failing Site Isolation
  // checks.
  GURL dest_url(embedded_test_server()->GetURL("bar.foo.com", "/title2.html"));
  PwnCommitIPC(shell()->web_contents(), start_url, dest_url,
               url::Origin::Create(start_url));
  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess());
  // ExecJs will sometimes finish before the renderer gets killed, so we must
  // ignore the result.
  std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
                       "history.pushState({}, '', location.href);");
  EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait());
}

// Test that same-document navigations cannot claim to commit a file: origin
// when the last committed origin is not a file URL, even if
// allow_universal_access_from_file_urls is enabled. The universal access
// setting allows file: documents to navigate to other origins, but does not
// allow non-file documents to become file: documents.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       CrossOriginSameDocumentCommitUniversalAccessFileOrigin) {
  auto prefs = shell()->web_contents()->GetOrCreateWebPreferences();
  prefs.allow_universal_access_from_file_urls = true;
  shell()->web_contents()->SetWebPreferences(prefs);

  GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  url::Origin file_origin = url::Origin::Create(GURL("file:///"));
  EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()
                   ->HasOriginCheckExemptionForWebView(
                       rfh->GetProcess()->GetDeprecatedID(), file_origin));

  // Do a same-document navigation, using an interceptor that replaces the
  // origin with a file: origin while keeping the original URL. The browser
  // should reject this because the previously committed origin is not file:.
  PwnCommitIPC(shell()->web_contents(), start_url, start_url, file_origin);
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
  // ExecJs will sometimes finish before the renderer gets killed, so we must
  // ignore the result.
  std::ignore = ExecJs(rfh, "history.pushState({}, '', location.href);");
  EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait());

  // The process should not have been granted an origin check exemption based
  // on the rejected commit's origin.
  EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()
                   ->HasOriginCheckExemptionForWebView(
                       rfh->GetProcess()->GetDeprecatedID(), file_origin));
}

// Test that a file: URL document with allow_universal_access_from_file_urls
// enabled can perform multiple same-document pushState navigations to different
// cross-origin URLs without losing its file: committed origin or causing
// browser-side validation failures on subsequent pushState calls.
// NOTE: Universal access from file scheme behaves differently on macOS, so the
// test is disabled on macOS (crbug.com/981018).
#if BUILDFLAG(IS_MAC)
#define MAYBE_MultipleCrossOriginSameDocumentPushStateFromFileUrl \
  DISABLED_MultipleCrossOriginSameDocumentPushStateFromFileUrl
#else
#define MAYBE_MultipleCrossOriginSameDocumentPushStateFromFileUrl \
  MultipleCrossOriginSameDocumentPushStateFromFileUrl
#endif
IN_PROC_BROWSER_TEST_F(
    SecurityExploitBrowserTest,
    MAYBE_MultipleCrossOriginSameDocumentPushStateFromFileUrl) {
  auto prefs = shell()->web_contents()->GetOrCreateWebPreferences();
  prefs.allow_universal_access_from_file_urls = true;
  shell()->web_contents()->SetWebPreferences(prefs);

  GURL file_url = GetTestUrl("", "simple_page.html");
  ASSERT_TRUE(file_url.SchemeIsFile());
  ASSERT_TRUE(NavigateToURL(shell(), file_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  url::Origin initial_origin = rfh->GetLastCommittedOrigin();
  EXPECT_EQ(url::kFileScheme, initial_origin.scheme());

  // Perform first pushState to a cross-origin HTTPS URL.
  EXPECT_TRUE(
      ExecJs(rfh, "history.pushState({}, '', 'https://example.com/1');"));
  EXPECT_TRUE(rfh->IsRenderFrameLive());
  EXPECT_EQ(initial_origin, rfh->GetLastCommittedOrigin());

  // Perform second pushState to a different cross-origin HTTPS URL.
  // Verify that the browser still retains the file: origin on
  // last_committed_origin_ and allows the second same-document navigation
  // to succeed.
  EXPECT_TRUE(
      ExecJs(rfh, "history.pushState({}, '', 'https://example.org/2');"));
  EXPECT_TRUE(rfh->IsRenderFrameLive());
  EXPECT_EQ(initial_origin, rfh->GetLastCommittedOrigin());
}

// Test that receiving a commit with a URL with an invalid scheme properly
// terminates the renderer process. See https://crbug.com/324934416.
// TODO(crbug.com/40092527): This test can be removed once the browser
// stops using cross-document URLs computed by the renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, BadUrlSchemeOnCommit) {
  GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  // Navigate to a new URL, with an interceptor that replaces the URL with one
  // that has an illegal scheme. Note that most cross-document navigations where
  // the renderer's commit URL disagrees with the browser's expectation will
  // currently be caught by a DCHECK in debug builds, but this case still works
  // in release builds until the browser process becomes the authority for
  // cross-document URLs in https://crbug.com/888079. For now, we can test this
  // case and avoid the DCHECK by claiming to commit about:blank#blocked, which
  // is given an exception in RenderFrameHostImpl's CalculateLoadingURL.
  GURL url("about:blank#blocked");
  GURL bad_scheme_url("bar:com");
  PwnCommitIPC(shell()->web_contents(), url, bad_scheme_url,
               url::Origin::Create(url));

  RenderProcessHost* process = root->current_frame_host()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(process);

  // ExecJs will sometimes finish before the renderer gets killed, so we must
  // ignore the result.
  std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
                       "location.href = 'about:blank#blocked';");

  // When the IPC message is received and validation fails, the process is
  // terminated. However, the notification for that should be processed in a
  // separate task of the message loop, so ensure that the process is still
  // considered alive.
  EXPECT_TRUE(process->IsInitializedAndNotDead());

  EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
}

// Test that receiving a same-document commit with a URL with an invalid scheme
// properly terminates the renderer process. See https://crbug.com/324934416.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       BadUrlSchemeOnSameDocumentCommit) {
  GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Do a same-document navigation to a URL with an incorrect scheme, but with
  // the expected origin, using an interceptor that replaces the URL.
  GURL dest_url("bar:com");
  PwnCommitIPC(shell()->web_contents(), start_url, dest_url,
               url::Origin::Create(start_url));
  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess());
  // ExecJs will sometimes finish before the renderer gets killed, so we must
  // ignore the result.
  std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
                       "history.pushState({}, '', location.href);");
  EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
}

namespace {

// Interceptor that replaces |interface_params| with the specified
// value for the first DidCommitProvisionalLoad message it observes in the given
// |web_contents| while in scope.
class ScopedInterfaceParamsReplacer : public DidCommitNavigationInterceptor {
 public:
  ScopedInterfaceParamsReplacer(
      WebContents* web_contents,
      mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override)
      : DidCommitNavigationInterceptor(web_contents),
        params_override_(std::move(params_override)) {}

  ScopedInterfaceParamsReplacer(const ScopedInterfaceParamsReplacer&) = delete;
  ScopedInterfaceParamsReplacer& operator=(
      const ScopedInterfaceParamsReplacer&) = delete;

  ~ScopedInterfaceParamsReplacer() override = default;

 protected:
  bool WillProcessDidCommitNavigation(
      RenderFrameHost* render_frame_host,
      NavigationRequest* navigation_request,
      mojom::DidCommitProvisionalLoadParamsPtr*,
      mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
      override {
    interface_params->Swap(&params_override_);

    return true;
  }

 private:
  mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override_;
};

}  // namespace

// Test that, as a general rule, not receiving new
// DidCommitProvisionalLoadInterfaceParamsPtr for a cross-document navigation
// properly terminates the renderer process. There is one exception to this
// rule, see: RenderFrameHostImplBrowserTest.
// InterfaceProviderRequestIsOptionalForFirstCommit.
// TODO(crbug.com/40519010): when all clients are converted to use
// BrowserInterfaceBroker, PendingReceiver<InterfaceProvider>-related code will
// be removed.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       MissingInterfaceProviderOnNonSameDocumentCommit) {
  const GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  const GURL non_same_document_url(
      embedded_test_server()->GetURL("/title2.html"));

  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* frame = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess());

  NavigationHandleObserver navigation_observer(shell()->web_contents(),
                                               non_same_document_url);
  ScopedInterfaceParamsReplacer replacer(shell()->web_contents(), nullptr);
  EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), non_same_document_url));
  EXPECT_EQ(bad_message::RFH_INTERFACE_PROVIDER_MISSING, kill_waiter.Wait());

  // Verify that the death of the renderer process doesn't leave behind and
  // leak NavigationRequests - see https://crbug.com/869193.
  EXPECT_FALSE(frame->HasPendingCommitNavigation());
  EXPECT_FALSE(navigation_observer.has_committed());
  EXPECT_TRUE(navigation_observer.is_error());
  EXPECT_TRUE(navigation_observer.last_committed_url().is_empty());
  EXPECT_EQ(net::ERR_ABORTED, navigation_observer.net_error_code());
}

// Test that a compromised renderer cannot ask to upload an arbitrary file in
// OpenURL.  This is a regression test for https://crbug.com/726067.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OpenUrl_ResourceRequestBody) {
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  GURL target_url(embedded_test_server()->GetURL("/echoall"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();

  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      root->current_frame_host()->GetProcess());

  // Prepare a file to upload.
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::ScopedTempDir temp_dir;
  base::FilePath file_path;
  std::string file_content("test-file-content");
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
  ASSERT_TRUE(base::WriteFile(file_path, file_content));

  // Simulate an OpenURL Mojo method asking to POST a file that the renderer
  // shouldn't have access to.
  auto params = CreateOpenURLParams(target_url);
  params->post_body = new network::ResourceRequestBody;
  params->post_body->AppendFileRange(file_path, 0, file_content.size(),
                                     base::Time());
  params->should_replace_current_entry = true;

  static_cast<mojom::FrameHost*>(root->current_frame_host())
      ->OpenURL(std::move(params));

  // Verify that the malicious navigation did not commit the navigation to
  // |target_url|.
  EXPECT_EQ(start_url, root->current_frame_host()->GetLastCommittedURL());

  // Verify that the malicious renderer got killed.
  EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait());
}

// Tests that a compromised renderer cannot probe parent directories using
// Windows path traversal variants like ".. " via FileUtilitiesHost. See
// https://crbug.com/498887785.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       PathTraversalWithDotDotSpace) {
  base::ScopedAllowBlockingForTesting allow_blocking;

  // Create a temp directory with a granted dir.
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

  base::FilePath granted_dir = temp_dir.GetPath().AppendASCII("granted");
  ASSERT_TRUE(base::CreateDirectory(granted_dir));

  // Grant the renderer access to the granted directory.
  RenderProcessHost* process =
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess();
  ChildProcessId child_id = process->GetID();

  auto* security_policy = ChildProcessSecurityPolicyImpl::GetInstance();
  security_policy->GrantReadFile(child_id, granted_dir);

  EXPECT_TRUE(security_policy->CanReadFile(child_id, granted_dir));

  // Construct a malicious path that traverses out of the granted dir.
  base::FilePath malicious_path = granted_dir.Append(FILE_PATH_LITERAL(".. "));

  // Bind the FileUtilitiesHost interface.
  mojo::Remote<blink::mojom::FileUtilitiesHost> file_utilities;
  FileUtilitiesHostImpl::Create(child_id,
                                file_utilities.BindNewPipeAndPassReceiver());

  // Ask for file info on the malicious path.
  std::optional<base::File::Info> file_info;
  base::RunLoop run_loop;
  file_utilities->GetFileInfo(
      malicious_path, base::BindLambdaForTesting(
                          [&](const std::optional<base::File::Info>& info) {
                            file_info = info;
                            run_loop.Quit();
                          }));
  run_loop.Run();

  // The FileInfo request should fail. Prior to the fix in crbug.com/498887785,
  // this worked on Windows where ".. " at the end of the path was not
  // recognized as a parent directory traversal in ChildProcessSecurityPolicy
  // security checks.
  EXPECT_FALSE(file_info.has_value());
}

// A simple Mojo client that reads a Blob's contents and captures the
// final net::Error status code upon completion. This is used to verify
// whether the browser process blocked a file read (e.g., ERR_FAILED)
// or if the read proceeded normally.
class TestBlobReaderClient : public blink::mojom::BlobReaderClient {
 public:
  explicit TestBlobReaderClient(
      mojo::PendingReceiver<blink::mojom::BlobReaderClient> receiver)
      : receiver_(this, std::move(receiver)) {}

  void OnCalculatedSize(uint64_t total_size,
                        uint64_t expected_content_size) override {}

  void OnComplete(int32_t status, uint64_t data_length) override {
    status_ = status;
    run_loop_.Quit();
  }

  void Wait() { run_loop_.Run(); }

  int32_t status_ = net::OK;

 private:
  mojo::Receiver<blink::mojom::BlobReaderClient> receiver_;
  base::RunLoop run_loop_;
};

// Tests that a compromised renderer cannot access actual files in a non-granted
// parent directory using corner case parent traversal variants like ".. ". See
// https://crbug.com/498887785. Note that this test passes both before and after
// the fix for that bug, because it turned out that Windows's corner case
// behavior for ".. " only occurs at the end of a path and not in the middle,
// but the test is still useful to catch regressions.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       FileReadBlockedForInvalidPathTraversalWithDotDotSpace) {
  base::ScopedAllowBlockingForTesting allow_blocking;

  // Create a temp directory with a granted dir and a secret file.
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

  base::FilePath granted_dir = temp_dir.GetPath().AppendASCII("granted");
  base::FilePath secret_file = temp_dir.GetPath().AppendASCII("secret.txt");

  ASSERT_TRUE(base::CreateDirectory(granted_dir));
  ASSERT_TRUE(base::WriteFile(secret_file, "secret"));

  // Grant the renderer access to the granted directory.
  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  ChildProcessId child_id = rfh->GetProcess()->GetID();

  auto* security_policy = ChildProcessSecurityPolicyImpl::GetInstance();
  security_policy->GrantReadFile(child_id, granted_dir);

  // Verify the setup assumptions.
  ASSERT_TRUE(security_policy->CanReadFile(child_id, granted_dir));
  ASSERT_FALSE(security_policy->CanReadFile(child_id, secret_file));

  // Construct a malicious path that traverses out of the granted dir
  // using ".. " (dot-dot-space).
  base::FilePath malicious_path =
      granted_dir.AppendASCII(".. ").AppendASCII("secret.txt");

  // Try to read the secret file via Blob interfaces in the rest of this test
  // and ensure that this fails (i.e., that the dot-dot-space pattern doesn't
  // accidentally grant a compromised renderer access to a file outside
  // `granted_dir`). First, register a file-backed blob with the malicious path
  // via Mojo.
  mojo::AssociatedRemote<blink::mojom::FileBackedBlobFactory> factory;
  rfh->BindFileBackedBlobFactory(
      factory.BindNewEndpointAndPassDedicatedReceiver());

  auto element = blink::mojom::DataElementFile::New(
      malicious_path, /*offset=*/0, /*length=*/6,
      /*expected_modification_time=*/std::nullopt);

  mojo::Remote<blink::mojom::Blob> blob;
  factory->RegisterBlob(blob.BindNewPipeAndPassReceiver(), "uuid-1234",
                        "text/plain", std::move(element));

  // Attempt to read the Blob's contents.
  mojo::ScopedDataPipeProducerHandle producer;
  mojo::ScopedDataPipeConsumerHandle consumer;
  ASSERT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(nullptr, producer, consumer));

  mojo::PendingRemote<blink::mojom::BlobReaderClient> client_remote;
  TestBlobReaderClient client(client_remote.InitWithNewPipeAndPassReceiver());

  blob->ReadAll(std::move(producer), std::move(client_remote));
  client.Wait();

  std::string blob_contents;
  EXPECT_TRUE(mojo::BlockingCopyToString(std::move(consumer), &blob_contents));

  // Verify the read results and make sure this fails.
  EXPECT_NE(net::OK, client.status_);
  EXPECT_EQ("", blob_contents);
}

// Forging a navigation commit after the initial empty document will result in a
// renderer kill, even if the URL used is about:blank.
// See https://crbug.com/766262 for an example advanced case that involves
// forging a frame's unique name.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       NonInitialAboutBlankRendererKill) {
  // Navigate normally.
  GURL url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));
  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  // Simulate an about:blank commit without a NavigationRequest. It will fail
  // because only initial commits are allowed to do this.
  auto params = mojom::DidCommitProvisionalLoadParams::New();
  params->did_create_new_entry = false;
  params->url = GURL("about:blank");
  params->referrer = blink::mojom::Referrer::New();
  params->transition = ui::PAGE_TRANSITION_LINK;
  params->should_update_history = false;
  params->method = "GET";
  params->page_state = blink::PageState::CreateFromURL(GURL("about:blank"));
  params->origin = url::Origin::Create(GURL("about:blank"));
  params->embedding_token = base::UnguessableToken::Create();
  params->document_sequence_number = 1;
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
  static_cast<mojom::FrameHost*>(rfh)->DidCommitProvisionalLoad(
      std::move(params),
      mojom::DidCommitProvisionalLoadInterfaceParams::New(
          mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker>()
              .InitWithNewPipeAndPassReceiver()));

  // Verify that the malicious renderer got killed.
  EXPECT_EQ(bad_message::RFH_NO_MATCHING_NAVIGATION_REQUEST_ON_COMMIT,
            kill_waiter.Wait());
}

namespace {

// Interceptor that replaces the embedding token in the
// DidCommitProvisionalLoadParams.
class DidCommitEmbeddingTokenReplacer : public DidCommitNavigationInterceptor {
 public:
  DidCommitEmbeddingTokenReplacer(WebContents* web_contents,
                                  base::UnguessableToken embedding_token)
      : DidCommitNavigationInterceptor(web_contents),
        embedding_token_(std::move(embedding_token)) {}

  DidCommitEmbeddingTokenReplacer(const DidCommitEmbeddingTokenReplacer&) =
      delete;
  DidCommitEmbeddingTokenReplacer& operator=(
      const DidCommitEmbeddingTokenReplacer&) = delete;

  ~DidCommitEmbeddingTokenReplacer() override = default;

 private:
  // DidCommitNavigationInterceptor:
  bool WillProcessDidCommitNavigation(
      RenderFrameHost* render_frame_host,
      NavigationRequest* navigation_request,
      mojom::DidCommitProvisionalLoadParamsPtr* params,
      mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
      override {
    (*params)->embedding_token = embedding_token_;
    return true;
  }

  base::UnguessableToken embedding_token_;
};

}  // namespace

// TODO(crbug.com/516558664): Test is failing on Linux.
#if BUILDFLAG(IS_LINUX)
#define MAYBE_AttemptUseStolenEmbedderToken \
  DISABLED_AttemptUseStolenEmbedderToken
#else
#define MAYBE_AttemptUseStolenEmbedderToken AttemptUseStolenEmbedderToken
#endif
// Verify that the renderer is terminated if a compromised renderer passes an
// embedding token used by another frame to
// RenderFrameHostImpl::DidCommitProvisionalLoad().
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       MAYBE_AttemptUseStolenEmbedderToken) {
  // Explicitly isolating a.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("a.com");

  const GURL kUrl(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), kUrl));
  WebContents* web_contents = shell()->web_contents();

  // Create a separate window and navigate it to b.com to get a valid,
  // registered embedding token from another frame.
  GURL kPopupUrl(embedded_test_server()->GetURL("b.com", "/title.html"));
  Shell* new_window = OpenPopup(shell(), kPopupUrl, "popup");
  std::optional<base::UnguessableToken> stolen_token =
      new_window->web_contents()->GetPrimaryMainFrame()->GetEmbeddingToken();
  ASSERT_TRUE(stolen_token.has_value());

  // Check that replacing the embedding token of the next navigation
  // with a fresh, unused embedding token doesn't terminate the renderer.
  {
    DidCommitEmbeddingTokenReplacer interceptor(
        web_contents, base::UnguessableToken::Create());
    const GURL kUrl2(embedded_test_server()->GetURL("a.com", "/title2.html"));
    EXPECT_TRUE(NavigateToURL(web_contents, kUrl2));
  }

  // Test that overwriting the embedding token of the next navigation
  // with a stolen token from another frame causes the navigation to fail and
  // terminates the compromised renderer process.
  {
    RenderProcessHostBadIpcMessageWaiter kill_waiter(
        web_contents->GetPrimaryMainFrame()->GetProcess());
    DidCommitEmbeddingTokenReplacer interceptor(web_contents,
                                                stolen_token.value());
    const GURL kUrl3(embedded_test_server()->GetURL("a.com", "/title3.html"));
    EXPECT_FALSE(NavigateToURL(shell(), kUrl3));
    EXPECT_EQ(bad_message::RFH_UNEXPECTED_EMBEDDING_TOKEN, kill_waiter.Wait());
  }
}

// Make sure that a renderer is terminated if it sends an invalid net error code
// in a DidFailLoadWithError() IPC. See https://crbug.com/407069514.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       DidFailLoadWithInvalidErrorCode) {
  GURL url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  // Navigate normally and wait for commit (but not full load).
  TestFrameNavigationObserver commit_observer(shell());
  GURL failed_url(embedded_test_server()->GetURL("/title2.html"));
  ASSERT_TRUE(ExecJs(shell(), JsReplace("location.href = $1;", failed_url)));
  commit_observer.WaitForCommit();

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  // Simulate a DidFailLoadWithError message with an invalid error code.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
  int32_t invalid_error_code = INT_MAX;
  ASSERT_FALSE(net::IsOkOrDefinedError(invalid_error_code));
  static_cast<blink::mojom::LocalFrameHost*>(rfh)->DidFailLoadWithError(
      failed_url, invalid_error_code);

  // Verify that the malicious renderer got killed.
  EXPECT_EQ(bad_message::RFHI_INVALID_NET_ERROR_CODE, kill_waiter.Wait());
}

class SecurityExploitBrowserTestMojoBlobURLs
    : public SecurityExploitBrowserTest {
 public:
  SecurityExploitBrowserTestMojoBlobURLs() = default;

  void TearDown() override {
    storage::BlobUrlRegistry::SetURLStoreCreationHookForTesting(nullptr);
  }
};

// Check that when site isolation is enabled, an origin can't create a blob URL
// for a different origin.  Similar to the test above, but checks the
// mojo-based Blob URL implementation.  See https://crbug.com/886976.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestMojoBlobURLs,
                       CreateMojoBlobURLInDifferentOrigin) {
  IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  RenderFrameHost* rfh = shell()->web_contents()->GetPrimaryMainFrame();

  // Intercept future blob URL registrations and overwrite the blob URL origin
  // with b.com.
  std::string target_origin = "http://b.com";
  std::string blob_path = "5881f76e-10d2-410d-8c61-ef210502acfd";

  base::RepeatingCallback<void(storage::BlobUrlRegistry*, mojo::ReceiverId)>
      blob_url_registry_intercept_hook;

  blob_url_registry_intercept_hook =
      base::BindRepeating(&BlobURLStoreInterceptor::Intercept,
                          GURL("blob:" + target_origin + "/" + blob_path));
  storage::BlobUrlRegistry::SetURLStoreCreationHookForTesting(
      &blob_url_registry_intercept_hook);

  // Register a blob URL from the a.com main frame, which will go through the
  // interceptor above and be rewritten to register the blob URL with the b.com
  // origin. This should result in a kill because a.com should not be allowed
  // to create blob URLs outside of its own origin.
  content::RenderProcessHostBadMojoMessageWaiter crash_observer(
      rfh->GetProcess());

  // The renderer should always get killed, but sometimes ExecJs returns
  // true anyway, so just ignore the result.
  std::ignore = ExecJs(rfh, "URL.createObjectURL(new Blob(['foo']))");

  // If the process is killed, this test passes.
  EXPECT_EQ(
      "Received bad user message: "
      "URL with invalid origin passed to BlobURLStore::Register",
      crash_observer.Wait());
}

// Check that with site isolation enabled, an origin can't create a filesystem
// URL for a different origin.  See https://crbug.com/888001.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       CreateFilesystemURLInDifferentOrigin) {
  IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());

  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  // Block the renderer on operation that never completes, to shield it from
  // receiving unexpected browser->renderer IPCs that might CHECK.
  rfh->ExecuteJavaScriptWithUserGestureForTests(
      u"var r = new XMLHttpRequest();"
      u"r.open('GET', '/slow?99999', false);"
      u"r.send(null);"
      u"while (1);",
      base::NullCallback(), ISOLATED_WORLD_ID_GLOBAL);

  // Set up a blob ID and populate it with attacker-controlled value. This
  // is just using the blob APIs directly since creating arbitrary blobs is not
  // what is prohibited; this data is not in any origin.
  std::string payload = "<html><body>pwned.</body></html>";
  std::string payload_type = "text/html";
  std::unique_ptr<content::BlobHandle> blob = CreateMemoryBackedBlob(
      rfh->GetSiteInstance()->GetBrowserContext(), payload, payload_type);
  std::string blob_id = blob->GetUUID();

  // Target a different origin.
  std::string target_origin = "http://b.com";
  GURL target_url =
      GURL("filesystem:" + target_origin + "/temporary/exploit.html");

  // Note: a well-behaved renderer would always call Open first before calling
  // Create and Write, but it's actually not necessary for the original attack
  // to succeed, so we omit it. As a result there are some log warnings from the
  // quota observer.

  PwnMessageHelper::FileSystemCreate(rfh->GetProcess(), 23, target_url, false,
                                     false, false, rfh->GetStorageKey());

  // Write the blob into the file. If successful, this places an
  // attacker-controlled value in a resource on the target origin.
  PwnMessageHelper::FileSystemWrite(rfh->GetProcess(), 24, target_url, blob_id,
                                    0, rfh->GetStorageKey());

  // Now navigate to `target_url` in a subframe. It should not succeed, and the
  // subframe should not contain `payload`.
  TestNavigationObserver observer(shell()->web_contents());
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  NavigateFrameToURL(root->child_at(0), target_url);
  EXPECT_FALSE(observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_FILE_NOT_FOUND, observer.last_net_error_code());

  RenderFrameHost* attacked_rfh = root->child_at(0)->current_frame_host();
  std::string body =
      EvalJs(attacked_rfh, "document.body.innerText").ExtractString();
  EXPECT_TRUE(base::StartsWith(body, "Could not load the requested resource",
                               base::CompareCase::INSENSITIVE_ASCII))
      << " body=" << body;
}

// Verify that when a compromised renderer tries to navigate a remote frame to
// a disallowed URL (e.g., file URL), that navigation is blocked.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       BlockIllegalOpenURLFromRemoteFrame) {
  // Explicitly isolating a.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("a.com");

  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  FrameTreeNode* child = root->child_at(0);

  // Simulate an IPC message where the top frame asks the remote subframe to
  // navigate to a file: URL.
  SiteInstanceImpl* a_com_instance =
      root->current_frame_host()->GetSiteInstance();
  RenderFrameProxyHost* proxy =
      child->current_frame_host()
          ->browsing_context_state()
          ->GetRenderFrameProxyHost(a_com_instance->group());
  EXPECT_TRUE(proxy);

  TestNavigationObserver observer(shell()->web_contents());
  static_cast<mojom::FrameHost*>(proxy->frame_tree_node()->current_frame_host())
      ->OpenURL(CreateOpenURLParams(GURL("file:///")));
  observer.Wait();

  // Verify that the malicious navigation was blocked.  Currently, this happens
  // by rewriting the target URL to about:blank#blocked.
  //
  // TODO(alexmos): Consider killing the renderer process in this case, since
  // this security check is already enforced in the renderer process.
  EXPECT_EQ(GURL(kBlockedURL),
            child->current_frame_host()->GetLastCommittedURL());

  // Navigate to the starting page again to recreate the proxy, then try the
  // same malicious navigation with a chrome:// URL.
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  child = root->child_at(0);
  proxy = child->current_frame_host()
              ->browsing_context_state()
              ->GetRenderFrameProxyHost(a_com_instance->group());
  EXPECT_TRUE(proxy);

  TestNavigationObserver observer_2(shell()->web_contents());
  GURL chrome_url(std::string(kChromeUIScheme) + "://" +
                  std::string(kChromeUIGpuHost));
  static_cast<mojom::FrameHost*>(proxy->frame_tree_node()->current_frame_host())
      ->OpenURL(CreateOpenURLParams(chrome_url));
  observer_2.Wait();
  EXPECT_EQ(GURL(kBlockedURL),
            child->current_frame_host()->GetLastCommittedURL());
}

// Helper class to replace a particular URL as the client_side_redirect_url
// in BeginNavigationParams.
class ClientSideRedirectUrlReplacer : public FrameHostInterceptor {
 public:
  ClientSideRedirectUrlReplacer(WebContents* web_contents,
                                const GURL& url_to_inject)
      : FrameHostInterceptor(web_contents), url_to_inject_(url_to_inject) {}

  ClientSideRedirectUrlReplacer(const ClientSideRedirectUrlReplacer&) = delete;
  ClientSideRedirectUrlReplacer& operator=(
      const ClientSideRedirectUrlReplacer&) = delete;

  bool WillDispatchBeginNavigation(
      RenderFrameHost* render_frame_host,
      blink::mojom::CommonNavigationParamsPtr* common_params,
      blink::mojom::BeginNavigationParamsPtr* begin_params,
      mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
      mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client)
      override {
    if (is_activated_) {
      (*begin_params)->client_side_redirect_url = url_to_inject_;
      is_activated_ = false;
    }
    return true;
  }

  void Activate() { is_activated_ = true; }

 private:
  GURL url_to_inject_;
  bool is_activated_ = false;
};

// Verify that a compromised renderer can't poison client_side_redirect_url
// with a privileged URL (e.g. to navigate to it on reload /
// LoadOriginalRequestURL).
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       BlockIllegalClientSideRedirectUrl) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());

  GURL webui_url(GetWebUIURL(kChromeUIGpuHost));
  ClientSideRedirectUrlReplacer injector(web_contents, webui_url);

  // Setup the interceptor to inject the WebUI URL into the next
  // BeginNavigation's client_side_redirect_url.
  injector.Activate();

  // Trigger a normal renderer-initiated navigation to a benign URL. The
  // injector will poison the client_side_redirect_url in that IPC's
  // BeginNavigationParams.
  GURL next_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  TestNavigationManager nav_manager(web_contents, next_url);
  EXPECT_TRUE(ExecJs(web_contents, JsReplace("location.href = $1;", next_url)));

  // Wait for the navigation to finish.
  ASSERT_TRUE(nav_manager.WaitForNavigationFinished());
  EXPECT_TRUE(nav_manager.was_successful());
  EXPECT_EQ(next_url, web_contents->GetLastCommittedURL());

  // At this point, the WebUI URL should've been filtered out and replaced
  // with about:blank#blocked.
  EXPECT_EQ(GURL(kBlockedURL), web_contents->GetController()
                                   .GetLastCommittedEntry()
                                   ->GetOriginalRequestURL());

  // Simulate the user clicking "Request Desktop Site" or similar,
  // triggering a reload of the original request URL.
  TestNavigationObserver reload_observer(web_contents);
  web_contents->GetController().LoadOriginalRequestURL();
  reload_observer.Wait();

  // Ensure that the browser doesn't navigate to the WebUI URL, which should've
  // been filtered out when processing the corresponding BeginNavigation IPC.
  EXPECT_NE(webui_url, web_contents->GetLastCommittedURL());
  EXPECT_EQ(GURL(kBlockedURL), web_contents->GetLastCommittedURL());
}

// Verify that a compromised renderer can't set client_side_redirect_url to a
// cross-origin web URL that is not hosted by the process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       BlockCrossOriginClientSideRedirectUrl) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());

  GURL cross_origin_url(
      embedded_test_server()->GetURL("b.com", "/title2.html"));
  ClientSideRedirectUrlReplacer injector(web_contents, cross_origin_url);

  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      web_contents->GetPrimaryMainFrame()->GetProcess());

  injector.Activate();

  GURL next_url(embedded_test_server()->GetURL("a.com", "/title3.html"));
  ExecuteScriptAsync(web_contents, JsReplace("location.href = $1;", next_url));

  EXPECT_EQ(bad_message::RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL,
            kill_waiter.Wait());
}

// Verify that a sandboxed frame can legitimately perform client-side redirects
// without being terminated, but is terminated if it attempts to spoof a
// client_side_redirect_url that it does not host.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       SandboxedFrameClientSideRedirectUrl) {
  GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();

  // Create a sandboxed child frame at a.com.
  GURL child_url(embedded_test_server()->GetURL("a.com", "/title2.html"));
  std::string js_str = base::StringPrintf(
      "var frame = document.createElement('iframe'); "
      "frame.id = 'sandboxed_child'; "
      "frame.sandbox = 'allow-scripts'; "
      "frame.src = '%s'; "
      "document.body.appendChild(frame);",
      child_url.spec().c_str());
  EXPECT_TRUE(ExecJs(root->current_frame_host(), js_str));
  ASSERT_TRUE(WaitForLoadStop(web_contents));

  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
  ASSERT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kOrigin));

  // Legitimate client-side redirect in sandboxed frame: the sandboxed frame
  // navigates itself, and Blink populates client_side_redirect_url with
  // child_url. Verify that the navigation succeeds and the renderer is not
  // terminated.
  GURL next_url(embedded_test_server()->GetURL("a.com", "/title3.html"));
  TestNavigationObserver nav_observer(web_contents);
  EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1;", next_url)));
  nav_observer.Wait();
  EXPECT_TRUE(nav_observer.last_navigation_succeeded());

  // Malicious attempt: a compromised sandboxed renderer attempts to set
  // client_side_redirect_url to a cross-origin URL.
  subframe = root->child_at(0)->current_frame_host();
  GURL cross_origin_url(
      embedded_test_server()->GetURL("b.com", "/title2.html"));
  ClientSideRedirectUrlReplacer injector(web_contents, cross_origin_url);

  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  injector.Activate();

  GURL final_url(embedded_test_server()->GetURL("a.com", "/empty.html"));
  ExecuteScriptAsync(subframe, JsReplace("location.href = $1;", final_url));

  EXPECT_EQ(bad_message::RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL,
            kill_waiter.Wait());
}

// Regression test for crbug.com/495529018.
// Verify that a speculative RenderFrameHost cannot update FrameReplicationState
// in the BrowsingContextState that it shares with the FrameTreeNode's current
// cross-origin document.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       SpeculativeFrameCannotUpdateFrameReplicationState) {
  IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());

  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* child =
      web_contents->GetPrimaryFrameTree().root()->child_at(0);
  RenderFrameHostImpl* current_rfh = child->current_frame_host();

  // Start a cross-process navigation in the subframe so that a speculative
  // RenderFrameHost is created. The speculative RenderFrameHost shares the
  // FrameTreeNode's BrowsingContextState with the current document.
  GURL cross_site_url(embedded_test_server()->GetURL("c.com", "/title1.html"));
  TestNavigationManager nav_manager(web_contents, cross_site_url);
  EXPECT_TRUE(BeginNavigateToURLFromRenderer(child, cross_site_url));
  nav_manager.WaitForSpeculativeRenderFrameHostCreation();

  RenderFrameHostImpl* speculative_rfh =
      child->render_manager()->speculative_frame_host();
  ASSERT_TRUE(speculative_rfh);
  ASSERT_EQ(RenderFrameHostImpl::LifecycleStateImpl::kSpeculative,
            speculative_rfh->lifecycle_state());
  EXPECT_NE(speculative_rfh->GetProcess(), current_rfh->GetProcess());
  ASSERT_EQ(speculative_rfh->browsing_context_state().get(),
            current_rfh->browsing_context_state().get());

  const blink::mojom::FrameReplicationState& replication_state =
      child->current_replication_state();
  ASSERT_EQ(blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone,
            replication_state.insecure_request_policy);
  ASSERT_TRUE(replication_state.insecure_navigations_set.empty());
  ASSERT_FALSE(replication_state.has_received_user_gesture_before_nav);
  bool initial_is_ad_frame = replication_state.is_ad_frame;

  // Simulate a compromised renderer sending state updates from the speculative
  // RenderFrameHost before the navigation has committed. These should be
  // ignored and must not modify the replication state of the still-current
  // cross-origin document.
  static_cast<blink::mojom::LocalFrameHost*>(speculative_rfh)
      ->EnforceInsecureRequestPolicy(
          blink::mojom::InsecureRequestPolicy::kBlockAllMixedContent);
  static_cast<blink::mojom::LocalFrameHost*>(speculative_rfh)
      ->EnforceInsecureNavigationsSet({1u});
  static_cast<blink::mojom::LocalFrameHost*>(speculative_rfh)
      ->HadStickyUserActivationBeforeNavigationChanged(true);
  speculative_rfh->UpdateIsAdFrame(true);

  EXPECT_EQ(blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone,
            replication_state.insecure_request_policy);
  EXPECT_TRUE(replication_state.insecure_navigations_set.empty());
  EXPECT_FALSE(replication_state.has_received_user_gesture_before_nav);
  EXPECT_EQ(initial_is_ad_frame, replication_state.is_ad_frame);
}

class RemoteFrameHostInterceptor
    : public blink::mojom::RemoteFrameHostInterceptorForTesting {
 public:
  explicit RemoteFrameHostInterceptor(
      RenderFrameProxyHost* render_frame_proxy_host,
      const url::Origin& evil_origin)
      : evil_origin_(evil_origin),
        swapped_impl_(
            render_frame_proxy_host->frame_host_receiver_for_testing(),
            this) {}

  ~RemoteFrameHostInterceptor() override = default;

  RemoteFrameHost* GetForwardingInterface() override {
    return swapped_impl_.old_impl();
  }

  void RouteMessageEvent(
      const std::optional<blink::LocalFrameToken>& source_frame_token,
      const url::Origin& source_origin,
      const std::optional<url::Origin>& target_origin,
      blink::TransferableMessage message) override {
    // Forward the message to the actual RFPH replacing |source_origin| with the
    // "evil origin".
    GetForwardingInterface()->RouteMessageEvent(
        std::move(source_frame_token), evil_origin_, std::move(target_origin),
        std::move(message));
  }

  void OpenURL(blink::mojom::OpenURLParamsPtr params) override {
    intercepted_params_ = std::move(params);
  }

  blink::mojom::OpenURLParamsPtr GetInterceptedParams() {
    return std::move(intercepted_params_);
  }

 private:
  url::Origin evil_origin_;
  blink::mojom::OpenURLParamsPtr intercepted_params_;
  mojo::test::ScopedSwapImplForTesting<blink::mojom::RemoteFrameHost>
      swapped_impl_;
};

class CapabilityDelegationRemoteFrameHostInterceptor
    : public blink::mojom::RemoteFrameHostInterceptorForTesting {
 public:
  explicit CapabilityDelegationRemoteFrameHostInterceptor(
      RenderFrameProxyHost* render_frame_proxy_host)
      : swapped_impl_(
            render_frame_proxy_host->frame_host_receiver_for_testing(),
            this) {}

  ~CapabilityDelegationRemoteFrameHostInterceptor() override = default;

  RemoteFrameHost* GetForwardingInterface() override {
    return swapped_impl_.old_impl();
  }

  void RouteMessageEvent(
      const std::optional<blink::LocalFrameToken>& source_frame_token,
      const url::Origin& source_origin,
      const std::optional<url::Origin>& target_origin,
      blink::TransferableMessage message) override {
    // Inject the delegated capability into the message before forwarding.
    message.delegated_capability =
        blink::mojom::DelegatedCapability::kFullscreenRequest;
    GetForwardingInterface()->RouteMessageEvent(
        std::move(source_frame_token), source_origin, std::move(target_origin),
        std::move(message));
  }

  void OpenURL(blink::mojom::OpenURLParamsPtr params) override {
    intercepted_params_ = std::move(params);
  }

  blink::mojom::OpenURLParamsPtr GetInterceptedParams() {
    return std::move(intercepted_params_);
  }

 private:
  blink::mojom::OpenURLParamsPtr intercepted_params_;
  mojo::test::ScopedSwapImplForTesting<blink::mojom::RemoteFrameHost>
      swapped_impl_;
};

// Test verifying that a compromised renderer can't lie about the source_origin
// passed along with the RouteMessageEvent() mojo message.  See also
// https://crbug.com/915721.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PostMessageSourceOrigin) {
  // Explicitly isolating a.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("b.com");

  // Navigate to a page with an OOPIF.
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  // Sanity check of test setup: main frame and subframe should be isolated.
  WebContents* web_contents = shell()->web_contents();
  RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
  RenderFrameHost* subframe = ChildFrameAt(main_frame, 0);
  EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess());

  // We need to get ahold of the RenderFrameProxyHost representing the main
  // frame for the subframe's process, to install the mojo interceptor.
  FrameTreeNode* main_frame_node =
      static_cast<WebContentsImpl*>(shell()->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  FrameTreeNode* subframe_node = main_frame_node->child_at(0);
  SiteInstanceImpl* b_com_instance =
      subframe_node->current_frame_host()->GetSiteInstance();
  RenderFrameProxyHost* main_frame_proxy_host =
      main_frame_node->current_frame_host()
          ->browsing_context_state()
          ->GetRenderFrameProxyHost(b_com_instance->group());

  // Prepare to intercept the RouteMessageEvent IPC message that will come
  // from the subframe process.
  url::Origin evil_source_origin =
      web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin();
  RemoteFrameHostInterceptor mojo_interceptor(main_frame_proxy_host,
                                              evil_source_origin);

  // Post a message from the subframe to the cross-site parent and intercept the
  // associated IPC message, changing it to simulate a compromised subframe
  // renderer lying that the |source_origin| of the postMessage is the origin of
  // the parent (not of the subframe).
  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')"));
  EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_SOURCE_ORIGIN,
            kill_waiter.Wait());
}

// Regression test for browser-side validation of the allow-popups sandbox
// attribute. See https://crbug.com/487471101.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       WindowOpenDisallowedFromSandboxedFrame) {
  // Explicitly isolating b.com helps establish consistent expectations for the
  // sandboxed frame across platforms.
  IsolateOrigin("b.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  // Create cross-site sandboxed child frame. The frame lacks the allow-popups
  // attribute, so it should not be allowed to create popups.
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.sandbox = 'allow-scripts'; "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
  EXPECT_TRUE(
      subframe->GetSiteInstance()->GetSecurityPrincipal().IsSandboxed());
  EXPECT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kPopups));

  // Simulate that the b.com renderer is compromised and sends an IPC to open a
  // popup, bypassing renderer-side checks in blink::CreateNewWindow(). The
  // browser process should detect this and terminate the renderer.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
  params->target_url = GURL("about:blank");
  params->disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
  subframe->CreateNewWindow(std::move(params), base::DoNothing());
  EXPECT_EQ(bad_message::RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME,
            kill_waiter.Wait());
  EXPECT_FALSE(subframe->IsRenderFrameLive());
  EXPECT_EQ(1u, Shell::windows().size());
}

// Regression test for browser-side validation of the allow-modals sandbox
// attribute. A sandboxed frame without allow-modals should not be able to show
// modal dialogs (alert, confirm, prompt). This is a variant of
// WindowOpenDisallowedFromSandboxedFrame for the kModals sandbox flag.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       ModalDialogDisallowedFromSandboxedFrame) {
  IsolateOrigin("b.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  // Create cross-site sandboxed child frame. The frame lacks the allow-modals
  // attribute, so it should not be allowed to show modal dialogs.
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.sandbox = 'allow-scripts'; "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
  EXPECT_TRUE(
      subframe->GetSiteInstance()->GetSecurityPrincipal().IsSandboxed());
  EXPECT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kModals));

  // Simulate that the b.com renderer is compromised and sends an IPC to show
  // a modal dialog, bypassing renderer-side checks in LocalDOMWindow::alert().
  // The browser process should detect this and terminate the renderer.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  subframe->RunModalAlertDialog(u"test", false, base::DoNothing());
  EXPECT_EQ(bad_message::RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME,
            kill_waiter.Wait());
  EXPECT_FALSE(subframe->IsRenderFrameLive());
}

// Regression test for browser-side validation of POST submissions in
// CreateNewWindow. See https://crbug.com/487768779.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       CreateNewWindowWithInaccessibleFile) {
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  // Simulate that the renderer is compromised and sends an IPC to open a popup,
  // using a POST submission that includes a file the renderer does not have
  // access to. The browser process should detect this and kill the renderer.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
  mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
  params->target_url = main_url;
  params->disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB;
  scoped_refptr<network::ResourceRequestBody> request_body =
      new network::ResourceRequestBody();
  base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits");
  request_body->AppendFileRange(
      bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time());
  params->form_submission_post_data = std::move(request_body);
  main_frame->CreateNewWindow(std::move(params), base::DoNothing());
  EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait());
  EXPECT_FALSE(main_frame->IsRenderFrameLive());
  EXPECT_EQ(1u, Shell::windows().size());
}

// Test verifying that a compromised renderer can't lie about the source_origin
// passed along with the RouteMessageEvent() mojo message.  Similar to the test
// above, but exercises a scenario where the source origin is opaque and the
// precursor needs to be validated. This provides coverage for messages sent
// from sandboxed frames; see https://crbug.com/40606810 and
// https://crbug.com/325410297.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       PostMessageOpaqueSourceOrigin) {
  // Explicitly isolating b.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("b.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  // Create cross-site sandboxed child frame.
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.sandbox = 'allow-scripts'; "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  // Sanity check of test setup: main frame and subframe should be in separate
  // processes, and subframe should be sandboxed.
  FrameTreeNode* subframe_node = root->child_at(0);
  RenderFrameHostImpl* subframe = subframe_node->current_frame_host();
  EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess());
  EXPECT_TRUE(
      subframe->GetSiteInstance()->GetSecurityPrincipal().IsSandboxed());

  // Retrieve the RenderFrameProxyHost representing the main frame for the
  // subframe's process.
  RenderFrameProxyHost* main_frame_proxy_host =
      main_frame->browsing_context_state()->GetRenderFrameProxyHost(
          subframe->GetSiteInstance()->group());

  // Prepare to intercept the RouteMessageEvent IPC message that will come from
  // the subframe process. Set the fake source origin to an opaque origin with
  // a.com as the precursor.
  url::Origin precursor_origin = main_frame->GetLastCommittedOrigin();
  url::Origin evil_source_origin = precursor_origin.DeriveNewOpaqueOrigin();
  EXPECT_TRUE(evil_source_origin.opaque());
  EXPECT_EQ("a.com",
            evil_source_origin.GetTupleOrPrecursorTupleIfOpaque().host());

  RemoteFrameHostInterceptor mojo_interceptor(main_frame_proxy_host,
                                              evil_source_origin);

  // Post a message from the subframe to the cross-site parent and intercept the
  // associated IPC message, changing it to simulate a compromised subframe
  // renderer lying that the |source_origin| of the postMessage has an incorrect
  // precursor of a.com, rather than b.com. This should result in a renderer
  // kill.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')"));
  EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_SOURCE_ORIGIN,
            kill_waiter.Wait());
}

// Test verifying that a compromised renderer cannot delegate a capability
// (e.g. fullscreen) without transient user activation.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       PostMessageDelegatedCapabilityWithoutUserActivation) {
  // Explicitly isolating b.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("b.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  // Create cross-site child frame.
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  // main frame and subframe should be in separate processes.
  FrameTreeNode* subframe_node = root->child_at(0);
  RenderFrameHostImpl* subframe = subframe_node->current_frame_host();
  EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess());

  // Retrieve the RenderFrameProxyHost representing the main frame for the
  // subframe's process.
  RenderFrameProxyHost* main_frame_proxy_host =
      main_frame->browsing_context_state()->GetRenderFrameProxyHost(
          subframe->GetSiteInstance()->group());

  // Intercept the RouteMessageEvent IPC message that will come from
  // the subframe process.
  CapabilityDelegationRemoteFrameHostInterceptor mojo_interceptor(
      main_frame_proxy_host);

  // Post a message from the subframe to the cross-site parent and intercept the
  // associated IPC message, changing it to include a delegated capability.
  // Since there is no active or recently consumed user activation on the
  // subframe, this should result in a renderer kill.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')",
                     EXECUTE_SCRIPT_NO_USER_GESTURE));
  EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_DELEGATED_CAPABILITY,
            kill_waiter.Wait());
}

// Test verifying that a legitimate renderer with user activation CAN delegate a
// capability (e.g. fullscreen) to another frame.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       PostMessageDelegatedCapabilityWithUserActivation) {
  // Explicitly isolating b.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("b.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  // Create cross-site child frame.
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  // main frame and subframe should be in separate processes.
  FrameTreeNode* subframe_node = root->child_at(0);
  RenderFrameHostImpl* subframe = subframe_node->current_frame_host();
  EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess());

  // Retrieve the RenderFrameProxyHost representing the main frame for the
  // subframe's process.
  RenderFrameProxyHost* main_frame_proxy_host =
      main_frame->browsing_context_state()->GetRenderFrameProxyHost(
          subframe->GetSiteInstance()->group());

  // Intercept the RouteMessageEvent IPC message that will come from
  // the subframe process.
  CapabilityDelegationRemoteFrameHostInterceptor mojo_interceptor(
      main_frame_proxy_host);

  // Post a message from the subframe to the cross-site parent and intercept the
  // associated IPC message, changing it to include a delegated capability.
  // This time, we execute the script WITH user gesture (default ExecJs option).
  // The browser should allow this and NOT terminate the subframe process.
  EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')"));

  // Verify that the subframe process is still alive.
  EXPECT_TRUE(subframe->IsRenderFrameLive());
}

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       InvalidRemoteNavigationInitiator) {
  // Explicitly isolating a.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("a.com");

  // Navigate to a test page where the subframe is cross-site (and because of
  // IsolateOrigin call above in a separate process) from the main frame.
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  RenderProcessHost* main_process = main_frame->GetProcess();
  RenderFrameHost* subframe = ChildFrameAt(main_frame, 0);
  ASSERT_TRUE(subframe);
  RenderProcessHost* subframe_process = subframe->GetProcess();
  EXPECT_NE(main_process->GetDeprecatedID(),
            subframe_process->GetDeprecatedID());

  // Prepare to intercept OpenURL Mojo message that will come from
  // the main frame.
  FrameTreeNode* main_frame_node =
      static_cast<WebContentsImpl*>(shell()->web_contents())
          ->GetPrimaryFrameTree()
          .root();
  FrameTreeNode* child_node = main_frame_node->child_at(0);
  SiteInstanceImpl* a_com_instance =
      main_frame_node->current_frame_host()->GetSiteInstance();
  RenderFrameProxyHost* proxy =
      child_node->current_frame_host()
          ->browsing_context_state()
          ->GetRenderFrameProxyHost(a_com_instance->group());
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);
  {
    RemoteFrameHostInterceptor interceptor(proxy, url::Origin());

    // Have the main frame request navigation in the "remote" subframe.  This
    // will result in OpenURL Mojo message being sent to the
    // RenderFrameProxyHost.
    EXPECT_TRUE(ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
                       "window.frames[0].location = '/title1.html';"));

    // Change the intercepted message to simulate a compromised subframe
    // renderer lying that the |initiator_origin| is the origin of the
    // |subframe|.
    auto evil_params = interceptor.GetInterceptedParams();
    evil_params->initiator_origin = subframe->GetLastCommittedOrigin();

    // Inject the invalid IPC and verify that the renderer gets terminated.
    static_cast<mojom::FrameHost*>(main_frame)->OpenURL(std::move(evil_params));
  }

  EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
}

class BeginNavigationInitiatorReplacer : public FrameHostInterceptor {
 public:
  BeginNavigationInitiatorReplacer(
      WebContents* web_contents,
      std::optional<url::Origin> initiator_to_inject)
      : FrameHostInterceptor(web_contents),
        initiator_to_inject_(initiator_to_inject) {}

  BeginNavigationInitiatorReplacer(const BeginNavigationInitiatorReplacer&) =
      delete;
  BeginNavigationInitiatorReplacer& operator=(
      const BeginNavigationInitiatorReplacer&) = delete;

  bool WillDispatchBeginNavigation(
      RenderFrameHost* render_frame_host,
      blink::mojom::CommonNavigationParamsPtr* common_params,
      blink::mojom::BeginNavigationParamsPtr* begin_params,
      mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
      mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client)
      override {
    if (is_activated_) {
      (*common_params)->initiator_origin = initiator_to_inject_;
      is_activated_ = false;
    }

    return true;
  }

  void Activate() { is_activated_ = true; }

 private:
  std::optional<url::Origin> initiator_to_inject_;
  bool is_activated_ = false;
};

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       InvalidBeginNavigationInitiator) {
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());

  // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
  // the test creates the RenderFrameHostImpl that is the target of the IPC.
  BeginNavigationInitiatorReplacer injector(
      web_contents, url::Origin::Create(GURL("http://b.com")));

  // Explicitly isolating a.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("a.com");

  // Navigate to a test page that will be locked to a.com.
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(web_contents, main_url));

  // Start monitoring for renderer kills.
  RenderProcessHost* main_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);

  // Have the main frame navigate and lie that the initiator origin is b.com.
  injector.Activate();
  // Don't expect a response for the script, as the process may be killed
  // before the script sends its completion message.
  ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");

  // Verify that the renderer was terminated.
  EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
}

// Similar to the test above, but ensure that initiator origins are validated
// even for opaque origins.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       InvalidBeginNavigationOpaqueInitiator) {
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());

  // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
  // the test creates the RenderFrameHostImpl that is the target of the IPC.
  url::Origin injected_origin(url::Origin::Create(GURL("http://evil.com")));
  injected_origin = injected_origin.DeriveNewOpaqueOrigin();
  BeginNavigationInitiatorReplacer injector(web_contents, injected_origin);

  // Explicitly isolating b.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("b.com");

  // Navigate to a test page at a.com.
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(web_contents, main_url));

  // Add a cross-site sandboxed child frame at b.com.
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.sandbox = 'allow-scripts'; "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  // Sanity check of test setup: main frame and subframe should be in separate
  // processes, and subframe should be sandboxed.
  FrameTreeNode* subframe_node = root->child_at(0);
  RenderFrameHostImpl* subframe = subframe_node->current_frame_host();
  EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess());
  EXPECT_TRUE(
      subframe->GetSiteInstance()->GetSecurityPrincipal().IsSandboxed());

  // Start monitoring for renderer kills.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());

  // Have the sandboxed subframe navigate and lie that the initiator origin is
  // an opaque origin with the precursor of evil.com instead of b.com.
  injector.Activate();
  // Don't expect a response for the script, as the process may be killed
  // before the script sends its completion message.
  ExecuteScriptAsync(subframe, "window.location = '/title2.html';");

  // Verify that the renderer was terminated.
  EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
}

// Similar to the test above, but ensure that opaque initiator origins are
// validated even when the frame sending BeginNavigation is currently showing an
// error page. Some subframe error pages are not currently process-isolated, so
// a renderer for the embedding site can send IPCs on their behalf and must not
// be allowed to claim an arbitrary precursor.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       InvalidBeginNavigationOpaqueInitiatorFromErrorPage) {
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());

  // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
  // the test creates the RenderFrameHostImpl that is the target of the IPC.
  url::Origin injected_origin =
      url::Origin::Create(GURL("http://evil.com")).DeriveNewOpaqueOrigin();
  BeginNavigationInitiatorReplacer injector(web_contents, injected_origin);

  // Explicitly isolating a.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("a.com");

  // Navigate to a test page that will be locked to a.com.
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(web_contents, main_url));
  RenderFrameHostImpl* main_frame =
      web_contents->GetPrimaryFrameTree().root()->current_frame_host();

  // Set CSP to block iframes, so the subframe navigation below results in an
  // error page that stays in the current (a.com) process.
  EXPECT_TRUE(ExecJs(main_frame,
                     "var meta = document.createElement('meta');"
                     "meta.httpEquiv = 'Content-Security-Policy';"
                     "meta.content = \"frame-src 'none'\";"
                     "document.head.appendChild(meta);"));

  // Create a subframe that fails to load and commits an error page.
  GURL blocked_url(embedded_test_server()->GetURL("a.com", "/title2.html"));
  TestNavigationObserver error_observer(web_contents);
  EXPECT_TRUE(
      ExecJs(main_frame, JsReplace("var f = document.createElement('iframe');"
                                   "f.src = $1;"
                                   "document.body.appendChild(f);",
                                   blocked_url)));
  error_observer.Wait();
  EXPECT_FALSE(error_observer.last_navigation_succeeded());

  RenderFrameHostImpl* error_frame =
      main_frame->child_at(0)->current_frame_host();
  ASSERT_TRUE(error_frame->IsErrorDocument());

  // Start monitoring for renderer kills.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(error_frame->GetProcess());

  // Have the error page subframe navigate and lie that the initiator origin is
  // an opaque origin with the precursor of evil.com. Note that an absolute URL
  // is required here, since relative URLs in error documents resolve against
  // chrome-error://chromewebdata/ and would be blocked in the renderer.
  injector.Activate();
  // Don't expect a response for the script, as the process may be killed
  // before the script sends its completion message.
  ExecuteScriptAsync(error_frame,
                     JsReplace("window.location = $1;", blocked_url));

  // Verify that the renderer was terminated.
  EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       MissingBeginNavigationInitiator) {
  // Prepare to intercept BeginNavigation mojo IPC.  This has to be done before
  // the test creates the RenderFrameHostImpl that is the target of the IPC.
  WebContents* web_contents = shell()->web_contents();
  BeginNavigationInitiatorReplacer injector(web_contents, std::nullopt);

  // Navigate to a test page.
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(web_contents, main_url));

  // Start monitoring for renderer kills.
  RenderProcessHost* main_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);

  // Have the main frame submit a BeginNavigation IPC with a missing initiator.
  injector.Activate();
  // Don't expect a response for the script, as the process may be killed
  // before the script sends its completion message.
  ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");

  // Verify that the renderer was terminated.
  EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_MISSING_INITIATOR_ORIGIN,
            kill_waiter.Wait());
}

// Ensure that DidChangeOpener IPC messages do not succeed if they attempt to
// assign an opener from a different Browsing Context Group (BCG).
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OpenerChangeDisallowedAcrossBrowsingInstances) {
  // Explicitly isolate a.test to ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("a.test");

  // Force process reuse so tab 1 and tab 2 are in the same renderer process.
  // This guarantees that the opener token (A1) is valid in the same process as
  // the proxy in A2's SiteInstance that will be used to send the spoofed
  // DidChangeOpener IPC later.
  RenderProcessHost::SetMaxRendererProcessCount(1);

  // Navigate to A1.
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHostImpl* tab1_rfh = web_contents->GetPrimaryMainFrame();
  FrameTreeNode* tab1_root = tab1_rfh->frame_tree_node();

  Shell* tab2 = CreateBrowser();
  // Create an unrelated window and navigate it to A2. Workaround: navigate
  // `tab2` to a random site before navigating to attacker's site to ensure that
  // `tab2` shares the process with tab1, rather than staying in its initial
  // process.
  ASSERT_TRUE(NavigateToURL(
      tab2, embedded_test_server()->GetURL("d.test", "/title3.html")));
  ASSERT_TRUE(NavigateToURL(
      tab2, embedded_test_server()->GetURL("a.test", "/title2.html")));
  RenderFrameHostImpl* tab2_rfh = static_cast<RenderFrameHostImpl*>(
      tab2->web_contents()->GetPrimaryMainFrame());
  FrameTreeNode* tab2_root = tab2_rfh->frame_tree_node();
  EXPECT_FALSE(tab1_rfh->GetSiteInstance()->IsRelatedSiteInstance(
      tab2_rfh->GetSiteInstance()));
  EXPECT_EQ(tab1_rfh->GetProcess(), tab2_rfh->GetProcess());

  // Open a cross-site page B in the same BCG as A2.
  GURL cross_site_url(embedded_test_server()->GetURL("b.test", "/title3.html"));
  Shell* tab3 = OpenPopup(tab2, cross_site_url, "foo");
  RenderFrameHostImpl* tab3_rfh = static_cast<RenderFrameHostImpl*>(
      tab3->web_contents()->GetPrimaryMainFrame());
  FrameTreeNode* tab3_root = tab3_rfh->frame_tree_node();
  EXPECT_EQ(tab3_root->opener(), tab2_root);
  EXPECT_TRUE(tab2_rfh->GetSiteInstance()->IsRelatedSiteInstance(
      tab3_rfh->GetSiteInstance()));

  // Grab the token of the A1 document that will be set as the new opener.
  blink::LocalFrameToken opener_token = tab1_rfh->GetFrameToken();

  // Look up the proxy that represents B in A2's SiteInstance. This is the
  // target of the malicious DidChangeOpener IPC. This should exist as long as
  // a.test and b.test are process-isolated from each other.
  RenderFrameProxyHost* proxy =
      tab3_rfh->browsing_context_state()->GetRenderFrameProxyHost(
          tab2_rfh->GetSiteInstance()->group());
  ASSERT_TRUE(proxy);

  // Simulate the vulnerability by sending the DidChangeOpener IPC using the
  // token of the frame in the wrong BCG. This goes to the
  // RenderFrameHostManager of tab3, trying to set its opener to the FTN that's
  // in the other BCG.
  proxy->DidChangeOpener(opener_token);

  // The opener should *not* be updated to tab1_root. It should stay the same,
  // still pointing to tab2.
  EXPECT_NE(tab3_root->opener(), tab1_root);
  EXPECT_EQ(tab3_root->opener(), tab2_root);
}

// Verify that a compromised renderer can't use a BFCached page and a rogue
// DidChangeOpener IPC to establish an opener relationship with a
// cross-BrowsingInstance page.
//
// Setup:
//   Tab 1: A1 in BCG1 -> navigates to WebUI in BCG3, A1 gets bfcached
//   Tab 2: A2 in BCG2
//   Tab 3: B in BCG2
// Attacker controls both A and B, and executes DidChangeOpener on a proxy for
// tab 3 in A2's SiteInstance, asking the browser process to update tab3's
// opener to A1. A1 is bfcached, and the test ensures that the opener isn't set
// to the WebUI page which is the current RFH in A1's FTN.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OpenerChangeDisallowedAcrossBrowsingInstances_Bfcache) {
  // This test assumes that back-forward cache is enabled.
  if (!IsBackForwardCacheEnabled()) {
    GTEST_SKIP();
  }

  // Explicitly isolate a.test to ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("a.test");

  // Force process reuse so tab 1 and tab 2 are in the same renderer process.
  // This guarantees that the opener token (A1) is valid in the same process as
  // the proxy in A2's SiteInstance that will be used to send the spoofed
  // DidChangeOpener IPC later.
  RenderProcessHost::SetMaxRendererProcessCount(1);

  // Navigate to A1.
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHostImpl* tab1_rfh = web_contents->GetPrimaryMainFrame();
  FrameTreeNode* tab1_root = tab1_rfh->frame_tree_node();

  Shell* tab2 = CreateBrowser();
  // Create an unrelated window and navigate it to A2. Workaround: navigate
  // `tab2` to a random site before navigating to attacker's site to ensure that
  // `tab2` shares the process with tab1, rather than staying in its initial
  // process.
  ASSERT_TRUE(NavigateToURL(
      tab2, embedded_test_server()->GetURL("d.test", "/title3.html")));
  ASSERT_TRUE(NavigateToURL(
      tab2, embedded_test_server()->GetURL("a.test", "/title2.html")));
  RenderFrameHostImpl* tab2_rfh = static_cast<RenderFrameHostImpl*>(
      tab2->web_contents()->GetPrimaryMainFrame());
  FrameTreeNode* tab2_root = tab2_rfh->frame_tree_node();
  EXPECT_FALSE(tab1_rfh->GetSiteInstance()->IsRelatedSiteInstance(
      tab2_rfh->GetSiteInstance()));
  EXPECT_EQ(tab1_rfh->GetProcess(), tab2_rfh->GetProcess());

  // Open a cross-site page B (still controlled by the attacker) in the
  // same BCG as A2.
  GURL attacker_cross_site_url(
      embedded_test_server()->GetURL("b.test", "/title3.html"));
  Shell* tab3 = OpenPopup(tab2, attacker_cross_site_url, "foo");
  RenderFrameHostImpl* tab3_rfh = static_cast<RenderFrameHostImpl*>(
      tab3->web_contents()->GetPrimaryMainFrame());
  FrameTreeNode* tab3_root = tab3_rfh->frame_tree_node();
  EXPECT_EQ(tab3_root->opener(), tab2_root);
  EXPECT_TRUE(tab2_rfh->GetSiteInstance()->IsRelatedSiteInstance(
      tab3_rfh->GetSiteInstance()));

  // Grab the token of the A1 document that will be set as the new opener.
  blink::LocalFrameToken opener_token = tab1_rfh->GetFrameToken();

  // Navigate A1 to a WebUI URL. This should force a BrowsingInstance swap. The
  // original RenderFrameHost is placed into BFCache. Note that the popups
  // created so far don't stop BFCache from working because they're in a
  // separate BCG.
  GURL chrome_url(std::string(kChromeUIScheme) + "://" +
                  std::string(kChromeUIGpuHost));
  EXPECT_TRUE(NavigateToURL(shell(), chrome_url));
  RenderFrameHostImpl* tab1_webui_rfh = tab1_root->current_frame_host();
  EXPECT_NE(tab1_rfh, tab1_webui_rfh);
  EXPECT_TRUE(tab1_rfh->IsInBackForwardCache());
  EXPECT_FALSE(tab1_webui_rfh->GetSiteInstance()->IsRelatedSiteInstance(
      tab2_rfh->GetSiteInstance()));
  EXPECT_FALSE(tab1_webui_rfh->GetSiteInstance()->IsRelatedSiteInstance(
      tab3_rfh->GetSiteInstance()));

  // Look up the proxy that represents B in A2's SiteInstance (both
  // controlled by the attacker). This is the target of the malicious
  // DidChangeOpener IPC. This should exist as long as a.test and b.test
  // are process-isolated from each other.
  RenderFrameProxyHost* proxy =
      tab3_rfh->browsing_context_state()->GetRenderFrameProxyHost(
          tab2_rfh->GetSiteInstance()->group());
  ASSERT_TRUE(proxy);

  // Simulate the vulnerability by sending the DidChangeOpener IPC using the
  // token of the BFCached frame. This goes to the RenderFrameHostManager of
  // tab3, trying to set its opener to the FTN that's currently showing the
  // WebUI page.
  proxy->DidChangeOpener(opener_token);

  // The opener should *not* be updated to tab1_root. It should stay the same,
  // still pointing to tab2.
  EXPECT_NE(tab3_root->opener(), tab1_root);
  EXPECT_EQ(tab3_root->opener(), tab2_root);

  // Normally, opener updates trigger proxy creation to ensure that the frame
  // that had its opener updated has access to the new opener chain. Ensure we
  // didn't create a RenderFrameProxyHost for the WebUI page in tab3's
  // SiteInstanceGroup, connecting the WebUI page to the attacker-controlled
  // tab3.
  RenderFrameProxyHost* webui_to_non_webui_proxy =
      tab1_webui_rfh->browsing_context_state()->GetRenderFrameProxyHost(
          tab3_rfh->GetSiteInstance()->group());
  EXPECT_FALSE(webui_to_non_webui_proxy);
}

// Similar to OpenerChangeDisallowedAcrossBrowsingInstances_Bfcache, but
// exercises the case where the supplied opener token resolves to a speculative
// RenderFrameHost (which happens to be in the popup tab itself). This should
// fail even within the same BCG because the new opener is still in the
// speculative state and not the current frame of its FrameTreeNode.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OpenerChangeDisallowedViaSpeculativeFrame) {
  // Explicitly isolate a.test to ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("a.test");

  // Navigate to A1.
  EXPECT_TRUE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("a.test", "/title1.html")));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHostImpl* tab1_rfh = web_contents->GetPrimaryMainFrame();
  FrameTreeNode* tab1_root = tab1_rfh->frame_tree_node();

  // Open A2 in the same BCG as A1.
  GURL popup_url(embedded_test_server()->GetURL("a.test", "/title2.html"));
  Shell* tab2 = OpenPopup(shell(), popup_url, "foo");
  RenderFrameHostImpl* tab2_rfh = static_cast<RenderFrameHostImpl*>(
      tab2->web_contents()->GetPrimaryMainFrame());
  FrameTreeNode* tab2_root = tab2_rfh->frame_tree_node();
  EXPECT_EQ(tab2_root->opener(), tab1_root);
  EXPECT_TRUE(tab1_rfh->GetSiteInstance()->IsRelatedSiteInstance(
      tab2_rfh->GetSiteInstance()));

  // Navigate tab 2 to B in the same BCG.
  GURL b_url(embedded_test_server()->GetURL("b.test", "/title1.html"));
  {
    TestNavigationObserver nav_observer(tab2->web_contents(), 1);
    EXPECT_TRUE(ExecJs(tab2, JsReplace("location.href = $1;", b_url)));
    nav_observer.Wait();
  }
  tab2_rfh = static_cast<RenderFrameHostImpl*>(
      tab2->web_contents()->GetPrimaryMainFrame());
  EXPECT_EQ(tab2_root->opener(), tab1_root);
  EXPECT_TRUE(tab1_rfh->GetSiteInstance()->IsRelatedSiteInstance(
      tab2_rfh->GetSiteInstance()));

  // Start a cross-process navigation in tab 2 back to A, and pause it once the
  // speculative RenderFrameHost has been created.
  GURL dest_url(embedded_test_server()->GetURL("a.test", "/title3.html"));
  TestNavigationManager nav_manager(tab2->web_contents(), dest_url);
  EXPECT_TRUE(ExecJs(tab2, JsReplace("location.href = $1;", dest_url)));
  nav_manager.WaitForSpeculativeRenderFrameHostCreation();
  RenderFrameHostImpl* speculative_rfh =
      tab2_root->render_manager()->speculative_frame_host();
  ASSERT_TRUE(speculative_rfh);
  ASSERT_EQ(speculative_rfh->GetProcess(), tab1_rfh->GetProcess());
  ASSERT_EQ(tab2_root->current_frame_host(), tab2_rfh);

  // Find SiteInstance A's proxy for tab 2, as the target for the IPC.
  RenderFrameProxyHost* proxy =
      tab2_rfh->browsing_context_state()->GetRenderFrameProxyHost(
          tab1_rfh->GetSiteInstance()->group());
  ASSERT_TRUE(proxy);

  // Grab the speculative RenderFrameHost's frame token and simulate a rogue
  // DidChangeOpener IPC that supplies it as the new opener for tab 2, which
  // would (legally) create a cycle if it succeeds, but should fail because the
  // token is for a speculative RFH.
  blink::LocalFrameToken opener_token = speculative_rfh->GetFrameToken();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(tab1_rfh->GetProcess());
  proxy->DidChangeOpener(opener_token);

  // The opener should *not* be updated to tab2_root, since the frame token is
  // for a speculative RFH.
  EXPECT_EQ(tab2_root->opener(), tab1_root);

  // The renderer that supplied a speculative frame's token as opener should
  // be terminated.
  EXPECT_EQ(bad_message::RFH_INACTIVE_CHECK_FROM_SPECULATIVE_RFH,
            kill_waiter.Wait());
}

namespace {

// An interceptor class that allows replacing the URL of the commit IPC from
// the renderer process to the browser process.
class DidCommitUrlReplacer : public DidCommitNavigationInterceptor {
 public:
  DidCommitUrlReplacer(WebContents* web_contents, const GURL& replacement_url)
      : DidCommitNavigationInterceptor(web_contents),
        replacement_url_(replacement_url) {}

  DidCommitUrlReplacer(const DidCommitUrlReplacer&) = delete;
  DidCommitUrlReplacer& operator=(const DidCommitUrlReplacer&) = delete;

  ~DidCommitUrlReplacer() override = default;

 protected:
  bool WillProcessDidCommitNavigation(
      RenderFrameHost* render_frame_host,
      NavigationRequest* navigation_request,
      mojom::DidCommitProvisionalLoadParamsPtr* params,
      mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
      override {
    (**params).url = replacement_url_;
    return true;
  }

 private:
  GURL replacement_url_;
};

}  // namespace

// Test which verifies that when an exploited renderer process sends a commit
// message with URL that the process is not allowed to commit.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DidCommitInvalidURL) {
  // Explicitly isolating foo.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("foo.com");

  RenderFrameDeletedObserver initial_frame_deleted_observer(
      shell()->web_contents()->GetPrimaryMainFrame());

  // Test assumes the initial RenderFrameHost to be deleted. Disable
  // back-forward cache to ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // Navigate to foo.com initially.
  GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), foo_url));

  // Wait for the RenderFrameHost which was current before the navigation to
  // foo.com to be deleted. This is necessary, since on a slow system the
  // UnloadACK event can arrive after the DidCommitUrlReplacer instance below
  // is created. The replacer code has checks to ensure that all frames being
  // deleted it has seen being created, which with delayed UnloadACK is
  // violated.
  initial_frame_deleted_observer.WaitUntilDeleted();

  // Create the interceptor object which will replace the URL of the subsequent
  // navigation with bar.com based URL.
  GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html"));
  DidCommitUrlReplacer url_replacer(shell()->web_contents(), bar_url);

  // Navigate to another URL within foo.com, which would usually be committed
  // successfully, but when the URL is modified it should result in the
  // termination of the renderer process.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess());
  EXPECT_FALSE(NavigateToURL(
      shell(), embedded_test_server()->GetURL("foo.com", "/title2.html")));
  EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
}

// Test which verifies that when an exploited renderer process sends a commit
// message with URL that the process is not allowed to commit.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       DISABLED_DidCommitInvalidURLWithOpaqueOrigin) {
  // Explicitly isolating foo.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("foo.com");

  RenderFrameDeletedObserver initial_frame_deleted_observer(
      shell()->web_contents()->GetPrimaryMainFrame());

  // Test assumes the initial RenderFrameHost to be deleted. Disable
  // back-forward cache to ensure that it doesn't get preserved in the cache.
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);

  // Navigate to foo.com initially.
  GURL foo_url(embedded_test_server()->GetURL("foo.com",
                                              "/page_with_blank_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), foo_url));

  // Wait for the RenderFrameHost which was current before the navigation to
  // foo.com to be deleted. This is necessary, since on a slow system the
  // UnloadACK event can arrive after the DidCommitUrlReplacer instance below
  // is created. The replacer code has checks to ensure that all frames being
  // deleted it has seen being created, which with delayed UnloadACK is
  // violated.
  initial_frame_deleted_observer.WaitUntilDeleted();

  // Create the interceptor object which will replace the URL of the subsequent
  // navigation with bar.com based URL.
  GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html"));
  DidCommitUrlReplacer url_replacer(shell()->web_contents(), bar_url);

  // Navigate the subframe to a data URL, which would usually be committed
  // successfully in the same process as foo.com, but when the URL is modified
  // it should result in the termination of the renderer process.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess());

  // Using BeginNavigateIframeToURL is necessary here, since the process
  // termination will result in DidFinishNavigation notification with the
  // navigation not in "committed" state. NavigateIframeToURL waits for the
  // navigation to complete and ignores non-committed navigations, therefore
  // it will wait indefinitely.
  GURL data_url(R"(data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E)");
  EXPECT_TRUE(BeginNavigateIframeToURL(shell()->web_contents(), "test_iframe",
                                       data_url));
  EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
}

// Test which verifies that a WebUI process cannot send a commit message with
// URL for a web document.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       WebUIProcessDidCommitWebURL) {
  // Navigate to a WebUI document.
  GURL webui_url(GetWebUIURL(kChromeUIGpuHost));
  EXPECT_TRUE(NavigateToURL(shell(), webui_url));

  // Create the interceptor object which will replace the URL of the subsequent
  // navigation with |web_url|.
  GURL web_url(embedded_test_server()->GetURL("foo.com", "/title3.html"));
  DidCommitUrlReplacer url_replacer(shell()->web_contents(), web_url);

  // Navigate to another URL within the WebUI, which would usually be committed
  // successfully, but when the URL is modified it should result in the
  // termination of the renderer process.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess());
  GURL second_webui_url(webui_url.Resolve("/foo"));
  EXPECT_FALSE(NavigateToURL(shell(), second_webui_url));
  EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
}

// Test that verifies that if a RenderFrameHost is incorrectly given WebUI
// bindings the browser process crashes due to CHECK enforcements.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       AllowBindingsForNonWebUIProcess) {
  // Navigate to a web URL.
  GURL initial_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), initial_url));

  // Grant WebUI bindings to the frame to simulate a bug in the code that
  // incorrectly does it and verify the browser process crashes.
  EXPECT_NOTREACHED_DEATH(
      shell()->web_contents()->GetPrimaryMainFrame()->AllowBindings(
          BindingsPolicySet({BindingsPolicyValue::kWebUi})));
}

// Tests that a web page cannot bind to a WebUI interface if a WebUI page is the
// currently committed RenderFrameHost in the tab (https://crbug.com/1225929).
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, BindToWebUIFromWebViaMojo) {
  // Navigate to a non-privileged web page, and simulate a renderer compromise
  // by granting MojoJS.
  GURL web_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  TestNavigationManager navigation(shell()->web_contents(), web_url);
  shell()->LoadURL(web_url);
  EXPECT_TRUE(navigation.WaitForResponse());
  RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  main_frame->GetFrameBindingsControl()->EnableMojoJsBindings(nullptr);
  ASSERT_TRUE(navigation.WaitForNavigationFinished());

  // Open a popup so that the process won't exit on its own when leaving.
  OpenBlankWindow(static_cast<WebContentsImpl*>(shell()->web_contents()));

  // When the page unloads (after the cross-process navigation to an actual
  // WebUI page below), try to bind to a WebUI interface from the web
  // RenderFrameHost. Ensure the unload timer and bfcache are disabled so that
  // the handler has a chance to run.
  // This test uses `pagehide` rather than `unload` since they occur at the
  // same timing but `unload` is being deprecated.
  main_frame->DisableUnloadTimerForTesting();
  DisableBackForwardCacheForTesting(shell()->web_contents(),
                                    BackForwardCache::TEST_REQUIRES_NO_CACHING);
  ASSERT_TRUE(ExecJs(main_frame, R"(
        // Intentionally leak pipe as a global so it doesn't get GCed.
        newMessagePipe = Mojo.createMessagePipe();
        onpagehide = function () {
          Mojo.bindInterface('mojom.ProcessInternalsHandler',
                             newMessagePipe.handle0);
        };
      )"));

  // Now navigate to a WebUI page and expect the previous renderer process to be
  // killed when asking to bind to the WebUI interface.
  GURL webui_url(
      GetWebUIURL(kChromeUIProcessInternalsHost).Resolve("#general"));
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
  EXPECT_TRUE(NavigateToURL(shell(), webui_url));

  // Verify that the previous renderer was terminated.
  EXPECT_EQ(bad_message::RFH_INVALID_WEB_UI_CONTROLLER, kill_waiter.Wait());
}

namespace {

// An interceptor class that allows replacing the PageState of the DidCommit IPC
// from the renderer process to the browser process.
class DidCommitPageStateReplacer : public DidCommitNavigationInterceptor {
 public:
  DidCommitPageStateReplacer(WebContents* web_contents,
                             const blink::PageState& page_state)
      : DidCommitNavigationInterceptor(web_contents),
        replacement_page_state_(page_state) {}

  DidCommitPageStateReplacer(const DidCommitPageStateReplacer&) = delete;
  DidCommitPageStateReplacer& operator=(const DidCommitPageStateReplacer&) =
      delete;

  ~DidCommitPageStateReplacer() override = default;

 protected:
  bool WillProcessDidCommitNavigation(
      RenderFrameHost* render_frame_host,
      NavigationRequest* navigation_request,
      mojom::DidCommitProvisionalLoadParamsPtr* params,
      mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
      override {
    (**params).page_state = replacement_page_state_;
    return true;
  }

 private:
  blink::PageState replacement_page_state_;
};

}  // namespace

// Test that committing a navigation with a PageState that does not list all of
// its file paths in GetReferencedFiles will cause a renderer kill.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PageStateWithUnlistedFile) {
  // Navigate to foo.com initially.
  GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), foo_url));

  // Create a PageState that contains a file path which isn't in the list of
  // referenced files which are validated.
  GURL foo_url2(embedded_test_server()->GetURL("foo.com", "/title2.html"));
  blink::ExplodedPageState exploded_page_state;
  ASSERT_TRUE(blink::DecodePageState(
      blink::PageState::CreateFromURL(foo_url2).ToEncodedData(),
      &exploded_page_state));
  scoped_refptr<network::ResourceRequestBody> request_body =
      new network::ResourceRequestBody();
  base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits");
  request_body->AppendFileRange(
      bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time());
  exploded_page_state.top.http_body.request_body = request_body;
  exploded_page_state.top.http_body.http_content_type = u"text/plain";
  std::string encoded_page_state;
  blink::EncodePageState(exploded_page_state, &encoded_page_state);
  blink::PageState page_state =
      blink::PageState::CreateFromEncodedData(encoded_page_state);

  // Create an interceptor which will put the modified PageState into the next
  // navigation's DidCommit message.
  DidCommitPageStateReplacer page_state_replacer(shell()->web_contents(),
                                                 page_state);

  // Navigate in the same renderer process to send the bad PageState.
  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
  EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), foo_url2));

  // Verify that the malicious renderer was killed, for the right reason.
  EXPECT_EQ(bad_message::RFH_CAN_ACCESS_FILES_OF_PAGE_STATE_AT_COMMIT,
            kill_waiter.Wait());
}

// Similar to the test above, but also uses a malformed DocumentState within the
// corrupted PageState, to make it harder to find file paths that are present.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       PageStateWithUnlistedFileAndBadDocumentState) {
  // Navigate to foo.com initially.
  GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), foo_url));

  // Create a PageState that contains a file path which isn't in the list of
  // referenced files which are validated.
  GURL foo_url2(embedded_test_server()->GetURL("foo.com", "/title2.html"));
  blink::ExplodedPageState exploded_page_state;
  ASSERT_TRUE(blink::DecodePageState(
      blink::PageState::CreateFromURL(foo_url2).ToEncodedData(),
      &exploded_page_state));
  scoped_refptr<network::ResourceRequestBody> request_body =
      new network::ResourceRequestBody();
  base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits");
  request_body->AppendFileRange(
      bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time());
  exploded_page_state.top.http_body.request_body = request_body;
  exploded_page_state.top.http_body.http_content_type = u"text/plain";

  // Also modify the DocumentState to force RecursivelyAppendReferencedFiles to
  // return false.
  exploded_page_state.top.document_state = {u"one", u"two"};

  std::string encoded_page_state;
  blink::EncodePageState(exploded_page_state, &encoded_page_state);
  blink::PageState page_state =
      blink::PageState::CreateFromEncodedData(encoded_page_state);

  // Create an interceptor which will put the modified PageState into the next
  // navigation's DidCommit message.
  DidCommitPageStateReplacer page_state_replacer(shell()->web_contents(),
                                                 page_state);

  // Navigate in the same renderer process to send the bad PageState.
  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
  EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), foo_url2));

  // Verify that the malicious renderer was killed, for the right reason.
  EXPECT_EQ(bad_message::RFH_CAN_ACCESS_FILES_OF_PAGE_STATE_AT_COMMIT,
            kill_waiter.Wait());
}

// Inner delegate attachment for MimeHandlerView has an extra phase in the
// beginning, invoked via RenderFrameHost::PrepareForInnerContentsAttach(),
// which currently invokes beforeunload handlers prior to proceeding with
// normal attachment in AttachInnerWebContents()/AttachGuestPage() in a later
// task. Ensure that a misbehaving renderer doesn't trigger RenderFrameDeleted()
// via an unload ACK in that first PrepareForInnerContentsAttach() phase, where
// we aren't actually expecting that unload ACK.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       SpoofUnloadACKDuringInnerDelegateAttach) {
  // Start on a page with a blank iframe, simulating the normal starting point
  // of attaching an inner delegate (e.g., for MimeHandlerView) to a placeholder
  // subframe.
  GURL main_url(
      embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHostImpl* main_frame = web_contents->GetPrimaryMainFrame();
  RenderFrameHostImpl* subframe = main_frame->child_at(0)->current_frame_host();

  // Add a beforeunload handler to the blank subframe. This shouldn't ever
  // happen during the normal inner delegate attachment flow, but a compromised
  // renderer can still do it. This is not technically necessary for the
  // renderer to send a spoofed unlock ACK while we're in this phase, but it
  // makes the time window where this ACK can lead to problems larger and more
  // practical.
  // TODO(crbug.com/40249634): Make it impossible for placeholder
  // frames to trigger beforeunload during inner delegate attachment.
  subframe->DisableBeforeUnloadHangMonitorForTesting();
  EXPECT_TRUE(ExecJs(subframe, "window.onbeforeunload = function() {};"));

  // Prepare for inner web contents attach (this simulates MimeHandlerView
  // creating the PDF inner delegate). This currently triggers the beforeunload
  // phase.
  base::test::TestFuture<RenderFrameHost*> future;
  subframe->PrepareForInnerWebContentsAttach(future.GetCallback());

  // Verify that the subframe is waiting for a beforeunload ACK and also
  // attaching an inner delegate.
  EXPECT_TRUE(subframe->is_waiting_for_beforeunload_completion());
  EXPECT_TRUE(subframe->frame_tree_node()
                  ->render_manager()
                  ->is_attaching_inner_delegate());

  RenderFrameHostWrapper observer(subframe);

  // Spoof DidUnloadRenderFrame IPC while waiting for beforeunload ACK.
  subframe->OnUnloadACK();

  // Ensure the subframe is still live, and its RenderFrame is not deleted.
  ASSERT_FALSE(observer.IsRenderFrameDeleted());

  // Check that we're still in the inner delegate attachment phase.
  EXPECT_TRUE(subframe->frame_tree_node()
                  ->render_manager()
                  ->is_attaching_inner_delegate());

  // The RFH shouldn't change when inner delegate attachment is ready to
  // proceed.
  EXPECT_EQ(subframe, future.Get());

  // Make sure proceeding with the inner WebContents attachment succeeds.
  EXPECT_TRUE(CreateAndAttachInnerContents(subframe));
  EXPECT_TRUE(subframe->frame_tree_node()
                  ->render_manager()
                  ->is_inner_delegate_attached());
}

// Test that when a compromised renderer sends an unload ACK for a placeholder
// frame after its inner delegate is attached, we ensure that any children
// created by the compromised renderer are properly deleted, preventing
// ghost frames. See https://crbug.com/517241992.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       NoChildrenAfterUnloadACKWithInnerDelegateAttached) {
  // Start on a page with a blank iframe, simulating the normal starting point
  // of attaching an inner delegate (e.g., for MimeHandlerView) to a placeholder
  // subframe.
  GURL main_url(
      embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHostImpl* main_frame = web_contents->GetPrimaryMainFrame();
  RenderFrameHostImpl* subframe = main_frame->child_at(0)->current_frame_host();

  // Add a child frame to the subframe so that we can test if it gets properly
  // detached. A compromised renderer could create this child.
  EXPECT_TRUE(ExecJs(subframe,
                     "let f = document.createElement('iframe'); "
                     "document.body.appendChild(f);"));
  ASSERT_EQ(1U, subframe->child_count());
  RenderFrameHostImpl* grandchild = subframe->child_at(0)->current_frame_host();
  RenderFrameHostWrapper grandchild_observer(grandchild);

  // Attach an inner delegate.
  // This synchronously calls SwapOuterDelegateFrame() which sends the Unload
  // IPC, and it sets is_inner_delegate_attached().
  EXPECT_TRUE(CreateAndAttachInnerContents(subframe));

  EXPECT_TRUE(subframe->frame_tree_node()
                  ->render_manager()
                  ->is_inner_delegate_attached());

  // Pretend that the compromised renderer sent the unload ACK without first
  // detaching the grandchild frame.
  subframe->OnUnloadACK();

  // Ensure the subframe is no longer live and that its child frame was deleted.
  EXPECT_TRUE(grandchild_observer.IsRenderFrameDeleted());
  EXPECT_EQ(0U, subframe->child_count());
}

// Test that the placeholder frame for an inner delegate has its children
// cleared synchronously at attach time, without depending on the renderer's
// DidUnloadRenderFrame ACK (which a misbehaving renderer may withhold), and
// that the placeholder cannot acquire new children after the inner delegate is
// attached. See https://crbug.com/518078552.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       NoChildrenOnInnerDelegatePlaceholderAfterAttach) {
  // Start on a page with a blank iframe, simulating the normal starting point
  // of attaching an inner delegate (e.g., for MimeHandlerView) to a placeholder
  // subframe.
  GURL main_url(
      embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHostImpl* main_frame = web_contents->GetPrimaryMainFrame();
  RenderFrameHostImpl* subframe = main_frame->child_at(0)->current_frame_host();

  // Add a child frame to the subframe so that we can test if it gets properly
  // detached. A compromised renderer could create this child.
  EXPECT_TRUE(ExecJs(subframe,
                     "let f = document.createElement('iframe'); "
                     "document.body.appendChild(f);"));
  ASSERT_EQ(1U, subframe->child_count());
  RenderFrameHostImpl* grandchild = subframe->child_at(0)->current_frame_host();
  RenderFrameHostWrapper grandchild_observer(grandchild);

  // Swallow the unload ACK so that any cleanup observed below is solely due to
  // browser-side attach logic and not the renderer's DidUnloadRenderFrame ACK.
  subframe->SetUnloadACKCallbackForTesting(
      base::BindRepeating([]() { return true; }));

  // Attach an inner delegate. This synchronously calls SwapOuterDelegateFrame()
  // which sends the Unload IPC, and it sets is_inner_delegate_attached().
  EXPECT_TRUE(CreateAndAttachInnerContents(subframe));
  EXPECT_TRUE(subframe->frame_tree_node()
                  ->render_manager()
                  ->is_inner_delegate_attached());
  EXPECT_TRUE(subframe->inner_tree_main_frame_tree_node_id());

  // The placeholder frame must have no children immediately after attach,
  // independently of whether the renderer ACKs the unload.
  EXPECT_TRUE(grandchild_observer.IsRenderFrameDeleted());
  EXPECT_EQ(0U, subframe->child_count());

  // Simulate the renderer attempting to create a new child on the placeholder
  // frame after the inner delegate has been attached. The placeholder's render
  // frame is still considered created in the browser (since the unload ACK was
  // swallowed above), so OnCreateChildFrame()'s lifecycle checks alone would
  // not reject this; the dedicated inner-delegate check should drop it.
  ASSERT_TRUE(subframe->IsRenderFrameLive());
  mojo::AssociatedRemote<mojom::Frame> frame_remote;
  std::ignore = frame_remote.BindNewEndpointAndPassDedicatedReceiver();
  mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker> bib_remote;
  mojo::AssociatedReceiver<blink::mojom::AssociatedInterfaceProvider>
      associated_interface_provider_receiver(nullptr);
  std::ignore = associated_interface_provider_receiver
                    .BindNewEndpointAndPassDedicatedRemote();
  subframe->OnCreateChildFrame(
      subframe->GetProcess()->GetNextRoutingID(), frame_remote.Unbind(),
      bib_remote.InitWithNewPipeAndPassReceiver(),
      blink::mojom::PolicyContainerBindParams::New(
          mojo::PendingAssociatedRemote<blink::mojom::PolicyContainerHost>()
              .InitWithNewEndpointAndPassReceiver()),
      associated_interface_provider_receiver.Unbind(),
      blink::mojom::TreeScopeType::kDocument, "", "uniqueName1",
      /*is_created_by_script=*/false, blink::LocalFrameToken(),
      base::UnguessableToken::Create(), blink::DocumentToken(),
      blink::FramePolicy(), blink::mojom::FrameOwnerProperties(),
      blink::FrameOwnerElementType::kIframe, ukm::kInvalidSourceId);

  // The placeholder frame must still have no children.
  EXPECT_EQ(0U, subframe->child_count());
}

class BeginNavigationTransitionReplacer : public FrameHostInterceptor {
 public:
  BeginNavigationTransitionReplacer(WebContents* web_contents,
                                    ui::PageTransition transition_to_inject)
      : FrameHostInterceptor(web_contents),
        transition_to_inject_(transition_to_inject) {}

  BeginNavigationTransitionReplacer(const BeginNavigationTransitionReplacer&) =
      delete;
  BeginNavigationTransitionReplacer& operator=(
      const BeginNavigationTransitionReplacer&) = delete;

  bool WillDispatchBeginNavigation(
      RenderFrameHost* render_frame_host,
      blink::mojom::CommonNavigationParamsPtr* common_params,
      blink::mojom::BeginNavigationParamsPtr* begin_params,
      mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
      mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client)
      override {
    if (is_activated_) {
      (*common_params)->transition = transition_to_inject_;
      is_activated_ = false;
    }

    return true;
  }

  void Activate() { is_activated_ = true; }

 private:
  ui::PageTransition transition_to_inject_;
  bool is_activated_ = false;
};

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, NonWebbyTransition) {
  const ui::PageTransition test_cases[] = {
      ui::PAGE_TRANSITION_TYPED,
      ui::PAGE_TRANSITION_AUTO_BOOKMARK,
      ui::PAGE_TRANSITION_GENERATED,
      ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
      ui::PAGE_TRANSITION_RELOAD,
      ui::PAGE_TRANSITION_KEYWORD,
      ui::PAGE_TRANSITION_KEYWORD_GENERATED};

  for (ui::PageTransition transition : test_cases) {
    // Prepare to intercept BeginNavigation mojo IPC.  This has to be done
    // before the test creates the RenderFrameHostImpl that is the target of the
    // IPC.
    WebContents* web_contents = shell()->web_contents();
    BeginNavigationTransitionReplacer injector(web_contents, transition);

    // Navigate to a test page.
    GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
    EXPECT_TRUE(NavigateToURL(web_contents, main_url));

    // Start monitoring for renderer kills.
    RenderProcessHost* main_process =
        web_contents->GetPrimaryMainFrame()->GetProcess();
    RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);

    // Have the main frame submit a BeginNavigation IPC with a missing
    // initiator.
    injector.Activate();
    // Don't expect a response for the script, as the process may be killed
    // before the script sends its completion message.
    ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");

    // Verify that the renderer was terminated.
    EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_NON_WEBBY_TRANSITION,
              kill_waiter.Wait());
  }
}

class SecurityExploitViaDisabledWebSecurityTest
    : public SecurityExploitBrowserTest {
 public:
  SecurityExploitViaDisabledWebSecurityTest() {
    // To get around BlockedSchemeNavigationThrottle. Other attempts at getting
    // around it don't work, i.e.:
    // -if the request is made in a child frame then the frame is torn down
    // immediately on process killing so the navigation doesn't complete
    // -if it's classified as same document, then a DCHECK in
    // NavigationRequest::CreateRendererInitiated fires
    feature_list_.InitAndEnableFeature(
        features::kAllowContentInitiatedDataUrlNavigations);
  }

 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Simulate a compromised renderer, otherwise the cross-origin request to
    // file: is blocked.
    command_line->AppendSwitch(switches::kDisableWebSecurity);
    SecurityExploitBrowserTest::SetUpCommandLine(command_line);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Test to verify that an exploited renderer process trying to specify a
// non-empty URL for base_url_for_data_url on navigation is correctly
// terminated.
IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,
                       ValidateBaseUrlForDataUrl) {
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  GURL data_url("data:text/html,foo");
  base::FilePath file_path = GetTestFilePath("", "simple_page.html");
  GURL file_url = net::FilePathToFileURL(file_path);

  // Setup a BeginNavigate IPC with non-empty base_url_for_data_url.
  blink::mojom::CommonNavigationParamsPtr common_params =
      blink::mojom::CommonNavigationParams::New(
          data_url, url::Origin::Create(data_url),
          /* initiator_base_url= */ std::nullopt, blink::mojom::Referrer::New(),
          ui::PAGE_TRANSITION_LINK,
          blink::mojom::NavigationType::DIFFERENT_DOCUMENT,
          blink::NavigationDownloadPolicy(),
          false /* should_replace_current_entry */,
          file_url /* base_url_for_data_url */,
          base::TimeTicks::Now() /* actual_navigation_start */,
          base::TimeTicks::Now() /* navigation_start */, "GET",
          nullptr /* post_data */, network::mojom::SourceLocation::New(),
          false /* started_from_context_menu */, false /* has_user_gesture */,
          false /* text_fragment_token */,
          network::mojom::CSPDisposition::CHECK,
          std::vector<int>() /* initiator_origin_trial_features */,
          std::string() /* href_translate */,
          false /* is_history_navigation_in_new_child_frame */,
          base::TimeTicks() /* input_start */,
          network::mojom::RequestDestination::kDocument);
  blink::mojom::BeginNavigationParamsPtr begin_params =
      blink::mojom::BeginNavigationParams::New(
          std::nullopt /* initiator_frame_token */,
          rfh->current_initiator_state_token(), rfh->GetDocumentToken(),
          std::string() /* headers */, net::LOAD_NORMAL,
          false /* skip_service_worker */,
          blink::mojom::RequestContextType::LOCATION,
          blink::mojom::MixedContentContextType::kBlockable,
          false /* is_form_submission */,
          false /* was_initiated_by_link_click */,
          blink::mojom::ForceHistoryPush::kNo, GURL() /* searchable_form_url */,
          std::string() /* searchable_form_encoding */,
          GURL() /* client_side_redirect_url */,
          std::nullopt /* devtools_initiator_info */,
          nullptr /* trust_token_params */,
          base::TimeTicks() /* renderer_before_unload_start */,
          base::TimeTicks() /* renderer_before_unload_end */,
          base::TimeTicks() /* before_unload_dialog_opened */,
          base::TimeTicks() /* before_unload_dialog_closed */,
          false /* started_with_transient_activation */,
          false /* started_by_ad */, false /* is_container_initiated */,
          false /* has_rel_opener */,
          std::nullopt /* script_tool_invocation_id */);

  // Receiving the invalid IPC message should lead to renderer process
  // termination.
  RenderProcessHostBadIpcMessageWaiter process_kill_waiter(rfh->GetProcess());

  mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client;
  auto navigation_client_receiver =
      navigation_client.InitWithNewEndpointAndPassReceiver();
  rfh->frame_host_receiver_for_testing().impl()->BeginNavigation(
      std::move(common_params), std::move(begin_params), mojo::NullRemote(),
      std::move(navigation_client), mojo::NullRemote(), mojo::NullReceiver(),
      mojo::NullReceiver(), mojo::NullReceiver());
  EXPECT_EQ(bad_message::RFH_BASE_URL_FOR_DATA_URL_SPECIFIED,
            process_kill_waiter.Wait());

  EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile(
      rfh->GetProcess()->GetID(), file_path));

  // Reload the page to create another renderer process.
  TestNavigationObserver tab_observer(shell()->web_contents(), 1);
  shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
  tab_observer.Wait();

  // Make an XHR request to check if the page has access.
  std::string script = base::StringPrintf(
      "var xhr = new XMLHttpRequest()\n"
      "xhr.open('GET', '%s', false);\n"
      "try { xhr.send(); } catch (e) {}\n"
      "xhr.responseText;",
      file_url.spec().c_str());
  std::string result = EvalJs(shell()->web_contents(), script).ExtractString();
  EXPECT_TRUE(result.empty());
}

// Test to verify that an exploited renderer process trying to specify a
// empty URL for initiator_base_url on navigation is correctly terminated.
IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,
                       ValidateInitiatorBaseUrlNotEmpty) {
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  GURL url("about:blank");

  // Setup a BeginNavigate IPC with empty, but not nullopt, initiator_base_url.
  blink::mojom::CommonNavigationParamsPtr common_params =
      blink::mojom::CommonNavigationParams::New(
          url, url::Origin::Create(start_url),
          /* initiator_base_url= */ GURL(), blink::mojom::Referrer::New(),
          ui::PAGE_TRANSITION_LINK,
          blink::mojom::NavigationType::DIFFERENT_DOCUMENT,
          blink::NavigationDownloadPolicy(),
          false /* should_replace_current_entry */,
          GURL() /* base_url_for_data_url */,
          base::TimeTicks::Now() /* actual_navigation_start */,
          base::TimeTicks::Now() /* navigation_start */, "GET",
          nullptr /* post_data */, network::mojom::SourceLocation::New(),
          false /* started_from_context_menu */, false /* has_user_gesture */,
          false /* text_fragment_token */,
          network::mojom::CSPDisposition::CHECK,
          std::vector<int>() /* initiator_origin_trial_features */,
          std::string() /* href_translate */,
          false /* is_history_navigation_in_new_child_frame */,
          base::TimeTicks() /* input_start */,
          network::mojom::RequestDestination::kDocument);
  blink::mojom::BeginNavigationParamsPtr begin_params =
      blink::mojom::BeginNavigationParams::New(
          std::nullopt /* initiator_frame_token */,
          rfh->current_initiator_state_token(), rfh->GetDocumentToken(),
          std::string() /* headers */, net::LOAD_NORMAL,
          false /* skip_service_worker */,
          blink::mojom::RequestContextType::LOCATION,
          blink::mojom::MixedContentContextType::kBlockable,
          false /* is_form_submission */,
          false /* was_initiated_by_link_click */,
          blink::mojom::ForceHistoryPush::kNo, GURL() /* searchable_form_url */,
          std::string() /* searchable_form_encoding */,
          GURL() /* client_side_redirect_url */,
          std::nullopt /* devtools_initiator_info */,
          nullptr /* trust_token_params */,
          base::TimeTicks() /* renderer_before_unload_start */,
          base::TimeTicks() /* renderer_before_unload_end */,
          base::TimeTicks() /* before_unload_dialog_opened */,
          base::TimeTicks() /* before_unload_dialog_closed */,
          false /* started_with_transient_activation */,
          false /* started_by_ad */, false /* is_container_initiated */,
          false /* has_rel_opener */,
          std::nullopt /* script_tool_invocation_id */);

  // Receiving the invalid IPC message should lead to renderer process
  // termination.
  RenderProcessHostBadIpcMessageWaiter process_kill_waiter(rfh->GetProcess());

  mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client;
  auto navigation_client_receiver =
      navigation_client.InitWithNewEndpointAndPassReceiver();
  rfh->frame_host_receiver_for_testing().impl()->BeginNavigation(
      std::move(common_params), std::move(begin_params), mojo::NullRemote(),
      std::move(navigation_client), mojo::NullRemote(), mojo::NullReceiver(),
      mojo::NullReceiver(), mojo::NullReceiver());
  EXPECT_EQ(bad_message::RFH_INITIATOR_BASE_URL_IS_EMPTY,
            process_kill_waiter.Wait());
}

// Test to verify that an exploited renderer process trying to specify a nullopt
// initiator_state_token in BeginNavigation is correctly terminated.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       BeginNavigationNoInitiatorStateToken) {
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  GURL url("about:blank");

  blink::mojom::CommonNavigationParamsPtr common_params =
      blink::mojom::CommonNavigationParams::New(
          url, url::Origin::Create(start_url),
          /*initiator_base_url=*/std::nullopt, blink::mojom::Referrer::New(),
          ui::PAGE_TRANSITION_LINK,
          blink::mojom::NavigationType::DIFFERENT_DOCUMENT,
          blink::NavigationDownloadPolicy(),
          /*should_replace_current_entry=*/false,
          GURL() /* base_url_for_data_url */,
          base::TimeTicks::Now() /* actual_navigation_start */,
          base::TimeTicks::Now() /* navigation_start */, "GET",
          nullptr /* post_data */, network::mojom::SourceLocation::New(),
          /*started_from_context_menu=*/false, /*has_user_gesture=*/false,
          /*text_fragment_token=*/false, network::mojom::CSPDisposition::CHECK,
          std::vector<int>() /* initiator_origin_trial_features */,
          std::string() /* href_translate */,
          /*is_history_navigation_in_new_child_frame=*/false,
          base::TimeTicks() /* input_start */,
          network::mojom::RequestDestination::kDocument);
  blink::mojom::BeginNavigationParamsPtr begin_params =
      blink::mojom::BeginNavigationParams::New(
          std::nullopt /* initiator_frame_token */,
          std::nullopt /* initiator_state_token */, rfh->GetDocumentToken(),
          std::string() /* headers */, net::LOAD_NORMAL,
          /*skip_service_worker=*/false,
          blink::mojom::RequestContextType::LOCATION,
          blink::mojom::MixedContentContextType::kBlockable,
          /*is_form_submission=*/false,
          /*was_initiated_by_link_click=*/false,
          blink::mojom::ForceHistoryPush::kNo, GURL() /* searchable_form_url */,
          std::string() /* searchable_form_encoding */,
          GURL() /* client_side_redirect_url */,
          std::nullopt /* devtools_initiator_info */,
          nullptr /* trust_token_params */,
          base::TimeTicks() /* renderer_before_unload_start */,
          base::TimeTicks() /* renderer_before_unload_end */,
          base::TimeTicks() /* before_unload_dialog_opened */,
          base::TimeTicks() /* before_unload_dialog_closed */,
          /*started_with_transient_activation=*/false,
          /*started_by_ad=*/false, /*is_container_initiated=*/false,
          /*has_rel_opener=*/false,
          std::nullopt /* script_tool_invocation_id */);

  // Receiving the invalid IPC message should lead to renderer process
  // termination.
  RenderProcessHostBadIpcMessageWaiter process_kill_waiter(rfh->GetProcess());

  mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client;
  auto navigation_client_receiver =
      navigation_client.InitWithNewEndpointAndPassReceiver();
  rfh->frame_host_receiver_for_testing().impl()->BeginNavigation(
      std::move(common_params), std::move(begin_params), mojo::NullRemote(),
      std::move(navigation_client), mojo::NullRemote(), mojo::NullReceiver(),
      mojo::NullReceiver(), mojo::NullReceiver());
  EXPECT_EQ(bad_message::RFH_BEGIN_NAVIGATION_NO_INITIATOR_TOKENS,
            process_kill_waiter.Wait());
}

// Test to verify that an exploited renderer process trying to specify a nullopt
// initiator_document_token in BeginNavigation is correctly terminated.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       BeginNavigationNoInitiatorDocumentToken) {
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  GURL url("about:blank");

  blink::mojom::CommonNavigationParamsPtr common_params =
      blink::mojom::CommonNavigationParams::New(
          url, url::Origin::Create(start_url),
          /*initiator_base_url=*/std::nullopt, blink::mojom::Referrer::New(),
          ui::PAGE_TRANSITION_LINK,
          blink::mojom::NavigationType::DIFFERENT_DOCUMENT,
          blink::NavigationDownloadPolicy(),
          /*should_replace_current_entry=*/false,
          GURL() /* base_url_for_data_url */,
          base::TimeTicks::Now() /* actual_navigation_start */,
          base::TimeTicks::Now() /* navigation_start */, "GET",
          nullptr /* post_data */, network::mojom::SourceLocation::New(),
          /*started_from_context_menu=*/false, /*has_user_gesture=*/false,
          /*text_fragment_token=*/false, network::mojom::CSPDisposition::CHECK,
          std::vector<int>() /* initiator_origin_trial_features */,
          std::string() /* href_translate */,
          /*is_history_navigation_in_new_child_frame=*/false,
          base::TimeTicks() /* input_start */,
          network::mojom::RequestDestination::kDocument);
  blink::mojom::BeginNavigationParamsPtr begin_params =
      blink::mojom::BeginNavigationParams::New(
          std::nullopt /* initiator_frame_token */,
          rfh->current_initiator_state_token(), std::nullopt,
          std::string() /* headers */, net::LOAD_NORMAL,
          /*skip_service_worker=*/false,
          blink::mojom::RequestContextType::LOCATION,
          blink::mojom::MixedContentContextType::kBlockable,
          /*is_form_submission=*/false,
          /*was_initiated_by_link_click=*/false,
          blink::mojom::ForceHistoryPush::kNo, GURL() /* searchable_form_url */,
          std::string() /* searchable_form_encoding */,
          GURL() /* client_side_redirect_url */,
          std::nullopt /* devtools_initiator_info */,
          nullptr /* trust_token_params */,
          base::TimeTicks() /* renderer_before_unload_start */,
          base::TimeTicks() /* renderer_before_unload_end */,
          base::TimeTicks() /* before_unload_dialog_opened */,
          base::TimeTicks() /* before_unload_dialog_closed */,
          /*started_with_transient_activation=*/false,
          /*started_by_ad=*/false, /*is_container_initiated=*/false,
          /*has_rel_opener=*/false,
          std::nullopt /* script_tool_invocation_id */);

  // Receiving the invalid IPC message should lead to renderer process
  // termination.
  RenderProcessHostBadIpcMessageWaiter process_kill_waiter(rfh->GetProcess());

  mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client;
  auto navigation_client_receiver =
      navigation_client.InitWithNewEndpointAndPassReceiver();
  rfh->frame_host_receiver_for_testing().impl()->BeginNavigation(
      std::move(common_params), std::move(begin_params), mojo::NullRemote(),
      std::move(navigation_client), mojo::NullRemote(), mojo::NullReceiver(),
      mojo::NullReceiver(), mojo::NullReceiver());
  EXPECT_EQ(bad_message::RFH_BEGIN_NAVIGATION_NO_INITIATOR_TOKENS,
            process_kill_waiter.Wait());
}

// Tests what happens when a web renderer asks to begin navigating to a file
// url.
IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,
                       WebToFileNavigation) {
  // Navigate to a web page.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Have the webpage attempt to open a window with a file URL.
  //
  // Note that such attempt would normally be blocked in the renderer ("Not
  // allowed to load local resource: file:///..."), but the test here simulates
  // a compromised renderer by using --disable-web-security cmdline flag.
  GURL file_url = GetTestUrl("", "simple_page.html");
  WebContentsAddedObserver new_window_observer;
  TestNavigationObserver nav_observer(nullptr);
  nav_observer.StartWatchingNewWebContents();
  ASSERT_TRUE(ExecJs(shell()->web_contents(),
                     JsReplace("window.open($1, '_blank')", file_url)));
  WebContents* new_window = new_window_observer.GetWebContents();
  nav_observer.WaitForNavigationFinished();

  // Verify that the navigation got blocked.
  EXPECT_TRUE(nav_observer.last_navigation_succeeded());
  EXPECT_EQ(GURL(kBlockedURL), nav_observer.last_navigation_url());
  EXPECT_EQ(GURL(kBlockedURL),
            new_window->GetPrimaryMainFrame()->GetLastCommittedURL());
  EXPECT_EQ(
      shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(),
      new_window->GetPrimaryMainFrame()->GetLastCommittedOrigin());
  EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(),
            new_window->GetPrimaryMainFrame()->GetProcess());

  // Even though the navigation is blocked, we expect the opener relationship to
  // be established between the 2 windows.
  EXPECT_EQ(true, ExecJs(new_window, "!!window.opener"));
}

// Tests what happens when a web renderer asks to begin navigating to a
// view-source url.
IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,
                       WebToViewSourceNavigation) {
  // Navigate to a web page.
  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  // Have the webpage attempt to open a window with a view-source URL.
  //
  // Note that such attempt would normally be blocked in the renderer ("Not
  // allowed to load local resource: view-source:///..."), but the test here
  // simulates a compromised renderer by using --disable-web-security flag.
  base::FilePath file_path = GetTestFilePath("", "simple_page.html");
  GURL view_source_url =
      GURL(std::string(kViewSourceScheme) + ":" + start_url.spec());
  WebContentsAddedObserver new_window_observer;
  TestNavigationObserver nav_observer(nullptr);
  nav_observer.StartWatchingNewWebContents();
  ASSERT_TRUE(ExecJs(shell()->web_contents(),
                     JsReplace("window.open($1, '_blank')", view_source_url)));
  WebContents* new_window = new_window_observer.GetWebContents();
  nav_observer.WaitForNavigationFinished();

  // Verify that the navigation got blocked.
  EXPECT_TRUE(nav_observer.last_navigation_succeeded());
  EXPECT_EQ(GURL(kBlockedURL), nav_observer.last_navigation_url());
  EXPECT_EQ(GURL(kBlockedURL),
            new_window->GetPrimaryMainFrame()->GetLastCommittedURL());
  EXPECT_EQ(
      shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(),
      new_window->GetPrimaryMainFrame()->GetLastCommittedOrigin());
  EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(),
            new_window->GetPrimaryMainFrame()->GetProcess());

  // Even though the navigation is blocked, we expect the opener relationship to
  // be established between the 2 windows.
  EXPECT_EQ(true, ExecJs(new_window, "!!window.opener"));
}

class BeginNavigationTrustTokenParamsReplacer : public FrameHostInterceptor {
 public:
  BeginNavigationTrustTokenParamsReplacer(
      WebContents* web_contents,
      network::mojom::TrustTokenParamsPtr params_to_inject)
      : FrameHostInterceptor(web_contents),
        params_to_inject_(std::move(params_to_inject)) {}

  BeginNavigationTrustTokenParamsReplacer(
      const BeginNavigationTrustTokenParamsReplacer&) = delete;
  BeginNavigationTrustTokenParamsReplacer& operator=(
      const BeginNavigationTrustTokenParamsReplacer&) = delete;

  bool WillDispatchBeginNavigation(
      RenderFrameHost* render_frame_host,
      blink::mojom::CommonNavigationParamsPtr* common_params,
      blink::mojom::BeginNavigationParamsPtr* begin_params,
      mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
      mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client)
      override {
    if (is_activated_) {
      (*begin_params)->trust_token_params = params_to_inject_.Clone();
      is_activated_ = false;
    }

    return true;
  }

  void Activate() { is_activated_ = true; }

 private:
  network::mojom::TrustTokenParamsPtr params_to_inject_;
  bool is_activated_ = false;
};

class SecurityExploitBrowserTestWithTrustTokensEnabled
    : public SecurityExploitBrowserTest {
 public:
  SecurityExploitBrowserTestWithTrustTokensEnabled() = default;
};

// Test that the browser correctly reports a bad message when a child frame
// attempts to navigate with a Private State Tokens redemption operation
// associated with the navigation, but its parent lacks the
// private-state-token-redemption Permissions Policy feature.
IN_PROC_BROWSER_TEST_F(
    SecurityExploitBrowserTestWithTrustTokensEnabled,
    BrowserForbidsTrustTokenRedemptionWithoutPermissionsPolicy) {
  WebContents* web_contents = shell()->web_contents();

  // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
  // the test creates the RenderFrameHostImpl that is the target of the IPC.
  auto params = network::mojom::TrustTokenParams::New();
  params->operation = network::mojom::TrustTokenOperationType::kRedemption;
  BeginNavigationTrustTokenParamsReplacer replacer(web_contents,
                                                   std::move(params));

  GURL start_url(embedded_test_server()->GetURL(
      "/page-with-trust-token-permissions-policy-disabled.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHost* parent = web_contents->GetPrimaryMainFrame();
  ASSERT_FALSE(parent->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kTrustTokenRedemption));

  replacer.Activate();

  RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents)
                               ->GetPrimaryFrameTree()
                               .root()
                               ->child_at(0)
                               ->current_frame_host();
  ExecuteScriptAsync(child, JsReplace("location = $1", "/title2.html"));

  RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess());
  EXPECT_THAT(kill_waiter.Wait(),
              Optional(HasSubstr("Permissions Policy feature is absent")));
}

// Test that the browser correctly reports a bad message when a child frame
// attempts to navigate with a Private State Tokens signing operation associated
// with the navigation, but its parent lacks the private-state-token-redemption
// (sic) Permissions Policy feature.
IN_PROC_BROWSER_TEST_F(
    SecurityExploitBrowserTestWithTrustTokensEnabled,
    BrowserForbidsTrustTokenSigningWithoutPermissionsPolicy) {
  WebContents* web_contents = shell()->web_contents();

  // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
  // the test creates the RenderFrameHostImpl that is the target of the IPC.
  auto params = network::mojom::TrustTokenParams::New();
  params->operation = network::mojom::TrustTokenOperationType::kSigning;
  BeginNavigationTrustTokenParamsReplacer replacer(web_contents,
                                                   std::move(params));

  GURL start_url(embedded_test_server()->GetURL(
      "/page-with-trust-token-permissions-policy-disabled.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHost* parent = web_contents->GetPrimaryMainFrame();
  ASSERT_FALSE(parent->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kTrustTokenRedemption));

  replacer.Activate();

  RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents)
                               ->GetPrimaryFrameTree()
                               .root()
                               ->child_at(0)
                               ->current_frame_host();
  ExecuteScriptAsync(child, JsReplace("location = $1", "/title2.html"));

  RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess());
  EXPECT_THAT(kill_waiter.Wait(),
              Optional(HasSubstr("Permissions Policy feature is absent")));
}

// Test that the browser correctly reports a bad message when a child frame
// attempts to navigate with a Private State Tokens issue operation
// associated with the navigation, but its parent lacks the
// private-state-token-issuance Permissions Policy feature.
IN_PROC_BROWSER_TEST_F(
    SecurityExploitBrowserTestWithTrustTokensEnabled,
    BrowserForbidsTrustTokenIssuanceWithoutPermissionsPolicy) {
  WebContents* web_contents = shell()->web_contents();

  // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
  // the test creates the RenderFrameHostImpl that is the target of the IPC.
  auto params = network::mojom::TrustTokenParams::New();
  params->operation = network::mojom::TrustTokenOperationType::kIssuance;
  BeginNavigationTrustTokenParamsReplacer replacer(web_contents,
                                                   std::move(params));

  GURL start_url(embedded_test_server()->GetURL(
      "/page-with-trust-token-permissions-policy-disabled.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHost* parent = web_contents->GetPrimaryMainFrame();
  ASSERT_FALSE(parent->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kPrivateStateTokenIssuance));

  replacer.Activate();

  RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents)
                               ->GetPrimaryFrameTree()
                               .root()
                               ->child_at(0)
                               ->current_frame_host();
  ExecuteScriptAsync(child, JsReplace("location = $1", "/title2.html"));

  RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess());
  EXPECT_THAT(kill_waiter.Wait(),
              Optional(HasSubstr("Permissions Policy feature is absent")));
}

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWithTrustTokensEnabled,
                       BrowserForbidsTrustTokenParamsOnMainFrameNav) {
  WebContents* web_contents = shell()->web_contents();

  // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
  // the test creates the RenderFrameHostImpl that is the target of the IPC.
  BeginNavigationTrustTokenParamsReplacer replacer(
      web_contents, network::mojom::TrustTokenParams::New());

  GURL start_url(embedded_test_server()->GetURL("/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  replacer.Activate();

  RenderFrameHost* compromised_renderer = web_contents->GetPrimaryMainFrame();
  ExecuteScriptAsync(compromised_renderer,
                     JsReplace("location = $1", "/title2.html"));

  RenderProcessHostBadMojoMessageWaiter kill_waiter(
      compromised_renderer->GetProcess());
  EXPECT_THAT(
      kill_waiter.Wait(),
      Optional(HasSubstr("Private State Token params in main frame nav")));
}

class FencedFrameSecurityExploitBrowserTestWithTrustTokensEnabled
    : public SecurityExploitBrowserTestWithTrustTokensEnabled {
 protected:
  FencedFrameSecurityExploitBrowserTestWithTrustTokensEnabled() = default;

  WebContentsImpl* web_contents() {
    return static_cast<WebContentsImpl*>(shell()->web_contents());
  }

  RenderFrameHostImpl* primary_main_frame_host() {
    return web_contents()->GetPrimaryMainFrame();
  }

  test::FencedFrameTestHelper& fenced_frame_test_helper() {
    return fenced_frame_test_helper_;
  }

 private:
  test::FencedFrameTestHelper fenced_frame_test_helper_;
};

class FencedFrameBeginNavigationTrustTokenParamsReplacer
    : public BeginNavigationTrustTokenParamsReplacer {
 public:
  FencedFrameBeginNavigationTrustTokenParamsReplacer(
      WebContents* web_contents,
      network::mojom::TrustTokenParamsPtr params_to_inject)
      : BeginNavigationTrustTokenParamsReplacer(web_contents,
                                                std::move(params_to_inject)) {}

  FencedFrameBeginNavigationTrustTokenParamsReplacer(
      const FencedFrameBeginNavigationTrustTokenParamsReplacer&) = delete;
  FencedFrameBeginNavigationTrustTokenParamsReplacer& operator=(
      const FencedFrameBeginNavigationTrustTokenParamsReplacer&) = delete;

  bool WillDispatchBeginNavigation(
      RenderFrameHost* render_frame_host,
      blink::mojom::CommonNavigationParamsPtr* common_params,
      blink::mojom::BeginNavigationParamsPtr* begin_params,
      mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
      mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client)
      override {
    if (render_frame_host->IsFencedFrameRoot()) {
      BeginNavigationTrustTokenParamsReplacer::WillDispatchBeginNavigation(
          render_frame_host, common_params, begin_params, blob_url_token,
          navigation_client);
    }
    return true;
  }
};

IN_PROC_BROWSER_TEST_F(
    FencedFrameSecurityExploitBrowserTestWithTrustTokensEnabled,
    BrowserForbidsTrustTokenParamsOnFencedFrameNav) {
  WebContents* web_contents = shell()->web_contents();

  // Prepare to intercept BeginNavigation mojo IPC. This has to be done before
  // the test creates the RenderFrameHostImpl that is the target of the IPC.
  FencedFrameBeginNavigationTrustTokenParamsReplacer replacer(
      web_contents, network::mojom::TrustTokenParams::New());
  GURL start_url(embedded_test_server()->GetURL("/empty.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host());
  RenderFrameHostImplWrapper inner_fenced_frame_rfh(
      fenced_frame_test_helper().CreateFencedFrame(
          primary_rfh.get(),
          embedded_test_server()->GetURL("/fenced_frames/empty.html")));

  RenderFrameHost* compromised_renderer = inner_fenced_frame_rfh.get();
  RenderProcessHostBadMojoMessageWaiter kill_waiter(
      compromised_renderer->GetProcess());
  replacer.Activate();

  std::ignore = ExecJs(
      compromised_renderer,
      JsReplace("location.href=$1",
                embedded_test_server()->GetURL("/fenced_frames/title1.html")));

  std::optional<std::string> result = kill_waiter.Wait();
  EXPECT_THAT(result,
              Optional(HasSubstr("Private State Token params in fenced frame "
                                 "nav")));
}

class SecurityExploitTestFencedFramesDisabled
    : public SecurityExploitBrowserTest {
 public:
  SecurityExploitTestFencedFramesDisabled() {
    feature_list_.InitAndDisableFeature(blink::features::kFencedFrames);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Ensure that we kill the renderer process if we try to create a
// fenced-frame when the blink::features::kFencedFrames feature is not enabled.
IN_PROC_BROWSER_TEST_F(SecurityExploitTestFencedFramesDisabled,
                       CreateFencedFrameWhenFeatureDisabled) {
  GURL foo("http://foo.com/simple_page.html");
  EXPECT_TRUE(NavigateToURL(shell(), foo));
  EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle());
  EXPECT_FALSE(blink::features::IsFencedFramesEnabled());

  RenderFrameHostImpl* compromised_rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  mojo::PendingAssociatedRemote<blink::mojom::FencedFrameOwnerHost> remote;
  mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost> receiver;
  receiver = remote.InitWithNewEndpointAndPassReceiver();

  auto remote_frame_interfaces =
      blink::mojom::RemoteFrameInterfacesFromRenderer::New();
  remote_frame_interfaces->frame_host_receiver =
      mojo::AssociatedRemote<blink::mojom::RemoteFrameHost>()
          .BindNewEndpointAndPassDedicatedReceiver();
  mojo::AssociatedRemote<blink::mojom::RemoteFrame> frame;
  std::ignore = frame.BindNewEndpointAndPassDedicatedReceiver();
  remote_frame_interfaces->frame = frame.Unbind();

  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      compromised_rfh->GetProcess());
  static_cast<blink::mojom::LocalFrameHost*>(compromised_rfh)
      ->CreateFencedFrame(
          std::move(receiver), std::move(remote_frame_interfaces),
          blink::RemoteFrameToken(), base::UnguessableToken::Create());
  EXPECT_EQ(bad_message::RFH_FENCED_FRAME_MOJO_WHEN_DISABLED,
            kill_waiter.Wait());
}

// Ensure that we kill the renderer process if we try to do a top-level
// navigation using the special _unfencedTop IPC path when we are not inside
// a fenced frame. (Test from an iframe instead.)
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       UnfencedTopFromOutsideFencedFrame) {
  GURL main_url(embedded_test_server()->GetURL(
      "a.com", "/cross_site_iframe_factory.html?a(b)"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
                            ->GetPrimaryFrameTree()
                            .root();
  RenderFrameHostImpl* compromised_rfh =
      root->child_at(0)->current_frame_host();

  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      compromised_rfh->GetProcess());

  GURL url("http://foo.com/simple_page.html");
  auto params = CreateOpenURLParams(url);
  params->is_unfenced_top_navigation = true;
  static_cast<mojom::FrameHost*>(compromised_rfh)->OpenURL(std::move(params));

  EXPECT_EQ(bad_message::RFHI_UNFENCED_TOP_IPC_OUTSIDE_FENCED_FRAME,
            kill_waiter.Wait());
}

class SecurityExploitBrowserTestFencedFrames
    : public SecurityExploitBrowserTest {
 public:
  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    https_server()->StartAcceptingConnections();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    https_server()->AddDefaultHandlers(GetTestDataFilePath());
    https_server()->ServeFilesFromSourceDirectory(GetTestDataFilePath());
    https_server()->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
    SetupCrossSiteRedirector(https_server());

    // EmbeddedTestServer::InitializeAndListen() initializes its |base_url_|
    // which is required below. This cannot invoke Start() however as that kicks
    // off the "EmbeddedTestServer IO Thread" which then races with
    // initialization in ContentBrowserTest::SetUp(), http://crbug.com/674545.
    ASSERT_TRUE(https_server()->InitializeAndListen());
  }

  test::FencedFrameTestHelper& fenced_frame_test_helper() {
    return fenced_frame_test_helper_;
  }

  net::EmbeddedTestServer* https_server() { return &https_server_; }

 private:
  test::FencedFrameTestHelper fenced_frame_test_helper_{};
  net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
};

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
                       NavigateFencedFrameToInvalidURL) {
  GURL main_frame_url(https_server()->GetURL("a.test", "/simple_page.html"));
  std::vector<GURL> invalid_urls = {
      GURL("http://example.com"),
      GURL("http://example.com?<\n=block"),
      GURL("about:srcdoc"),
      GURL("data:text/html,<p>foo"),
      GURL("blob:https://example.com/a9400bf5-aaa8-4166-86e4-492c50f4ca2b"),
      GURL("file://folder"),
      GURL("javascript:console.log('foo');"),
      GetWebUIURL(kChromeUIHistogramHost),
      GURL(blink::kChromeUIHangURL)};

  for (const GURL& invalid_url : invalid_urls) {
    EXPECT_FALSE(blink::IsValidFencedFrameURL(invalid_url));
    EXPECT_TRUE(blink::features::IsFencedFramesEnabled());
    EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
    EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle());

    RenderFrameHostImpl* compromised_rfh = static_cast<RenderFrameHostImpl*>(
        shell()->web_contents()->GetPrimaryMainFrame());

    mojo::AssociatedRemote<blink::mojom::FencedFrameOwnerHost> remote;
    mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost>
        pending_receiver = remote.BindNewEndpointAndPassReceiver();

    auto remote_frame_interfaces =
        blink::mojom::RemoteFrameInterfacesFromRenderer::New();
    remote_frame_interfaces->frame_host_receiver =
        mojo::AssociatedRemote<blink::mojom::RemoteFrameHost>()
            .BindNewEndpointAndPassDedicatedReceiver();
    mojo::AssociatedRemote<blink::mojom::RemoteFrame> frame;
    std::ignore = frame.BindNewEndpointAndPassDedicatedReceiver();
    remote_frame_interfaces->frame = frame.Unbind();

    RenderProcessHostBadIpcMessageWaiter kill_waiter(
        compromised_rfh->GetProcess());
    static_cast<blink::mojom::LocalFrameHost*>(compromised_rfh)
        ->CreateFencedFrame(
            std::move(pending_receiver), std::move(remote_frame_interfaces),
            blink::RemoteFrameToken(), base::UnguessableToken::Create());
    EXPECT_EQ(compromised_rfh->GetFencedFrames().size(), 1u);

    FencedFrame* fenced_frame = compromised_rfh->GetFencedFrames()[0];
    static_cast<blink::mojom::FencedFrameOwnerHost*>(fenced_frame)
        ->Navigate(invalid_url, base::TimeTicks());
    EXPECT_EQ(bad_message::FF_NAVIGATION_INVALID_URL, kill_waiter.Wait());
  }
}

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
                       ChangeFencedFrameSandboxFlags) {
  GURL main_frame_url(https_server()->GetURL("a.test", "/simple_page.html"));

  EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
  RenderFrameHostImpl* root_rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  const GURL fenced_frame_url =
      https_server()->GetURL("a.test", "/fenced_frames/sandbox_flags.html");
  constexpr char kAddFencedFrameScript[] = R"({
      const fenced_frame = document.createElement('fencedframe');
      fenced_frame.config = new FencedFrameConfig($1);
      document.body.appendChild(fenced_frame);
    })";
  EXPECT_TRUE(
      ExecJs(root_rfh, JsReplace(kAddFencedFrameScript, fenced_frame_url)));

  RenderFrameHostImpl* fenced_rfh = nullptr;
  RenderFrameHostImpl* parent_rfh = nullptr;

  std::vector<FencedFrame*> fenced_frames = root_rfh->GetFencedFrames();
  EXPECT_EQ(fenced_frames.size(), 1u);
  FencedFrame* new_fenced_frame = fenced_frames.back();
  fenced_rfh = new_fenced_frame->GetInnerRoot();
  parent_rfh = fenced_rfh;

  RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess());

  blink::FramePolicy first_policy =
      fenced_rfh->frame_tree_node()->pending_frame_policy();

  first_policy.sandbox_flags = blink::kFencedFrameMandatoryUnsandboxedFlags;
  static_cast<blink::mojom::LocalFrameHost*>(parent_rfh)
      ->DidChangeFramePolicy(std::move(fenced_rfh->GetFrameToken()),
                             std::move(first_policy));

  EXPECT_EQ(bad_message::RFH_SANDBOX_FLAGS, kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
                       PullFocusAcrossFencedBoundary) {
  base::HistogramTester histogram_tester;
  GURL main_frame_url(https_server()->GetURL("a.test", "/simple_page.html"));

  EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
  RenderFrameHostImpl* root_rfh = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  const GURL fenced_frame_url =
      https_server()->GetURL("a.test", "/fenced_frames/button.html");
  constexpr char kAddFencedFrameScript[] = R"({
      const fenced_frame = document.createElement('fencedframe');
      fenced_frame.config = new FencedFrameConfig($1);
      document.body.appendChild(fenced_frame);
    })";
  EXPECT_TRUE(
      ExecJs(root_rfh, JsReplace(kAddFencedFrameScript, fenced_frame_url)));

  RenderFrameHostImpl* fenced_rfh = nullptr;

  std::vector<FencedFrame*> fenced_frames = root_rfh->GetFencedFrames();
  EXPECT_EQ(fenced_frames.size(), 1u);
  FencedFrame* new_fenced_frame = fenced_frames.back();
  fenced_rfh = new_fenced_frame->GetInnerRoot();

  root_rfh->DidFocusFrame();
  root_rfh->GetRenderWidgetHost()->ResetLostFocus();

  // The fenced frame should not be allowed to focus because it won't have
  // user activation, and the RenderWidgetHost won't have recently lost focus.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess());
  fenced_rfh->DidFocusFrame();
  EXPECT_EQ(bad_message::RFH_FOCUS_ACROSS_FENCED_BOUNDARY, kill_waiter.Wait());
}

// Regression test for crbug.com/40221940: a _unfencedTop navigation from a
// fenced frame must use an opaque initiator_origin. Previously the browser
// forwarded the renderer-supplied initiator_origin to NavigateFromFrameProxy(),
// leaking the fenced frame's identity to the destination via Sec-Fetch-Site.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
                       UnfencedTopNavigationHasOpaqueInitiatorOrigin) {
  GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  // Create a fenced frame and get its inner root RFH. Use a URL under
  // /fenced_frames/ which has the Supports-Loading-Mode header.
  GURL fenced_url(
      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
  RenderFrameHost* fenced_rfh_generic =
      fenced_frame_test_helper().CreateFencedFrame(
          shell()->web_contents()->GetPrimaryMainFrame(), fenced_url);
  ASSERT_TRUE(fenced_rfh_generic);
  RenderFrameHostImpl* fenced_rfh =
      static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);

  // The destination URL for the _unfencedTop navigation.
  GURL dest_url(https_server()->GetURL("b.test", "/simple_page.html"));

  // Watch for the navigation to start so we can inspect the initiator origin.
  TestNavigationManager nav_manager(shell()->web_contents(), dest_url);

  // Inject an OpenURL IPC from the fenced frame with
  // is_unfenced_top_navigation=true and a non-opaque initiator_origin.
  // A compromised renderer can set initiator_origin to any origin its
  // process hosts; VerifyInitiatorOrigin() will accept it.
  auto params = CreateOpenURLParams(dest_url);
  params->is_unfenced_top_navigation = true;
  params->initiator_origin = url::Origin::Create(fenced_url);
  ASSERT_FALSE(params->initiator_origin.opaque());
  static_cast<mojom::FrameHost*>(fenced_rfh)->OpenURL(std::move(params));

  // Wait for the navigation to reach the network.
  ASSERT_TRUE(nav_manager.WaitForRequestStart());

  // The fix replaces the renderer-supplied initiator_origin with an opaque
  // origin before passing it to NavigateFromFrameProxy(). Verify that the
  // navigation handle reflects the opaque origin.
  NavigationHandle* handle = nav_manager.GetNavigationHandle();
  ASSERT_TRUE(handle);
  ASSERT_TRUE(handle->GetInitiatorOrigin().has_value());
  EXPECT_TRUE(handle->GetInitiatorOrigin()->opaque())
      << "Expected opaque initiator origin for _unfencedTop navigation, got: "
      << handle->GetInitiatorOrigin()->Serialize();
}

// Verify that binding VibrationManager is rejected when requested from within a
// fenced frame.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
                       VibrationManagerBindRejectedInFencedFrame) {
  GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  GURL fenced_url(
      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
  RenderFrameHost* fenced_rfh_generic =
      fenced_frame_test_helper().CreateFencedFrame(
          shell()->web_contents()->GetPrimaryMainFrame(), fenced_url);
  ASSERT_TRUE(fenced_rfh_generic);
  RenderFrameHostImpl* fenced_rfh =
      static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);

  RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess());

  mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver =
      fenced_rfh->browser_interface_broker_receiver_for_testing();
  blink::mojom::BrowserInterfaceBroker* broker =
      broker_receiver.internal_state()->impl();

  mojo::Remote<device::mojom::VibrationManager> remote;
  broker->GetInterface(remote.BindNewPipeAndPassReceiver());

  EXPECT_EQ(bad_message::BIBI_BIND_VIBRATION_MANAGER_FOR_FENCED_FRAME,
            kill_waiter.Wait());
}

// Verify that binding BatteryMonitor is rejected when requested from within a
// fenced frame.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames,
                       BatteryMonitorBindRejectedInFencedFrame) {
  GURL main_url(https_server()->GetURL("a.test", "/simple_page.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  GURL fenced_url(
      https_server()->GetURL("a.test", "/fenced_frames/title1.html"));
  RenderFrameHost* fenced_rfh_generic =
      fenced_frame_test_helper().CreateFencedFrame(
          shell()->web_contents()->GetPrimaryMainFrame(), fenced_url);
  ASSERT_TRUE(fenced_rfh_generic);
  RenderFrameHostImpl* fenced_rfh =
      static_cast<RenderFrameHostImpl*>(fenced_rfh_generic);

  RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess());

  mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver =
      fenced_rfh->browser_interface_broker_receiver_for_testing();
  blink::mojom::BrowserInterfaceBroker* broker =
      broker_receiver.internal_state()->impl();

  mojo::Remote<device::mojom::BatteryMonitor> remote;
  broker->GetInterface(remote.BindNewPipeAndPassReceiver());

  EXPECT_EQ(bad_message::BIBI_BIND_BATTERY_MONITOR_FOR_FENCED_FRAME,
            kill_waiter.Wait());
}

namespace {

// Interceptor that replaces the origin in the DidCommitProvisionalLoadParams
// with the specified value for the first DidCommitProvisionalLoad message it
// observes in the given |web_contents| while in scope.
class DidCommitParamsOriginReplacer : public DidCommitNavigationInterceptor {
 public:
  DidCommitParamsOriginReplacer(WebContents* web_contents,
                                url::Origin origin_override)
      : DidCommitNavigationInterceptor(web_contents),
        origin_override_(std::move(origin_override)) {}

  DidCommitParamsOriginReplacer(const DidCommitParamsOriginReplacer&) = delete;
  DidCommitParamsOriginReplacer& operator=(
      const DidCommitParamsOriginReplacer&) = delete;

  ~DidCommitParamsOriginReplacer() override = default;

 protected:
  bool WillProcessDidCommitNavigation(
      RenderFrameHost* render_frame_host,
      NavigationRequest* navigation_request,
      mojom::DidCommitProvisionalLoadParamsPtr* params,
      mojom::DidCommitProvisionalLoadInterfaceParamsPtr*) override {
    (*params)->origin = origin_override_;

    return true;
  }

 private:
  url::Origin origin_override_;
};

}  // namespace

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginToCommit) {
  // Navigate normally.
  GURL url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  // Navigate again, but make it so that the DidCommitParams origin will
  // mismatch, causing a renderer kill.
  GURL url_2(embedded_test_server()->GetURL("a.test", "/title2.html"));
  NavigationHandleObserver navigation_observer(shell()->web_contents(), url_2);
  DidCommitParamsOriginReplacer replacer(shell()->web_contents(),
                                         url::Origin::Create(GURL("b.test")));
  RenderProcessHostBadIpcMessageWaiter kill_waiter(
      shell()->web_contents()->GetPrimaryMainFrame()->GetProcess());
  EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), url_2));
  EXPECT_EQ(bad_message::RFH_ORIGIN_TO_COMMIT_MISMATCH, kill_waiter.Wait());
}

// Ensure that the renderer is terminated if it sends an OpenURL IPC with an
// invalid SWITCH_TO_TAB disposition.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, OpenURLInvalidDisposition) {
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHostImpl* main_frame =
      web_contents->GetPrimaryFrameTree().root()->current_frame_host();

  // Simulate that the renderer is compromised and sends an IPC to OpenURL
  // with an invalid disposition (SWITCH_TO_TAB).
  auto params = CreateOpenURLParams(GURL("http://b.com/title2.html"));
  params->disposition = WindowOpenDisposition::SWITCH_TO_TAB;
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
  static_cast<mojom::FrameHost*>(main_frame)->OpenURL(std::move(params));
  EXPECT_EQ(bad_message::RFH_OPEN_URL_INVALID_DISPOSITION, kill_waiter.Wait());
  EXPECT_FALSE(main_frame->IsRenderFrameLive());
}

// Ensure that the renderer is terminated if it sends a CreateNewWindow IPC with
// an invalid SWITCH_TO_TAB disposition. Similar to the test above, except that
// it uses CreateNewWindow instead of OpenURL.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       CreateNewWindowInvalidDisposition) {
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHostImpl* main_frame =
      web_contents->GetPrimaryFrameTree().root()->current_frame_host();

  // Simulate that the renderer is compromised and sends an IPC to OpenURL
  // with an invalid disposition (SWITCH_TO_TAB).
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
  mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
  params->target_url = GURL("https://b.com");
  params->disposition = WindowOpenDisposition::SWITCH_TO_TAB;
  main_frame->CreateNewWindow(std::move(params), base::DoNothing());
  EXPECT_EQ(bad_message::RFH_CREATE_NEW_WINDOW_INVALID_DISPOSITION,
            kill_waiter.Wait());
  EXPECT_FALSE(main_frame->IsRenderFrameLive());
}

namespace {
class ScopedDumpWithoutCrashingCallback {
 public:
  explicit ScopedDumpWithoutCrashingCallback(base::RepeatingClosure callback) {
    DCHECK(!callback_);
    callback_ = std::move(callback);
    base::debug::SetDumpWithoutCrashingFunction(&DumpWithoutCrashingHandler);
  }

  ~ScopedDumpWithoutCrashingCallback() {
    base::debug::SetDumpWithoutCrashingFunction(nullptr);
    callback_.Reset();
  }

 private:
  static void DumpWithoutCrashingHandler() {
    if (callback_) {
      callback_.Run();
    }
  }

  static base::RepeatingClosure callback_;
};

base::RepeatingClosure ScopedDumpWithoutCrashingCallback::callback_;

class NavigationHeaderInterceptor : public FrameHostInterceptor {
 public:
  explicit NavigationHeaderInterceptor(WebContents* web_contents)
      : FrameHostInterceptor(web_contents) {}

  NavigationHeaderInterceptor(const NavigationHeaderInterceptor&) = delete;
  NavigationHeaderInterceptor& operator=(const NavigationHeaderInterceptor&) =
      delete;

  void set_headers_to_inject(const std::string& headers) {
    headers_to_inject_ = headers;
  }

  void Activate() { is_activated_ = true; }

  bool WillDispatchBeginNavigation(
      RenderFrameHost* render_frame_host,
      blink::mojom::CommonNavigationParamsPtr* common_params,
      blink::mojom::BeginNavigationParamsPtr* begin_params,
      mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token,
      mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client)
      override {
    if (is_activated_ && headers_to_inject_.has_value()) {
      (*begin_params)->headers = headers_to_inject_.value();
      is_activated_ = false;
    }

    return true;
  }

 private:
  std::optional<std::string> headers_to_inject_;
  bool is_activated_ = false;
};

class SecurityExploitHeaderKillDisabledTest
    : public SecurityExploitBrowserTest {
 public:
  SecurityExploitHeaderKillDisabledTest() : SecurityExploitBrowserTest() {
    feature_list_.InitWithFeatures({},
                                   {features::kKillOnInvalidNavigationHeaders});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

}  // namespace

// Tests that the renderer is killed if it provides arbitrary headers in a
// navigation request when kKillOnInvalidNavigationHeaders is enabled.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       ForbiddenHeaderInBeginNavigation) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  NavigationHeaderInterceptor interceptor(shell()->web_contents());
  interceptor.set_headers_to_inject("Cookie: secret=123");
  interceptor.Activate();

  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());

  ExecuteScriptAsync(rfhi, "location = '/title2.html';");
  EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait());
  EXPECT_FALSE(rfhi->IsRenderFrameLive());
}

// Tests that the renderer is killed if it provides arbitrary headers in a
// navigation request when the headers are separated by LF rather than CRLF.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       ForbiddenHeaderInBeginNavigation_LFSeparated) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  NavigationHeaderInterceptor interceptor(shell()->web_contents());
  interceptor.set_headers_to_inject("DNT: 1\nCookie: secret=123");
  interceptor.Activate();

  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());

  ExecuteScriptAsync(rfhi, "location = '/title2.html';");
  EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait());
  EXPECT_FALSE(rfhi->IsRenderFrameLive());
}

// Tests that the navigation succeeds if a renderer process provides arbitrary
// headers in a navigation request when kKillOnInvalidNavigationHeaders is
// disabled.
IN_PROC_BROWSER_TEST_F(SecurityExploitHeaderKillDisabledTest,
                       ForbiddenHeaderInBeginNavigation_KillDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  NavigationHeaderInterceptor interceptor(shell()->web_contents());
  interceptor.set_headers_to_inject("Cookie: secret=123");
  interceptor.Activate();

  RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame();

  TestNavigationObserver navigation_observer(shell()->web_contents());
  ExecuteScriptAsync(rfhi, "location = '/title2.html';");
  navigation_observer.Wait();

  EXPECT_EQ(embedded_test_server()->GetURL("a.test", "/title2.html"),
            shell()->web_contents()->GetLastCommittedURL());
}

// Tests that the renderer is killed if it provides arbitrary headers in an
// OpenURL request when kKillOnInvalidNavigationHeaders is enabled.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, ForbiddenHeaderInOpenURL) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  auto params = CreateOpenURLParams(
      embedded_test_server()->GetURL("a.test", "/title2.html"));
  params->extra_headers = "Cookie: secret=123";

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
  static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
  EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait());
  EXPECT_FALSE(rfhi->IsRenderFrameLive());
}

// Tests that the renderer is killed if it provides arbitrary headers in an
// OpenURL request when the headers are separated by LF rather than CRLF.
// OpenURLParams::extra_headers are normalized from LF to CRLF before they are
// applied to the outgoing request, so the allowlist check must apply the same
// normalization.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       ForbiddenHeaderInOpenURL_LFSeparated) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  auto params = CreateOpenURLParams(
      embedded_test_server()->GetURL("a.test", "/title2.html"));
  params->extra_headers = "DNT: 1\nCookie: secret=123";

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
  static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
  EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait());
  EXPECT_FALSE(rfhi->IsRenderFrameLive());
}

// Tests that the navigation succeeds if a renderer process provides arbitrary
// headers in an OpenURL request when kKillOnInvalidNavigationHeaders is
// disabled.
IN_PROC_BROWSER_TEST_F(SecurityExploitHeaderKillDisabledTest,
                       ForbiddenHeaderInOpenURL_KillDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  auto params = CreateOpenURLParams(
      embedded_test_server()->GetURL("a.test", "/title2.html"));
  params->extra_headers = "Cookie: secret=123";

  TestNavigationObserver navigation_observer(shell()->web_contents());
  static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
  navigation_observer.Wait();

  EXPECT_EQ(embedded_test_server()->GetURL("a.test", "/title2.html"),
            shell()->web_contents()->GetLastCommittedURL());
}

namespace {
// Used to communicate info about whether an Origin header was present
// from a URLLoaderInterceptor on the IO thread back to the test running on the
// UI thread.
struct ForbiddenOriginHeaderSharedState
    : public base::RefCountedThreadSafe<ForbiddenOriginHeaderSharedState> {
  ForbiddenOriginHeaderSharedState() = default;
  std::atomic<bool> intercepted{false};
  std::atomic<bool> has_origin_header{false};

 private:
  friend class base::RefCountedThreadSafe<ForbiddenOriginHeaderSharedState>;
  ~ForbiddenOriginHeaderSharedState() = default;
};
}  // namespace

// Tests that if a renderer provides an Origin header on an OpenURL request,
// the renderer is killed.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       ForbiddenOriginHeaderInOpenURL) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  auto params = CreateOpenURLParams(
      embedded_test_server()->GetURL("a.test", "/echoheader?origin"));
  params->extra_headers = "Origin: https://b.test";

  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());

  // Check that if a request was sent, the Origin header was stripped.
  auto state = base::MakeRefCounted<ForbiddenOriginHeaderSharedState>();
  URLLoaderInterceptor interceptor(base::BindRepeating(
      [](scoped_refptr<ForbiddenOriginHeaderSharedState> state,
         URLLoaderInterceptor::RequestParams* params) {
        if (params->url_request.url.path() == "/echoheader") {
          state->intercepted = true;
          state->has_origin_header = params->url_request.headers.HasHeader(
              net::HttpRequestHeaders::kOrigin);
        }
        return false;
      },
      state));

  static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
  EXPECT_EQ(bad_message::NR_BAD_ORIGIN_HEADER, kill_waiter.Wait());

  if (state->intercepted) {
    EXPECT_FALSE(state->has_origin_header);
  }
}

class SecurityExploitKillDisabledBrowserTest
    : public SecurityExploitBrowserTest {
 public:
  SecurityExploitKillDisabledBrowserTest() {
    feature_list_.InitAndDisableFeature(
        features::kKillOnUnexpectedOriginHeader);
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Tests that if the feature flag is disabled, the renderer is not killed.
IN_PROC_BROWSER_TEST_F(SecurityExploitKillDisabledBrowserTest,
                       ForbiddenOriginHeaderInOpenURL_KillDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());
  RenderProcessHost* rph = rfhi->GetProcess();

  auto params = CreateOpenURLParams(
      embedded_test_server()->GetURL("a.test", "/echoheader?origin"));
  params->extra_headers = "Origin: https://b.test";

  TestNavigationObserver nav_observer(shell()->web_contents());
  static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params));
  nav_observer.Wait();

  EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), rph);
  EXPECT_TRUE(rph->IsInitializedAndNotDead());
  EXPECT_EQ("None", EvalJs(shell(), "document.body.innerText").ExtractString());
}

// Tests that if a renderer provides an Origin header on a GET request,
// the renderer is killed.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OriginHeaderGETMismatchesInitiator) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  NavigationHeaderInterceptor interceptor(shell()->web_contents());
  interceptor.set_headers_to_inject("Origin: https://b.test");
  interceptor.Activate();

  RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
  ExecuteScriptAsync(rfhi, "location = '/echoheader?origin';");
  EXPECT_EQ(bad_message::NR_BAD_ORIGIN_HEADER, kill_waiter.Wait());
}

// Tests that if the feature flag is disabled, the renderer is not killed.
IN_PROC_BROWSER_TEST_F(SecurityExploitKillDisabledBrowserTest,
                       OriginHeaderPOSTMismatchesInitiator_KillDisabled) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  NavigationHeaderInterceptor interceptor(shell()->web_contents());
  interceptor.set_headers_to_inject("Origin: https://b.test");
  interceptor.Activate();

  RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame();
  RenderProcessHost* rph = rfhi->GetProcess();
  TestNavigationObserver nav_observer(shell()->web_contents());
  ExecuteScriptAsync(
      rfhi,
      "var f = document.createElement('form'); f.action='/echoheader?origin'; "
      "f.method='POST'; document.body.appendChild(f); f.submit();");
  nav_observer.Wait();

  EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), rph);
  EXPECT_TRUE(rph->IsInitializedAndNotDead());
  EXPECT_EQ(url::Origin::Create(start_url).Serialize(),
            EvalJs(shell(), "document.body.innerText").ExtractString());
}

// Tests that if a renderer provides an Origin header on a cross-origin GET
// request, the renderer is killed.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OriginHeaderCrossOriginGETMismatchesInitiator) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  NavigationHeaderInterceptor interceptor(shell()->web_contents());
  interceptor.set_headers_to_inject("Origin: https://b.test");
  interceptor.Activate();

  RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
  ExecuteScriptAsync(rfhi, "location = '" +
                               embedded_test_server()
                                   ->GetURL("c.test", "/echoheader?origin")
                                   .spec() +
                               "'");
  EXPECT_EQ(bad_message::NR_BAD_ORIGIN_HEADER, kill_waiter.Wait());
}

// Tests that a renderer can provide an Origin header on a POST request but
// the renderer is killed if the origin was incorrect.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OriginHeaderPOSTMismatchesInitiator) {
  GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), start_url));

  NavigationHeaderInterceptor interceptor(shell()->web_contents());
  interceptor.set_headers_to_inject("Origin: https://b.test");
  interceptor.Activate();

  RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess());
  ExecuteScriptAsync(
      rfhi,
      "var f = document.createElement('form'); f.action='/echoheader?origin'; "
      "f.method='POST'; document.body.appendChild(f); f.submit();");
  EXPECT_EQ(bad_message::NR_BAD_ORIGIN_HEADER, kill_waiter.Wait());
}

// Tests that a history navigation to a page that was originally reached via a
// POST-to-GET redirect does not crash/dump.
//
// Note: unlike other tests in this file, this test does not simulate a
// compromised renderer. Rather, the test is checking that a legitimate code
// path doesn't regress when we add stricter checks for a compromised renderer.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OriginHeaderInHistoryNavigation) {
  GURL url1(embedded_test_server()->GetURL("a.test", "/title1.html"));
  GURL url2(embedded_test_server()->GetURL("a.test", "/title2.html"));
  GURL redirect_url(embedded_test_server()->GetURL(
      "a.test", "/server-redirect?" + url1.spec()));

  // Perform a browser-initiated POST request with a non-null Origin
  // header that redirects to url1.
  {
    TestNavigationObserver nav_observer(shell()->web_contents());
    NavigationController::LoadURLParams load_params(redirect_url);
    load_params.load_type = NavigationController::LOAD_TYPE_HTTP_POST;
    load_params.post_data = network::ResourceRequestBody::CreateFromBytes(
        std::vector<uint8_t>{'f', 'o', 'o'});
    load_params.extra_headers = "Origin: https://origin.test";
    shell()->web_contents()->GetController().LoadURLWithParams(load_params);
    nav_observer.Wait();
    EXPECT_EQ(url1, shell()->web_contents()->GetLastCommittedURL());
  }

  // Navigate elsewhere so that we can navigate back.
  EXPECT_TRUE(NavigateToURL(shell(), url2));

  TestNavigationObserver back_nav_observer(shell()->web_contents());
  shell()->web_contents()->GetController().GoBack();
  back_nav_observer.Wait();

  // There should be no kill on the Back navigation because browser-initiated
  // navigations are excluded.
  EXPECT_TRUE(
      shell()->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive());
}

// Tests that a renderer-initiated history navigation with an incorrect Origin
// header does not kill the renderer.
//
// Note: unlike other tests in this file, this test does not simulate a
// compromised renderer. Rather, the test is checking that a legitimate code
// path doesn't regress when we add stricter checks for a compromised renderer.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OriginHeaderInRendererInitiatedHistoryNavigation) {
  GURL url1(embedded_test_server()->GetURL("a.test", "/echoheader?Origin"));
  GURL url2(embedded_test_server()->GetURL("a.test", "/title1.html"));

  // Perform an initial navigation with an Origin header.
  // We use browser-initiated navigation to set up the entry with an Origin
  // header. We use a header that is NOT expected for a GET navigation to ensure
  // it would trigger a kill if it were renderer-initiated.
  NavigationController::LoadURLParams load_params(url1);
  load_params.extra_headers = "Origin: https://foo.test";
  TestNavigationObserver nav_observer1(shell()->web_contents());
  shell()->web_contents()->GetController().LoadURLWithParams(load_params);
  nav_observer1.Wait();

  // Verify that url1 shows the Origin header.
  EXPECT_THAT(EvalJs(shell()->web_contents(), "document.body.innerText")
                  .ExtractString(),
              HasSubstr("https://foo.test"));

  // Navigate away so we can go back.
  EXPECT_TRUE(NavigateToURL(shell(), url2));

  // Perform a renderer-initiated back navigation.
  TestNavigationObserver back_nav_observer(shell()->web_contents());
  ExecuteScriptAsync(shell()->web_contents(), "history.back();");
  back_nav_observer.Wait();

  // There should be no kill on the Back navigation because history navigations
  // are excluded.
  EXPECT_TRUE(
      shell()->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive());

  // Verify that the back navigation also sent the Origin header.
  EXPECT_THAT(EvalJs(shell()->web_contents(), "document.body.innerText")
                  .ExtractString(),
              HasSubstr("https://foo.test"));
}

// Verify that EnterFullscreen from a cross-origin child frame without the
// allowfullscreen attribute is rejected with a bad message. The renderer-side
// Permissions Policy check should prevent this IPC, so receiving it indicates a
// compromised renderer. See https://crbug.com/488514078.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       EnterFullscreenDisallowedByPermissionsPolicy) {
  IsolateOrigin("b.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  // Create a cross-origin child frame WITHOUT allowfullscreen.
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
  ASSERT_FALSE(subframe->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kFullscreen));

  // Simulate a compromised renderer sending EnterFullscreen despite the
  // Permissions Policy denial. The browser should kill the renderer.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  subframe->EnterFullscreen(blink::mojom::FullscreenOptions::New(),
                            base::DoNothing());
  EXPECT_EQ(bad_message::RFH_ENTER_FULLSCREEN_PERMISSION_DENIED,
            kill_waiter.Wait());
  EXPECT_FALSE(subframe->IsRenderFrameLive());
}

// Verify that EnterFullscreen from a cross-origin child frame WITH the
// allowfullscreen attribute does NOT trigger a bad message.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       EnterFullscreenAllowedByPermissionsPolicy) {
  IsolateOrigin("b.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  // Create a cross-origin child frame WITH allowfullscreen.
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.src = '%s'; "
        "frame.allowFullscreen = true; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
  ASSERT_TRUE(subframe->IsFeatureEnabled(
      network::mojom::PermissionsPolicyFeature::kFullscreen));

  // EnterFullscreen should proceed without killing the renderer.
  subframe->EnterFullscreen(blink::mojom::FullscreenOptions::New(),
                            base::DoNothing());
  EXPECT_TRUE(subframe->IsRenderFrameLive());
}

// Verify that EnterFullscreen drops the is_xr_overlay flag when no XR setup
// was recorded by the browser.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       EnterFullscreenIgnoresXrOverlayFlagWithoutXrSetup) {
  IsolateOrigin("b.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.src = '%s'; "
        "frame.allowFullscreen = true; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();

  base::HistogramTester histogram_tester;

  // Give subframe transient user activation.
  subframe->UpdateUserActivationState(
      blink::mojom::UserActivationUpdateType::kNotifyActivation,
      blink::mojom::UserActivationNotificationType::kInteraction);

  auto options = blink::mojom::FullscreenOptions::New();
  options->is_xr_overlay = true;

  subframe->EnterFullscreen(std::move(options), base::DoNothing());

  // Wait for parent frame to process fullscreen change.
  EXPECT_TRUE(ExecJs(main_frame,
                     "new Promise(resolve => {"
                     "  if (document.fullscreenElement) resolve();"
                     "  else document.addEventListener('fullscreenchange', () "
                     "=> resolve(), {once: true});"
                     "});"));

  // Parent frame's iframe should match :fullscreen but NOT :xr-overlay.
  EXPECT_EQ(true,
            EvalJs(main_frame,
                   "document.querySelector('iframe').matches(':fullscreen')"));
  EXPECT_EQ(false,
            EvalJs(main_frame,
                   "document.querySelector('iframe').matches(':xr-overlay')"));

  histogram_tester.ExpectUniqueSample(
      "XR.DOMOverlay.IsXrOverlayFullscreenValid", false, 1);
}

// Verify that EnterFullscreen preserves the is_xr_overlay flag when XR setup
// was recorded by the browser.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       EnterFullscreenPropagatesXrOverlayFlagWithXrSetup) {
  IsolateOrigin("b.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();

  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.src = '%s'; "
        "frame.allowFullscreen = true; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }

  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();

  base::HistogramTester histogram_tester;

  // Record that an XR overlay setup occurred.
  subframe->SetIsXrOverlaySetup();

  auto options = blink::mojom::FullscreenOptions::New();
  options->is_xr_overlay = true;

  subframe->EnterFullscreen(std::move(options), base::DoNothing());

  // Wait for parent frame to process fullscreen change.
  EXPECT_TRUE(ExecJs(main_frame,
                     "new Promise(resolve => {"
                     "  if (document.fullscreenElement) resolve();"
                     "  else document.addEventListener('fullscreenchange', () "
                     "=> resolve(), {once: true});"
                     "});"));

  // Parent frame's iframe SHOULD match :xr-overlay.
  EXPECT_EQ(true,
            EvalJs(main_frame,
                   "document.querySelector('iframe').matches(':xr-overlay')"));

  histogram_tester.ExpectUniqueSample(
      "XR.DOMOverlay.IsXrOverlayFullscreenValid", true, 1);
}

// Regression test for browser-side validation of the allow-pointer-lock
// sandbox attribute. A sandboxed frame without allow-pointer-lock should not
// be able to acquire pointer lock via Mojo IPC.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       PointerLockDisallowedFromSandboxedFrame) {
  IsolateOrigin("b.com");
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.sandbox = 'allow-scripts'; "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }
  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
  EXPECT_TRUE(
      subframe->IsSandboxed(network::mojom::WebSandboxFlags::kPointerLock));
  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  RenderWidgetHostImpl* rwh = subframe->GetRenderWidgetHost();
  rwh->RequestMouseLock(false, false, base::DoNothing());
  EXPECT_EQ(bad_message::RWH_POINTER_LOCK_FROM_SANDBOXED_FRAME,
            kill_waiter.Wait());
  EXPECT_FALSE(subframe->IsRenderFrameLive());
}

// Regression test for browser-side validation of the
// allow-storage-access-by-user-activation sandbox attribute. A sandboxed frame
// without that token should not be able to request the storage-access
// permission by binding blink::mojom::PermissionService directly, bypassing
// the Blink-side check in DocumentStorageAccess::RequestStorageAccessImpl().
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       StorageAccessDisallowedFromSandboxedFrame) {
  IsolateOrigin("b.com");
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.sandbox = 'allow-scripts allow-same-origin'; "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }
  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
  ASSERT_TRUE(subframe->IsSandboxed(
      network::mojom::WebSandboxFlags::kStorageAccessByUserActivation));

  // Bind PermissionService straight at the sandboxed frame, the way a
  // compromised renderer would, skipping Blink's own gate entirely.
  mojo::Remote<blink::mojom::PermissionService> permission_service;
  PermissionServiceContext::GetOrCreateForCurrentDocument(subframe)
      ->CreateService(permission_service.BindNewPipeAndPassReceiver());

  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  auto descriptor = blink::mojom::PermissionDescriptor::New();
  descriptor->name = blink::mojom::PermissionName::STORAGE_ACCESS;
  permission_service->RequestPermission(std::move(descriptor),
                                        base::DoNothing());
  EXPECT_EQ(bad_message::PSI_STORAGE_ACCESS_FROM_SANDBOXED_FRAME,
            kill_waiter.Wait());
  EXPECT_FALSE(subframe->IsRenderFrameLive());
}

// Tests that a compromised renderer cannot exploit a CSP-blocked subframe error
// page to place a srcdoc frame into a sandboxed SiteInstance for a site that it
// doesn't have access to. This verifies the fix for
// https://crbug.com/502348223.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       ErrorPagePrecursorDoesNotLeakToSandboxedSrcdoc) {
  GURL attacker_url(
      embedded_test_server()->GetURL("attacker.test", "/title1.html"));
  GURL victim_url(
      embedded_test_server()->GetURL("victim.test", "/title1.html"));

  EXPECT_TRUE(NavigateToURL(shell(), attacker_url));
  RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  // Set CSP to block iframes, so we get an error page.
  EXPECT_TRUE(ExecJs(main_frame,
                     "var meta = document.createElement('meta');"
                     "meta.httpEquiv = 'Content-Security-Policy';"
                     "meta.content = \"frame-src 'none'\";"
                     "document.head.appendChild(meta);"));

  // Create an iframe to victim.test. It will be blocked and commit an error
  // page.
  TestNavigationObserver nav_observer(shell()->web_contents());
  EXPECT_TRUE(
      ExecJs(main_frame, JsReplace("var f = document.createElement('iframe');"
                                   "f.src = $1;"
                                   "document.body.appendChild(f);",
                                   victim_url)));
  nav_observer.Wait();

  EXPECT_FALSE(nav_observer.last_navigation_succeeded());
  EXPECT_EQ(net::ERR_BLOCKED_BY_CSP, nav_observer.last_net_error_code());

  RenderFrameHostImpl* error_frame =
      main_frame->child_at(0)->current_frame_host();
  EXPECT_TRUE(error_frame->IsErrorDocument());
  if (SiteIsolationPolicy::IsErrorPageIsolationEnabled(
          /*in_main_frame=*/false)) {
    ASSERT_NE(error_frame->GetProcess(), main_frame->GetProcess());
  } else {
    ASSERT_EQ(error_frame->GetProcess(), main_frame->GetProcess());
  }

  // Simulate a compromised renderer by injecting a sandboxed srcdoc inside the
  // error page. Once we have error page isolation for subframes, attacker.test
  // won't be able to do this step, and the precursor clearing defense that the
  // test verifies below would not apply (since it's not needed). So it only
  // makes sense to continue with this test when there's no subframe error page
  // isolation.
  if (SiteIsolationPolicy::IsErrorPageIsolationEnabled(
          /*in_main_frame=*/false)) {
    return;
  }
  TestNavigationObserver srcdoc_observer(shell()->web_contents());
  EXPECT_TRUE(ExecJs(error_frame,
                     "var f = document.createElement('iframe');"
                     "f.sandbox = 'allow-scripts';"
                     "f.srcdoc = 'foo';"
                     "document.body.appendChild(f);"));
  srcdoc_observer.Wait();

  RenderFrameHostImpl* srcdoc_frame =
      error_frame->child_at(0)->current_frame_host();

  // With the fix, the error page's opaque origin has no precursor. Check that
  // the sandboxed srcdoc's SiteInstance was not derived from victim.test.
  EXPECT_TRUE(srcdoc_frame->GetLastCommittedOrigin().opaque());
  EXPECT_TRUE(srcdoc_frame->GetLastCommittedOrigin()
                  .GetTupleOrPrecursorTupleIfOpaque()
                  .GetURL()
                  .is_empty());
  SiteInfo site_info = srcdoc_frame->GetSiteInstance()->GetSiteInfo();
  EXPECT_FALSE(site_info.site_url().DomainIs("victim.test"));

  // OOPSIFs require site isolation, so the srcdoc frame will be in a new
  // sandboxed process if site isolation is enabled; otherwise, it will go into
  // the error page's current unsandboxed process.
  if (AreAllSitesIsolatedForTesting()) {
    EXPECT_TRUE(site_info.IsSandboxed());
    EXPECT_NE(srcdoc_frame->GetProcess(), error_frame->GetProcess());
  } else {
    EXPECT_FALSE(site_info.IsSandboxed());
    EXPECT_EQ(srcdoc_frame->GetProcess(), error_frame->GetProcess());
  }
}

// Verify that binding GamepadMonitor is rejected when the Permissions Policy
// denies gamepad access.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       GamepadMonitorBlockedByPermissionsPolicy) {
  GURL url(
      embedded_test_server()->GetURL("/gamepad-permissions-policy-none.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  auto* frame = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess());

  mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver =
      frame->browser_interface_broker_receiver_for_testing();
  blink::mojom::BrowserInterfaceBroker* broker =
      broker_receiver.internal_state()->impl();

  mojo::Remote<device::mojom::GamepadMonitor> remote;
  broker->GetInterface(remote.BindNewPipeAndPassReceiver());

  EXPECT_EQ(
      bad_message::BIBI_BIND_GAMEPAD_MONITOR_BLOCKED_BY_PERMISSIONS_POLICY,
      kill_waiter.Wait());
}

// Verify that binding GamepadHapticsManager is rejected when the Permissions
// Policy denies gamepad access.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       GamepadHapticsManagerBlockedByPermissionsPolicy) {
  GURL url(
      embedded_test_server()->GetURL("/gamepad-permissions-policy-none.html"));
  EXPECT_TRUE(NavigateToURL(shell(), url));

  auto* frame = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess());

  mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver =
      frame->browser_interface_broker_receiver_for_testing();
  blink::mojom::BrowserInterfaceBroker* broker =
      broker_receiver.internal_state()->impl();

  mojo::Remote<device::mojom::GamepadHapticsManager> remote;
  broker->GetInterface(remote.BindNewPipeAndPassReceiver());

  EXPECT_EQ(bad_message::
                BIBI_BIND_GAMEPAD_HAPTICS_MANAGER_BLOCKED_BY_PERMISSIONS_POLICY,
            kill_waiter.Wait());
}

namespace {

// Helper class to wait for ReadyToCommitNavigation for the next navigation that
// results in an error page, and then to wait for a renderer kill for the
// process that will commit that error page.
class ErrorPageKillWaiter : public WebContentsObserver {
 public:
  explicit ErrorPageKillWaiter(WebContents* web_contents)
      : WebContentsObserver(web_contents) {}

  // WebContentsObserver:
  void ReadyToCommitNavigation(NavigationHandle* navigation_handle) override {
    if (NavigationRequest::From(navigation_handle)->DidEncounterError()) {
      kill_waiter_ = std::make_unique<RenderProcessHostBadIpcMessageWaiter>(
          navigation_handle->GetRenderFrameHost()->GetProcess());
      run_loop_.Quit();
    }
  }

  void DidFinishNavigation(NavigationHandle* navigation_handle) override {
    if (!kill_waiter_) {
      run_loop_.Quit();
    }
  }

  std::optional<bad_message::BadMessageReason> Wait() {
    run_loop_.Run();
    if (!kill_waiter_) {
      return std::nullopt;
    }
    return kill_waiter_->Wait();
  }

 private:
  std::unique_ptr<RenderProcessHostBadIpcMessageWaiter> kill_waiter_;
  base::RunLoop run_loop_;
};

}  // namespace

// Tests that a compromised renderer cannot lie about the URL when committing a
// CSP-blocked subframe error page. Such error pages are allowed to commit a URL
// that doesn't match the process lock, but the URL must still match the URL
// that the browser asked the renderer to commit.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       BlockedSubframeErrorPageDidCommitInvalidURL) {
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  GURL blocked_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  GURL spoofed_url(embedded_test_server()->GetURL("c.com", "/title2.html"));

  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>(
      shell()->web_contents()->GetPrimaryMainFrame());

  // Add a CSP that blocks all subframe navigations, so the navigation below
  // commits an error page.
  EXPECT_TRUE(ExecJs(main_frame,
                     "var meta = document.createElement('meta');"
                     "meta.httpEquiv = 'Content-Security-Policy';"
                     "meta.content = \"frame-src 'none'\";"
                     "document.head.appendChild(meta);"));

  // Simulate a compromised renderer that lies about the committed URL.
  DidCommitUrlReplacer url_replacer(shell()->web_contents(), spoofed_url);

  // Set up an observer to capture the error page process right before it
  // commits. Note that we can't assume that it will be the same as the main
  // frame's process due to subframe error page isolation.
  ErrorPageKillWaiter kill_waiter(shell()->web_contents());

  // Create an iframe that will be blocked by CSP and commit an error page. The
  // browser process should detect the URL mismatch and terminate the renderer.
  // We use ExecuteScriptAsync because the renderer might be terminated before
  // ExecJs returns.
  ExecuteScriptAsync(main_frame,
                     JsReplace("var f = document.createElement('iframe');"
                               "f.src = $1;"
                               "document.body.appendChild(f);",
                               blocked_url));
  EXPECT_EQ(bad_message::RFH_ERROR_PAGE_URL_MISMATCH, kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       OrientationLockFromSandboxedFrameIsRecorded) {
  base::HistogramTester histogram_tester;
  IsolateOrigin("b.com");
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.sandbox = 'allow-scripts'; "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }
  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
  ASSERT_TRUE(
      subframe->IsSandboxed(network::mojom::WebSandboxFlags::kOrientationLock));

  ScreenOrientationProvider* provider =
      web_contents->GetScreenOrientationProviderForTesting();
  provider->SetCurrentTargetFrameForTesting(subframe);
  provider->LockOrientation(
      device::mojom::ScreenOrientationLockType::LANDSCAPE_PRIMARY,
      base::DoNothing());
  provider->SetCurrentTargetFrameForTesting(nullptr);

  EXPECT_TRUE(subframe->IsRenderFrameLive());
  histogram_tester.ExpectUniqueSample(
      "Security.ScreenOrientation.LockRequestIsSandboxed", true, 1);
}

IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
                       FormSubmissionDisallowedFromSandboxedFrame) {
  IsolateOrigin("b.com");
  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));
  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root();
  RenderFrameHostImpl* main_frame = root->current_frame_host();
  GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html"));
  {
    std::string js_str = base::StringPrintf(
        "var frame = document.createElement('iframe'); "
        "frame.sandbox = 'allow-scripts'; "
        "frame.src = '%s'; "
        "document.body.appendChild(frame);",
        child_url.spec().c_str());
    EXPECT_TRUE(ExecJs(main_frame, js_str));
    ASSERT_TRUE(WaitForLoadStop(web_contents));
  }
  RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host();
  ASSERT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kForms));

  RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess());
  GURL next_url(embedded_test_server()->GetURL("b.com", "/empty.html"));
  // This navigation should be rejected because `is_form_submission` in
  // `begin_params` is set while the initiating frame is sandboxed with forms.
  blink::mojom::CommonNavigationParamsPtr common_params =
      blink::mojom::CommonNavigationParams::New(
          next_url, subframe->GetLastCommittedOrigin(),
          std::nullopt /* initiator_base_url */, blink::mojom::Referrer::New(),
          ui::PAGE_TRANSITION_LINK,
          blink::mojom::NavigationType::DIFFERENT_DOCUMENT,
          blink::NavigationDownloadPolicy(),
          false /* should_replace_current_entry */,
          GURL() /* base_url_for_data_url */,
          base::TimeTicks::Now() /* actual_navigation_start */,
          base::TimeTicks::Now() /* navigation_start */, "GET",
          nullptr /* post_data */, network::mojom::SourceLocation::New(),
          false /* started_from_context_menu */, false /* has_user_gesture */,
          false /* text_fragment_token */,
          network::mojom::CSPDisposition::CHECK,
          std::vector<int>() /* initiator_origin_trial_features */,
          std::string() /* href_translate */,
          false /* is_history_navigation_in_new_child_frame */,
          base::TimeTicks() /* input_start */,
          network::mojom::RequestDestination::kDocument);
  blink::mojom::BeginNavigationParamsPtr begin_params =
      blink::mojom::BeginNavigationParams::New(
          subframe->GetFrameToken() /* initiator_frame_token */,
          subframe->current_initiator_state_token(),
          subframe->GetDocumentToken(), std::string() /* headers */,
          net::LOAD_NORMAL, false /* skip_service_worker */,
          blink::mojom::RequestContextType::LOCATION,
          blink::mojom::MixedContentContextType::kBlockable,
          true /* is_form_submission: checked above */,
          false /* was_initiated_by_link_click */,
          blink::mojom::ForceHistoryPush::kNo, GURL() /* searchable_form_url */,
          std::string() /* searchable_form_encoding */,
          GURL() /* client_side_redirect_url */,
          std::nullopt /* devtools_initiator_info */,
          nullptr /* trust_token_params */,
          base::TimeTicks() /* renderer_before_unload_start */,
          base::TimeTicks() /* renderer_before_unload_end */,
          base::TimeTicks() /* before_unload_dialog_opened */,
          base::TimeTicks() /* before_unload_dialog_closed */,
          false /* started_with_transient_activation */,
          false /* started_by_ad */, false /* is_container_initiated */,
          false /* has_rel_opener */,
          std::nullopt /* script_tool_invocation_id */);
  mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client;
  auto navigation_client_receiver =
      navigation_client.InitWithNewEndpointAndPassReceiver();
  subframe->frame_host_receiver_for_testing().impl()->BeginNavigation(
      std::move(common_params), std::move(begin_params), mojo::NullRemote(),
      std::move(navigation_client), mojo::NullRemote(), mojo::NullReceiver(),
      mojo::NullReceiver(), mojo::NullReceiver());
  EXPECT_EQ(bad_message::RFH_FORM_SUBMISSION_FROM_SANDBOXED_FRAME,
            kill_waiter.Wait());
  EXPECT_FALSE(subframe->IsRenderFrameLive());
}

// Ensure that the renderer is terminated if it sends a DownloadURL IPC with a
// referrer URL that the renderer's process is not allowed to host.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DownloadURLInvalidReferrer) {
  // Explicitly isolating a.com helps ensure that this test is applicable on
  // platforms without site-per-process.
  IsolateOrigin("a.com");

  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
  EXPECT_TRUE(NavigateToURL(shell(), main_url));

  WebContentsImpl* web_contents =
      static_cast<WebContentsImpl*>(shell()->web_contents());
  RenderFrameHostImpl* main_frame =
      web_contents->GetPrimaryFrameTree().root()->current_frame_host();

  // Simulate that the renderer sends a DownloadURL IPC claiming that the
  // download was referred by a page on a different site.
  auto params = blink::mojom::DownloadURLParams::New();
  params->url = embedded_test_server()->GetURL("a.com", "/title2.html");
  params->initiator_origin = main_frame->GetLastCommittedOrigin();
  params->referrer = blink::mojom::Referrer::New();
  params->referrer->url =
      embedded_test_server()->GetURL("b.com", "/title1.html");
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
  main_frame->DownloadURL(std::move(params));
  EXPECT_EQ(bad_message::RFH_DOWNLOAD_URL_INVALID_REFERRER, kill_waiter.Wait());
  EXPECT_FALSE(main_frame->IsRenderFrameLive());
}

}  // namespace content
