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

#include <memory>

#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/values_test_util.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/chrome_content_browser_client.h"
#include "chrome/browser/extensions/api/messaging/native_messaging_test_util.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/api/automation_internal/automation_event_router.h"
#include "extensions/browser/api/messaging/channel_endpoint.h"
#include "extensions/browser/api/messaging/message_service.h"
#include "extensions/browser/api/storage/storage_api.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/bad_message.h"
#include "extensions/browser/browsertest_util.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_frame_host.h"
#include "extensions/browser/extension_web_contents_observer.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/renderer_startup_helper.h"
#include "extensions/browser/script_injection_tracker.h"
#include "extensions/browser/service_worker/service_worker_host.h"
#include "extensions/common/api/messaging/messaging_endpoint.h"
#include "extensions/common/api/messaging/port_id.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/automation_registry.mojom.h"
#include "extensions/common/mojom/event_router.mojom.h"
#include "extensions/common/mojom/frame.mojom-test-utils.h"
#include "extensions/common/mojom/message_port.mojom.h"
#include "extensions/common/mojom/service_worker_host.mojom-test-utils.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/test_extension_dir.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_database.mojom.h"
#include "url/gurl.h"

namespace extensions {

namespace {

// Helper to serve a page with CSP that blocks subframe navigations.
std::unique_ptr<net::test_server::HttpResponse> HandleCspBlockRequest(
    const net::test_server::HttpRequest& request) {
  if (request.relative_url != "/csp_block.html") {
    return nullptr;
  }
  auto response = std::make_unique<net::test_server::BasicHttpResponse>();
  response->set_code(net::HTTP_OK);
  response->set_content_type("text/html");
  response->AddCustomHeader("Content-Security-Policy", "frame-src 'none'");
  response->set_content(
      "<html><body>"
      "<iframe src=\"https://chromewebstore.google.com/\"></iframe>"
      "</body></html>");
  return response;
}

// A mock `mojom::MessagePort` implementation that allows tests to intercept
// communication from the browser-side `MessageService` (e.g., to verify
// connection success/failure and capture disconnect errors).
class TestMessagePort : public mojom::MessagePort {
 public:
  explicit TestMessagePort(base::RepeatingClosure quit_closure)
      : quit_closure_(std::move(quit_closure)) {}

  bool got_message() const { return got_message_; }

  const std::optional<std::string>& disconnect_error() const {
    return disconnect_error_;
  }

 private:
  void DispatchDisconnect(const std::string& error) override {
    disconnect_error_ = error;
    quit_closure_.Run();
  }

  void DeliverMessage(Message message) override {
    got_message_ = true;
    quit_closure_.Run();
  }

  bool got_message_ = false;
  std::optional<std::string> disconnect_error_;
  base::RepeatingClosure quit_closure_;
};

}  // namespace

// ExtensionFrameHostInterceptor is a helper for:
// - Intercepting mojom::LocalFrameHost method calls (e.g. methods
//   that would normally be handled / implemented by ExtensionFrameHost).
// - Allowing the test to mutate the method arguments (e.g. to simulate a
//   compromised renderer) before passing the method call to the usual handler.
class ExtensionFrameHostInterceptor
    : mojom::LocalFrameHostInterceptorForTesting {
 public:
  // The caller needs to ensure that `frame` stays alive longer than the
  // constructed ExtensionFrameHostInterceptor.
  explicit ExtensionFrameHostInterceptor(content::RenderFrameHost* frame)
      : frame_(frame),
        extension_frame_host_(
            ExtensionWebContentsObserver::GetForWebContents(
                content::WebContents::FromRenderFrameHost(frame_))
                ->extension_frame_host_for_testing()),
        scoped_swap_impl_(extension_frame_host_->receivers_for_testing(),
                          this) {}

  ~ExtensionFrameHostInterceptor() override = default;

  using RequestMutator =
      base::RepeatingCallback<void(mojom::RequestParams& request_params)>;
  void SetRequestMutator(RequestMutator request_mutator) {
    request_mutator_ = std::move(request_mutator);
  }

  using RequestCallbackHook =
      base::RepeatingCallback<void(bool success, const std::string& error)>;
  void SetRequestCallbackHook(RequestCallbackHook interceptor) {
    request_callback_hook_ = std::move(interceptor);
  }

  using OpenChannelToExtensionMutator =
      base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info)>;
  void SetOpenChannelToExtensionMutator(
      OpenChannelToExtensionMutator open_extension_mutator) {
    open_extension_mutator_ = std::move(open_extension_mutator);
  }

  void SimulateWatchedPageChange(
      const std::vector<std::string>& css_selectors) {
    GetForwardingInterface()->WatchedPageChange(css_selectors);
  }

  using WatchedPageChangeMutator =
      base::RepeatingCallback<void(std::vector<std::string>& css_selectors)>;
  void SetWatchedPageChangeMutator(
      WatchedPageChangeMutator watched_page_change_mutator) {
    watched_page_change_mutator_ = std::move(watched_page_change_mutator);
  }

  // We cannot call `FlushForTesting()` on the receiver side because
  // `ExtensionFrameHost` uses `content::RenderFrameHostReceiverSet` which does
  // not expose `FlushForTesting()`. Instead, we do a Mojo round-trip by calling
  // a method with a callback (`GetAppInstallState`) to ensure all previous
  // messages on the pipe have been processed.
  void FlushForTesting() {
    base::RunLoop run_loop;
    GetForwardingInterface()->GetAppInstallState(
        GURL("http://example.com"),
        base::BindOnce(
            [](base::OnceClosure quit_closure, const std::string&) {
              std::move(quit_closure).Run();
            },
            run_loop.QuitClosure()));
    run_loop.Run();
  }

 private:
  mojom::LocalFrameHost* GetForwardingInterface() override {
    return scoped_swap_impl_.old_impl();
  }

  void Request(mojom::RequestParamsPtr params,
               RequestCallback callback) override {
    // `//extensions/common/mojom/frame.mojom` specifies that `params` is
    // non-optional.
    CHECK(params);

    content::RenderFrameHost* current_target_frame =
        &extension_frame_host_->receivers_for_testing().CurrentTargetFrame();
    if (request_mutator_ && frame_ == current_target_frame) {
      request_mutator_.Run(*params);
    }

    // Notify the optional `request_callback_hook_` of the value of the
    // response.
    auto wrapped_callback = base::BindOnce(
        [](RequestCallback original_callback, RequestCallbackHook interceptor,
           bool success, base::ListValue response_wrapper,
           const std::string& error, mojom::ExtraResponseDataPtr extra_data) {
          if (interceptor) {
            interceptor.Run(success, error);
          }
          std::move(original_callback)
              .Run(success, std::move(response_wrapper), error,
                   std::move(extra_data));
        },
        std::move(callback), request_callback_hook_);

    GetForwardingInterface()->Request(std::move(params),
                                      std::move(wrapped_callback));
  }

  void OpenChannelToExtension(
      mojom::ExternalConnectionInfoPtr info,
      mojom::ChannelType channel_type,
      const std::string& channel_name,
      const PortId& port_id,
      mojo::PendingAssociatedRemote<mojom::MessagePort> port,
      mojo::PendingAssociatedReceiver<mojom::MessagePortHost> port_host)
      override {
    CHECK(info);
    content::RenderFrameHost* current_target_frame =
        &extension_frame_host_->receivers_for_testing().CurrentTargetFrame();
    if (open_extension_mutator_ && frame_ == current_target_frame) {
      open_extension_mutator_.Run(*info);
    }

    GetForwardingInterface()->OpenChannelToExtension(
        std::move(info), channel_type, channel_name, port_id, std::move(port),
        std::move(port_host));
  }

  void WatchedPageChange(
      const std::vector<std::string>& css_selectors) override {
    std::vector<std::string> mutated_selectors = css_selectors;
    content::RenderFrameHost* current_target_frame =
        &extension_frame_host_->receivers_for_testing().CurrentTargetFrame();
    if (watched_page_change_mutator_ && frame_ == current_target_frame) {
      watched_page_change_mutator_.Run(mutated_selectors);
    }

    GetForwardingInterface()->WatchedPageChange(mutated_selectors);
  }

  const raw_ptr<content::RenderFrameHost> frame_ = nullptr;
  RequestMutator request_mutator_;
  RequestCallbackHook request_callback_hook_;
  OpenChannelToExtensionMutator open_extension_mutator_;
  WatchedPageChangeMutator watched_page_change_mutator_;
  const raw_ptr<ExtensionFrameHost> extension_frame_host_ = nullptr;
  const mojo::test::ScopedSwapImplForTesting<mojom::LocalFrameHost>
      scoped_swap_impl_;
};

class ServiceWorkerHostInterceptorForProcessDeath
    : public mojom::ServiceWorkerHostInterceptorForTesting {
 public:
  // We use `worker_id` to have an weak handle to the `ServiceWorkerHost`
  // which will be destroyed when this object mutates the IPC to cause
  // a bad message resulting in process death.
  explicit ServiceWorkerHostInterceptorForProcessDeath(
      const WorkerId& worker_id)
      : worker_id_(worker_id) {
    auto* host = extensions::ServiceWorkerHost::GetWorkerFor(worker_id_);
    CHECK(host);
    std::ignore = host->receiver_for_testing().SwapImplForTesting(this);
  }

  mojom::ServiceWorkerHost* GetForwardingInterface() override {
    // This should be non-null if this interface is still receiving events.
    auto* host = extensions::ServiceWorkerHost::GetWorkerFor(worker_id_);
    CHECK(host);
    return host;
  }

  void OpenChannelToExtension(
      mojom::ExternalConnectionInfoPtr info,
      mojom::ChannelType channel_type,
      const std::string& channel_name,
      const PortId& port_id,
      mojo::PendingAssociatedRemote<mojom::MessagePort> port,
      mojo::PendingAssociatedReceiver<mojom::MessagePortHost> port_host)
      override {
    CHECK(info);
    if (open_extension_mutator_) {
      open_extension_mutator_.Run(*info);
    }

    GetForwardingInterface()->OpenChannelToExtension(
        std::move(info), channel_type, channel_name, port_id, std::move(port),
        std::move(port_host));
  }

  using OpenChannelToExtensionMutator =
      base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info)>;
  void SetOpenChannelToExtensionMutator(
      OpenChannelToExtensionMutator open_extension_mutator) {
    open_extension_mutator_ = std::move(open_extension_mutator);
  }

 private:
  OpenChannelToExtensionMutator open_extension_mutator_;
  const WorkerId worker_id_;
};

// Waits for a kill of the given RenderProcessHost and returns the
// BadMessageReason that caused an //extensions-triggerred kill.
//
// Example usage:
//   RenderProcessHostBadIpcMessageWaiter kill_waiter(render_process_host);
//   ... test code that triggers a renderer kill ...
//   EXPECT_EQ(bad_message::EFD_BAD_MESSAGE_PROCESS, kill_waiter.Wait());
class RenderProcessHostBadIpcMessageWaiter {
 public:
  explicit RenderProcessHostBadIpcMessageWaiter(
      content::RenderProcessHost* render_process_host)
      : internal_waiter_(render_process_host,
                         "Stability.BadMessageTerminated.Extensions") {}

  // Waits until the renderer process exits.  Returns the bad message that made
  // //extensions kill the renderer.  `std::nullopt` is returned if the
  // renderer was killed outside of //extensions or exited normally.
  [[nodiscard]] std::optional<bad_message::BadMessageReason> Wait() {
    std::optional<int> internal_result = internal_waiter_.Wait();
    if (!internal_result.has_value())
      return std::nullopt;
    return static_cast<bad_message::BadMessageReason>(internal_result.value());
  }

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

 private:
  content::RenderProcessHostKillWaiter internal_waiter_;
};

// Test suite covering how mojo/IPC messages are verified after being received
// from a (potentially compromised) renderer process.
class ExtensionSecurityExploitBrowserTest : public ExtensionBrowserTest {
 public:
  ExtensionSecurityExploitBrowserTest() = default;

  void SetUpOnMainThread() override {
    ExtensionBrowserTest::SetUpOnMainThread();

    host_resolver()->AddRule("*", "127.0.0.1");
    content::SetupCrossSiteRedirector(embedded_test_server());
    embedded_test_server()->RegisterRequestHandler(
        base::BindRepeating(&HandleCspBlockRequest));
    ASSERT_TRUE(embedded_test_server()->Start());
  }

  // Asks the `extension_id` to inject `content_script` into `web_contents`.
  // Returns true if the content script execution started successfully.
  bool ExecuteProgrammaticContentScript(content::WebContents* web_contents,
                                        const ExtensionId& extension_id,
                                        const std::string& content_script) {
    DCHECK(web_contents);
    int tab_id = ExtensionTabUtil::GetTabId(web_contents);
    const char kScriptTemplate[] = R"(
        chrome.scripting.executeScript({
            target: {tabId: %d},
            injectImmediately: true,
            func: () => { %s }
        });
    )";
    std::string background_script =
        base::StringPrintf(kScriptTemplate, tab_id, content_script.c_str());
    return BackgroundScriptExecutor::ExecuteScriptAsync(profile(), extension_id,
                                                        background_script);
  }

  // (Asynchronously) executes the given `script` in a user script world in
  // `web_contents`, associated with the given `extension_id`
  void ExecuteUserScript(content::WebContents& web_contents,
                         const ExtensionId& extension_id,
                         const std::string& script) {
    browsertest_util::ExecuteUserScriptNoWait(&web_contents, extension_id,
                                              script);
  }

  // Allows messaging APIs for user scripts created by the given `extension`.
  void AllowUserScriptMessaging(const Extension& extension) {
    RendererStartupHelperFactory::GetForBrowserContext(profile())
        ->SetUserScriptWorldProperties(
            extension, mojom::UserScriptWorldInfo::New(
                           extension.id(), /*world_id=*/std::nullopt,
                           /*csp=*/std::nullopt,
                           /*enable_messaging=*/true));
  }

  const Extension& active_extension() { return *active_extension_; }
  const ExtensionId& active_extension_id() { return active_extension_->id(); }
  const Extension& spoofed_extension() { return *spoofed_extension_; }
  const ExtensionId& spoofed_extension_id() { return spoofed_extension_->id(); }

  // Installs an `active_extension` and a separate, but otherwise identical
  // `spoofed_extension` (the only difference will be the extension id).
  void InstallTestExtensions() {
    auto install_extension =
        [this](TestExtensionDir& dir,
               const char* extra_manifest_bits) -> const Extension* {
      const char kManifestTemplate[] = R"(
          {
            %s
            "name": "ScriptInjectionTrackerBrowserTest - Programmatic",
            "version": "1.0",
            "manifest_version": 3,
            "host_permissions": ["http://foo.com/*"],
            "permissions": [
                "scripting",
                "tabs",
                "nativeMessaging",
                "storage"
            ],
            "background": {"service_worker": "background_script.js"}
          } )";
      dir.WriteManifest(
          base::StringPrintf(kManifestTemplate, extra_manifest_bits));
      dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), "");
      dir.WriteFile(FILE_PATH_LITERAL("page.html"), "<p>page</p>");
      return LoadExtension(dir.UnpackedPath());
    };

    // The key below corresponds to the extension ID used by
    // ScopedTestNativeMessagingHost::kExtensionId.
    const char kActiveExtensionKey[] = R"(
        "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcBHwzDvyBQ6bDppkIs9MP4ksKqCMyXQ/A52JivHZKh4YO/9vJsT3oaYhSpDCE9RPocOEQvwsHsFReW2nUEc6OLLyoCFFxIb7KkLGsmfakkut/fFdNJYh0xOTbSN8YvLWcqph09XAY2Y/f0AL7vfO1cuCqtkMt8hFrBGWxDdf9CQIDAQAB",
    )";
    active_extension_ = install_extension(active_dir_, kActiveExtensionKey);
    spoofed_extension_ = install_extension(spoofed_dir_, "");
    ASSERT_TRUE(active_extension_);
    ASSERT_TRUE(spoofed_extension_);
    ASSERT_EQ(active_extension_id(),
              ScopedTestNativeMessagingHost::kExtensionId);
    ASSERT_NE(active_extension_id(), spoofed_extension_id());
  }

 private:
  TestExtensionDir active_dir_;
  TestExtensionDir spoofed_dir_;
  raw_ptr<const Extension, DanglingUntriaged> active_extension_ = nullptr;
  raw_ptr<const Extension, DanglingUntriaged> spoofed_extension_ = nullptr;
};

// Test suite for covering ExtensionHostMsg_OpenChannelToExtension IPC.
class OpenChannelToExtensionExploitTest
    : public ExtensionSecurityExploitBrowserTest {
 public:
  OpenChannelToExtensionExploitTest() = default;

  void SetUpOnMainThread() override {
    ExtensionSecurityExploitBrowserTest::SetUpOnMainThread();

    // Navigate to an arbitrary, mostly empty test page.  Make sure that a new
    // RenderProcessHost is created to make sure it is covered by the
    // `ipc_message_waiter_`.  (A WebUI -> http navigation should swap the
    // RenderProcessHost on all platforms.)
    GURL test_page_url =
        embedded_test_server()->GetURL("foo.com", "/title1.html");
    auto* web_contents = GetActiveWebContents();
    int old_process_id =
        web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID();
    EXPECT_TRUE(NavigateToURL(web_contents, GURL("chrome://version")));
    EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
    int new_process_id =
        web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID();
    EXPECT_NE(old_process_id, new_process_id);

    // Install the extensions (and potentially spawn new RenderProcessHosts)
    // only *after* the `ipc_message_waiter_` has been constructed.
    InstallTestExtensions();
  }

  // Helper method to set up and load an extension with a manifest-sandboxed
  // page `sandbox.html` containing `sandbox_content`.
  const Extension* LoadSandboxedExtension(const std::string& sandbox_content) {
    sandboxed_dir_.WriteManifest(R"({
      "name": "Sandboxed Spoofing Test",
      "version": "1.0",
      "manifest_version": 3,
      "sandbox": {
        "pages": ["sandbox.html"]
      }
    })");
    sandboxed_dir_.WriteFile(FILE_PATH_LITERAL("sandbox.html"),
                             sandbox_content);
    sandboxed_dir_.WriteFile(FILE_PATH_LITERAL("main.html"), "<p>main</p>");
    const Extension* extension = LoadExtension(sandboxed_dir_.UnpackedPath());
    EXPECT_TRUE(extension);
    return extension;
  }

  // Helper method to initiate an `OpenChannelToExtension` IPC from `frame`
  // using `source_url`.
  void OpenChannelToExtension(content::RenderFrameHost* frame,
                              const Extension* extension,
                              const GURL& source_url) {
    content::RenderProcessHost* process = frame->GetProcess();
    ChannelEndpoint source(profile(), process->GetDeprecatedID(),
                           PortContext::ForFrame(frame->GetRoutingID()));

    mojom::ExternalConnectionInfo info;
    info.target_id = extension->id();
    info.source_endpoint = MessagingEndpoint::ForExtension(extension->id());
    info.source_url = source_url;

    mojo::PendingAssociatedRemote<mojom::MessagePort> port;
    mojo::PendingAssociatedReceiver<mojom::MessagePortHost> port_host;
    std::ignore = port.InitWithNewEndpointAndPassReceiver();
    std::ignore = port_host.InitWithNewEndpointAndPassRemote();

    MessageService::Get(profile())->OpenChannelToExtension(
        source,
        PortId(base::UnguessableToken::Create(), 0, true,
               mojom::SerializationFormat::kJson),
        info, mojom::ChannelType::kSendMessage, "test_channel", std::move(port),
        std::move(port_host));
  }

 private:
  TestExtensionDir sandboxed_dir_;
};

IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromContentScript_BadExtensionIdInMessagingSource) {
  // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
  // from a content script of an `active_extension_id`.
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(ExecuteProgrammaticContentScript(
      web_contents, active_extension_id(),
      "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
  auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
      web_contents->GetPrimaryMainFrame());
  interceptor->SetOpenChannelToExtensionMutator(
      base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
        EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
                  info.source_endpoint.type);
        EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
        // Mutate the IPC payload.
        info.source_endpoint.extension_id = spoofed_extension_id();
      }));
  EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_CONTENT_SCRIPT,
            kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromContentScript_UnexpectedNativeAppType) {
  // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
  // from a content script of an `active_extension_id`.
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(ExecuteProgrammaticContentScript(
      web_contents, active_extension_id(),
      "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
  auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
      web_contents->GetPrimaryMainFrame());
  interceptor->SetOpenChannelToExtensionMutator(
      base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
        EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
                  info.source_endpoint.type);
        EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
        // Mutate the IPC payload.
        info.source_endpoint.type = MessagingEndpoint::Type::kNativeApp;
      }));
  EXPECT_EQ(bad_message::EMF_INVALID_CHANNEL_SOURCE_TYPE, kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromContentScript_UnexpectedExtensionType) {
  // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
  // from a content script of an `active_extension_id`.
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(ExecuteProgrammaticContentScript(
      web_contents, active_extension_id(),
      "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
  auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
      web_contents->GetPrimaryMainFrame());
  interceptor->SetOpenChannelToExtensionMutator(
      base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
        EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
                  info.source_endpoint.type);
        EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
        // Mutate the IPC payload.
        info.source_endpoint.type = MessagingEndpoint::Type::kExtension;
      }));
  EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_EXTENSION_SOURCE,
            kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromContentScript_NoExtensionIdForExtensionType) {
  // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
  // from a content script of an `active_extension_id`.
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(ExecuteProgrammaticContentScript(
      web_contents, active_extension_id(),
      "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
  auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
      web_contents->GetPrimaryMainFrame());
  interceptor->SetOpenChannelToExtensionMutator(
      base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
        EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
                  info.source_endpoint.type);
        EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);

        // Mutate the IPC payload.
        info.source_endpoint.type = MessagingEndpoint::Type::kExtension;
        info.source_endpoint.extension_id = std::nullopt;
      }));
  EXPECT_EQ(bad_message::EMF_NO_EXTENSION_ID_FOR_EXTENSION_SOURCE,
            kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromContentScript_BadSourceUrl_FromFrame) {
  // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
  // from a content script of an `active_extension_id`.
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(ExecuteProgrammaticContentScript(
      web_contents, active_extension_id(),
      "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
  auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
      web_contents->GetPrimaryMainFrame());
  interceptor->SetOpenChannelToExtensionMutator(
      base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
        EXPECT_EQ(MessagingEndpoint::Type::kContentScript,
                  info.source_endpoint.type);
        EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);

        // Mutate `source_url` in the IPC payload.
        GURL actual_url = GetActiveWebContents()
                              ->GetPrimaryMainFrame()
                              ->GetLastCommittedURL();
        ASSERT_EQ(actual_url, info.source_url);
        GURL spoofed_url =
            embedded_test_server()->GetURL("spoofed.com", "/title1.html");
        ASSERT_NE(spoofed_url.GetHost(), actual_url.GetHost());
        info.source_url = spoofed_url;
      }));
  EXPECT_EQ(bad_message::EMF_INVALID_SOURCE_URL, kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromServiceWorker_BadSourceUrl) {
  // Navigate the test tab to an extension page.
  // TODO(crbug.com/40874764): Remove this test step - it is only here as
  // a workaround for the bug that impacts how renderer kills are detected when
  // there are no frames in a given renderer process.
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  EXPECT_TRUE(NavigateToURL(GetActiveWebContents(), test_page_url));

  // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
  // from the service worker of an `active_extension_id`.
  ASSERT_TRUE(BackgroundScriptExecutor::ExecuteScriptAsync(
      profile(), active_extension_id(),
      "chrome.runtime.sendMessage({greeting: 'hello'});"));

  std::vector<WorkerId> service_workers =
      ProcessManager::Get(profile())->GetServiceWorkersForExtension(
          active_extension_id());
  ASSERT_EQ(1u, service_workers.size());

  content::RenderProcessHost* service_worker_process =
      content::RenderProcessHost::FromID(service_workers[0].render_process_id);
  ASSERT_TRUE(service_worker_process);
  RenderProcessHostBadIpcMessageWaiter kill_waiter(service_worker_process);
  auto interceptor =
      std::make_unique<ServiceWorkerHostInterceptorForProcessDeath>(
          service_workers[0]);
  interceptor->SetOpenChannelToExtensionMutator(
      base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
        EXPECT_EQ(MessagingEndpoint::Type::kExtension,
                  info.source_endpoint.type);
        EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
        EXPECT_EQ(info.source_url.GetHost(), active_extension_id());

        // Mutate `source_url` in the IPC payload.
        info.source_url =
            spoofed_extension().GetResourceURL("some_resource.html");
        EXPECT_EQ(info.source_url.GetHost(), spoofed_extension_id());
      }));

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

// Tests a sandboxed page sending a spoofed source URL.
// Regression test for https://crbug.com/511765713.
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromSandboxedPage_SpoofedSourceUrl) {
  const Extension* extension = LoadSandboxedExtension("<p>sandbox</p>");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(
      NavigateToURL(web_contents, extension->GetResourceURL("sandbox.html")));

  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
  OpenChannelToExtension(main_frame, extension,
                         extension->GetResourceURL("main.html"));
  EXPECT_EQ(bad_message::EMF_INVALID_MESSAGE_FROM_SANDBOXED_PROCESS,
            kill_waiter.Wait());
}

// Same as `FromSandboxedPage_SpoofedSourceUrl` above, but exercises the flow
// from an about:blank child.
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromSandboxedPage_AboutBlankChild_SpoofedSourceUrl) {
  const Extension* extension = LoadSandboxedExtension(
      "<html><body><iframe src=\"about:blank\"></body></html>");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(
      NavigateToURL(web_contents, extension->GetResourceURL("sandbox.html")));

  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
  // Get the about:blank child frame.
  content::RenderFrameHost* child_frame = content::ChildFrameAt(main_frame, 0);
  ASSERT_TRUE(child_frame);
  EXPECT_EQ(GURL(url::kAboutBlankURL), child_frame->GetLastCommittedURL());

  RenderProcessHostBadIpcMessageWaiter kill_waiter(child_frame->GetProcess());
  OpenChannelToExtension(child_frame, extension,
                         extension->GetResourceURL("main.html"));
  EXPECT_EQ(bad_message::EMF_INVALID_MESSAGE_FROM_SANDBOXED_PROCESS,
            kill_waiter.Wait());
}

// Same as above, but without a spoofed URL. This should *still* be disallowed
// because we don't allow connections from sandboxed pages.
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromSandboxedPage_NoSpoofedSourceUrl) {
  const Extension* extension = LoadSandboxedExtension("<p>sandbox</p>");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(
      NavigateToURL(web_contents, extension->GetResourceURL("sandbox.html")));

  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
  OpenChannelToExtension(main_frame, extension,
                         extension->GetResourceURL("sandbox.html"));
  EXPECT_EQ(bad_message::EMF_INVALID_MESSAGE_FROM_SANDBOXED_PROCESS,
            kill_waiter.Wait());
}

// Same as above, but with an empty source_url. This should *still* be
// disallowed because we don't allow connections from sandboxed pages, and empty
// source URLs should not bypass the check. Regression test for
// https://crbug.com/517063356.
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromSandboxedPage_EmptySourceUrl) {
  const Extension* extension = LoadSandboxedExtension("<p>sandbox</p>");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(
      NavigateToURL(web_contents, extension->GetResourceURL("sandbox.html")));

  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
  OpenChannelToExtension(main_frame, extension, GURL());
  EXPECT_EQ(bad_message::EMF_INVALID_MESSAGE_FROM_SANDBOXED_PROCESS,
            kill_waiter.Wait());
}

// Tests that a sandboxed frame attempting to connect to a tab via
// OpenChannelToTab is rejected and the renderer is terminated.
// Regression test for crbug.com/529438897.
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromSandboxedPage_OpenChannelToTab) {
  const Extension* extension = LoadSandboxedExtension("<p>sandbox</p>");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(
      NavigateToURL(web_contents, extension->GetResourceURL("sandbox.html")));

  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());

  int target_tab_id = 1;

  mojo::PendingAssociatedRemote<mojom::MessagePort> port;
  mojo::PendingAssociatedReceiver<mojom::MessagePortHost> port_host;
  std::ignore = port.InitWithNewEndpointAndPassReceiver();
  std::ignore = port_host.InitWithNewEndpointAndPassRemote();

  content::RenderProcessHost* process = main_frame->GetProcess();
  ChannelEndpoint source(profile(), process->GetDeprecatedID(),
                         PortContext::ForFrame(main_frame->GetRoutingID()));

  MessageService::Get(profile())->OpenChannelToTab(
      source,
      PortId(base::UnguessableToken::Create(), 0, true,
             mojom::SerializationFormat::kJson),
      target_tab_id, /*frame_id=*/-1, /*document_id=*/std::string(),
      mojom::ChannelType::kConnect, "test_channel", std::move(port),
      std::move(port_host));

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

// This is a regression test for https://crbug.com/40875650.
IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
                       SendMessageFromContentScriptInDataUrlFrame) {
  // Install a test extension that 1) declaratively injects content scripts into
  // all frames (including data: frames thanks to `match_origin_as_fallback`),
  // 2) sends a message from the content script to the extension, 3) echoes back
  // the `source_url` of the message sender via `chrome.test.sendMessage`.
  //
  // Note that `chrome.scripting.executeScript` is unable to inject content
  // scripts into data: frames (without additional permissions), because this
  // API doesn't have a knob equivalent to `match_origin_as_fallback`.  This is
  // the primary reason for using manifest-declared content scripts in this
  // test.
  TestExtensionDir dir;
  const char kManifestTemplate[] = R"(
      {
        "name": "source_url echo-er",
        "version": "1.0",
        "manifest_version": 3,
        "content_scripts": [{
          "all_frames": true,
          "match_about_blank": true,
          "match_origin_as_fallback": true,
          "matches": ["*://foo.com/*"],
          "js": ["content_script.js"]
        }],
        "background": {"service_worker": "background_script.js"}
      } )";
  dir.WriteManifest(kManifestTemplate);
  const char kBackgroundScript[] = R"(
      chrome.runtime.onMessage.addListener(
        function(request, sender, sendResponse) {
          chrome.test.sendMessage(sender.url);
        }
      );
  )";
  dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), kBackgroundScript);
  const char kContentScript[] = R"(
      chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});
  )";
  dir.WriteFile(FILE_PATH_LITERAL("content_script.js"), kContentScript);
  const Extension* extension = LoadExtension(dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Navigate to foo.com (covered by `content_scripts.matches` above).
  GURL http_url = embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, http_url));

  // Add a data: URL subframe and wait for its `source_url` to be echoed back.
  // The data: URL encodes `<p>foo</p>` HTML doc.
  const GURL kDataUrl("data:text/html;charset=utf-8;base64,PHA+Zm9vPC9wPg==");
  ExtensionTestMessageListener listener(kDataUrl.spec());
  const char kScriptTemplate[] = R"(
      new Promise(function (resolve, reject) {
          var iframe = document.createElement('iframe');
          iframe.src = $1;
          iframe.onload = () => {
              resolve("onload");
          };
          document.body.appendChild(iframe);
      });
  )";
  ASSERT_EQ("onload",
            content::EvalJs(web_contents,
                            content::JsReplace(kScriptTemplate, kDataUrl)));
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  // The main verification here (against https://crbug.com/40875650) is that the
  // renderer process wasn't terminated (because of incorrectly classifying IPC
  // payload as spoofed / illegitimately claiming to come on behalf of the data
  // URL).  This verification happens at the test suite level (e.g. see
  // `content::NoRendererCrashesAssertion` for more details).
}

IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
                       SpoofedExtensionId_ExtensionFunctionDispatcher) {
  InstallTestExtensions();

  // Navigate to a test page.
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));
  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();

  // Verify the test setup by checking if the non-intercepted `chrome.storage`
  // API call will succeed.
  {
    ExtensionTestMessageListener listener("Got chrome.storage response");
    ASSERT_TRUE(ExecuteProgrammaticContentScript(web_contents,
                                                 active_extension_id(), R"(
          chrome.storage.local.set(
              { test_key: 'test value'},
              () => {
                  chrome.test.sendMessage('Got chrome.storage response');
              }
          ); )"));
    ASSERT_TRUE(listener.WaitUntilSatisfied());
  }

  // Prepare to mutate the extension id in the IPC associated with the
  // `chrome.storage.local.set`.
  auto interceptor =
      std::make_unique<ExtensionFrameHostInterceptor>(main_frame);
  interceptor->SetRequestMutator(
      base::BindLambdaForTesting([this](mojom::RequestParams& request_params) {
        if (request_params.name != "storage.set")
          return;

        EXPECT_EQ(active_extension_id(), request_params.extension_id);
        request_params.extension_id = spoofed_extension_id();
      }));

  // Trigger an IPC associated with the `chrome.storage.local.set` API and
  // verify that the mutated/spoofed extension id is detected and leads to
  // terminating the misbehaving renderer process.
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
  ASSERT_TRUE(ExecuteProgrammaticContentScript(GetActiveWebContents(),
                                               active_extension_id(),
                                               R"(
          chrome.storage.local.set({ test_key: 'test value2'}, () => {}); )"));
  EXPECT_EQ(bad_message::EFD_INVALID_EXTENSION_ID_FOR_PROCESS,
            kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromUserScript_BadExtensionIdInMessagingSource) {
  AllowUserScriptMessaging(active_extension());

  // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
  // from a user script of an `active_extension_id`.
  auto* web_contents = GetActiveWebContents();
  ExecuteUserScript(
      *web_contents, active_extension_id(),
      "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});");

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
  auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
      web_contents->GetPrimaryMainFrame());
  interceptor->SetOpenChannelToExtensionMutator(
      base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
        EXPECT_EQ(MessagingEndpoint::Type::kUserScript,
                  info.source_endpoint.type);
        EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
        // Mutate the IPC payload.
        info.source_endpoint.extension_id.reset();
      }));
  EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_USER_SCRIPT,
            kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromUserScript_SpoofedExtensionIdInMessagingSource) {
  AllowUserScriptMessaging(active_extension());

  // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
  // from a user script of an `active_extension_id`.
  auto* web_contents = GetActiveWebContents();
  ExecuteUserScript(
      *web_contents, active_extension_id(),
      "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});");

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
  auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
      web_contents->GetPrimaryMainFrame());
  interceptor->SetOpenChannelToExtensionMutator(
      base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
        EXPECT_EQ(MessagingEndpoint::Type::kUserScript,
                  info.source_endpoint.type);
        EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
        // Mutate the IPC payload.
        info.source_endpoint.extension_id = spoofed_extension_id();
      }));
  EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_USER_SCRIPT,
            kill_waiter.Wait());
}

IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromUserScript_TargetingAnotherExtensionId) {
  AllowUserScriptMessaging(active_extension());

  // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
  // from a user script of an `active_extension_id`.
  auto* web_contents = GetActiveWebContents();
  ExecuteUserScript(
      *web_contents, active_extension_id(),
      "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});");

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
  auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
      web_contents->GetPrimaryMainFrame());
  interceptor->SetOpenChannelToExtensionMutator(
      base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) {
        EXPECT_EQ(MessagingEndpoint::Type::kUserScript,
                  info.source_endpoint.type);
        EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
        // Mutate the IPC payload.
        info.target_id = spoofed_extension_id();
      }));
  EXPECT_EQ(bad_message::EMF_INVALID_EXTERNAL_EXTENSION_ID_FOR_USER_SCRIPT,
            kill_waiter.Wait());
}

// Tests that a renderer sending an unrecognized selector (one the browser
// didn't ask to watch) is ignored and does not trigger actions.
IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
                       WatchedPageChange_InvalidSelector_Ignored) {
  // Install an extension that watches "div.target" and injects a script.
  TestExtensionDir dir;
  dir.WriteManifest(R"({
    "name": "Extension",
    "version": "1.0",
    "manifest_version": 2,
    "permissions": [ "declarativeContent", "http://foo.com/*" ],
    "background": {"scripts": ["background.js"]}
  })");
  dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
    chrome.runtime.onInstalled.addListener(() => {
      chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
        chrome.declarativeContent.onPageChanged.addRules([{
          conditions: [
            new chrome.declarativeContent.PageStateMatcher({
              css: ["div.target"]
            })
          ],
          actions: [
            new chrome.declarativeContent.RequestContentScript({
              js: ["script.js"]
            })
          ]
        }]);
      });
    });
  )");
  dir.WriteFile(FILE_PATH_LITERAL("script.js"), "console.log('injected');");

  const Extension* extension = LoadExtension(dir.UnpackedPath());
  ASSERT_TRUE(extension);

  // Navigate to a test page on foo.com where the extension has permission.
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));
  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();

  // Prepare to intercept WatchedPageChange.
  auto interceptor =
      std::make_unique<ExtensionFrameHostInterceptor>(main_frame);

  // Simulate a spoofed IPC with a selector the browser didn't ask to watch.
  std::vector<std::string> spoofed_selectors = {"div.spoofed"};
  interceptor->SimulateWatchedPageChange(spoofed_selectors);
  interceptor->FlushForTesting();

  // Verify that `ScriptInjectionTracker` doesn't show injection for the
  // extension.
  EXPECT_FALSE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
      *main_frame->GetProcess(), extension->id()));

  // Now simulate a valid selector to prove it *would* work.
  std::vector<std::string> valid_selectors = {"div.target"};
  interceptor->SimulateWatchedPageChange(valid_selectors);
  interceptor->FlushForTesting();

  // Verify that `ScriptInjectionTracker` does show injection now.
  EXPECT_TRUE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
      *main_frame->GetProcess(), extension->id()));
}

// Tests that a renderer cannot trigger content script injection on a page the
// extension has no permission for, even if it spoofs a valid CSS selector. This
// is a regression test for crbug.com/497025738 and exercises the host
// permission check in `RequestContentScript::InstructRenderProcessToInject`.
//
// High-level flow:
// 1. Install Extension A that watches "div.target" on google.com. This makes
//    the selector globally known to the browser's CSS condition tracker. This
//    allows a compromised renderer to pass the check we have for valid
//    selectors on the `extensions::mojom::LocalFrameHost::WatchedPageChange`
//    IPC.
// 2. Install Extension B that has a declarative rule to inject a script when
//    "div.target" matches, but Extension B only has host permission for
//    bar.com.
// 3. Navigate to foo.com (where Extension B has no permission).
// 4. Simulate a compromised renderer sending a
//    `extensions::mojom::LocalFrameHost::WatchedPageChange` IPC claiming that
//    "div.target" matched on foo.com.
// 5. Verify that the browser blocks the script injection of Extension B because
//    Extension B lacks permission for foo.com, despite the selector being
//    valid.
IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
                       WatchedPageChange_ValidSelector_PermissionDenied) {
  // 1. Install Extension A to make "div.target" globally known.
  TestExtensionDir dir_a;
  dir_a.WriteManifest(R"({
    "name": "Extension A",
    "version": "1.0",
    "manifest_version": 2,
    "permissions": [ "declarativeContent", "http://google.com/*" ],
    "background": {"scripts": ["background.js"]}
  })");
  dir_a.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
    var rule = {
      conditions: [
        new chrome.declarativeContent.PageStateMatcher({
          pageUrl: { hostEquals: 'google.com' },
          css: ['div.target']
        })
      ],
      actions: [ new chrome.declarativeContent.ShowAction() ]
    };
    chrome.runtime.onInstalled.addListener(function() {
      chrome.declarativeContent.onPageChanged.addRules([rule]);
    });
  )");
  const Extension* extension_a = LoadExtension(dir_a.UnpackedPath());
  ASSERT_TRUE(extension_a);

  // 2. Install Extension B (target). It only has permission for bar.com.
  TestExtensionDir dir_b;
  dir_b.WriteManifest(R"({
    "name": "Extension B",
    "version": "1.0",
    "manifest_version": 2,
    "permissions": [ "declarativeContent", "http://bar.com/*" ],
    "background": {"scripts": ["background.js"]}
  })");
  dir_b.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
    var rule = {
      conditions: [
        new chrome.declarativeContent.PageStateMatcher({
          css: ['div.target']
        })
      ],
      actions: [ new chrome.declarativeContent.RequestContentScript({
        js: ["content.js"]
      }) ]
    };
    chrome.runtime.onInstalled.addListener(function() {
      chrome.declarativeContent.onPageChanged.addRules([rule]);
    });
  )");
  dir_b.WriteFile(FILE_PATH_LITERAL("content.js"), R"(
    chrome.test.sendMessage('injected');
  )");
  const Extension* extension_b = LoadExtension(dir_b.UnpackedPath());
  ASSERT_TRUE(extension_b);

  // Navigate to foo.com (where Extension B has no permission).
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));
  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();

  // Prepare to intercept WatchedPageChange.
  auto interceptor =
      std::make_unique<ExtensionFrameHostInterceptor>(main_frame);

  // Simulate a spoofed IPC with a selector that is valid globally, bypassing
  // the IPC check.
  std::vector<std::string> spoofed_selectors = {"div.target"};
  interceptor->SimulateWatchedPageChange(spoofed_selectors);
  interceptor->FlushForTesting();

  // Verify that `ScriptInjectionTracker` doesn't show injection for Extension B
  // on foo.com because we've checked that the extension has host permission
  // before allowing the script injection.
  EXPECT_FALSE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
      *main_frame->GetProcess(), extension_b->id()));
}

class EventRouterExploitTest : public ExtensionSecurityExploitBrowserTest {
 public:
  EventRouterExploitTest() = default;

  void SetUpOnMainThread() override {
    ExtensionSecurityExploitBrowserTest::SetUpOnMainThread();
    // This installs two extensions (`active_extension()` and
    // `spoofed_extension()`) which are identical except for differing extension
    // IDs. In tests below we treat them so `active_extension()` should have
    // access to do things, while `spoofed_extension()` should not.
    InstallTestExtensions();
  }

  // Binds the EventRouter interface as though `process` were the renderer
  // sending the message.
  mojo::AssociatedRemote<mojom::EventRouter> BindEventRouterForProcess(
      content::RenderProcessHost* process) {
    mojo::AssociatedRemote<mojom::EventRouter> event_router;
    EventRouter::BindForRenderer(
        process->GetID(),
        event_router.BindNewEndpointAndPassDedicatedReceiver());
    return event_router;
  }

  // Creates a minimal service worker context payload for spoofed EventRouter
  // messages.
  mojom::ServiceWorkerContextPtr CreateServiceWorkerContext(
      const Extension& extension) {
    return mojom::ServiceWorkerContext::New(extension.url(), 1 /* version_id */,
                                            1 /* thread_id */);
  }

 protected:
  // Attempts to register a main thread webRequest listener for `event_name`
  // from a content script process and verifies that the browser terminates
  // the sender process with `expected_reason`.
  void TestAddWebRequestListenerKillsSender(
      const char* event_name,
      bad_message::BadMessageReason expected_reason);
};

// Tests that attempting to add a main thread listener for the process of a
// webpage by specifying a listener URL will work for a URL the process has
// access to (i.e. the actual URL of the page), but fail for one it does not
// have access.
IN_PROC_BROWSER_TEST_F(EventRouterExploitTest,
                       AddListenerForMainThread_BadListenerUrl) {
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();

  mojo::AssociatedRemote<mojom::EventRouter> event_router;
  EventRouter::BindForRenderer(
      main_frame_process->GetID(),
      event_router.BindNewEndpointAndPassDedicatedReceiver());
  EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
      "test.eventName"));

  {
    // Try to add a listener for the actual URL of the page. This should be
    // fine.
    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewListenerUrl(test_page_url);
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForMainThread(std::move(event_listener));
    event_router.FlushForTesting();

    EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
        "test.eventName"));
  }

  {
    // Try to add a listener for "chrome://settings". It should trigger a bad
    // message as the process doesn't have access to that origin.
    mojo::test::BadMessageObserver bad_message_observer;

    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewListenerUrl(GURL("chrome://settings"));
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForMainThread(std::move(event_listener));
    event_router.FlushForTesting();

    EXPECT_EQ(
        "Tried to manipulate an event listener for an unauthorized listener "
        "URL.",
        bad_message_observer.WaitForBadMessage());
  }
}

// Tests that attempting to add a main thread listener for the process of a
// webpage by specifying an extension ID will work for an extension that has
// injected a content script, but fail for one that has not.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    AddListenerForMainThread_SpoofedExtensionIdWithContentScript) {
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();

  // Simulate a content script from `active_extension()` being injected into the
  // process.
  ScriptInjectionTracker::AddExtensionThatRanContentScriptsInProcessForTesting(
      *main_frame_process, active_extension_id());

  mojo::AssociatedRemote<mojom::EventRouter> event_router;
  EventRouter::BindForRenderer(
      main_frame_process->GetID(),
      event_router.BindNewEndpointAndPassDedicatedReceiver());

  EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
      "test.eventName"));

  {
    // Try to add a listener for `active_extension_id()`. This should be fine
    // because the extension will appear to have run a content script in the
    // page.
    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewExtensionId(active_extension_id());
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForMainThread(std::move(event_listener));
    event_router.FlushForTesting();

    EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
        "test.eventName"));
  }

  {
    // Try to add a listener for `spoofed_extension_id()`. It should trigger a
    // bad message because the content script was injected for
    // `active_extension_id()`.
    mojo::test::BadMessageObserver bad_message_observer;

    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id());
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForMainThread(std::move(event_listener));
    event_router.FlushForTesting();

    EXPECT_EQ(
        "Tried to manipulate an event listener for an unauthorized extension "
        "ID.",
        bad_message_observer.WaitForBadMessage());
  }
}

// Tests that attempting to add a main thread listener for the process of a
// webpage by specifying an extension ID will work for an extension that has
// injected a user script, but fail for one that has not.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    AddListenerForMainThread_SpoofedExtensionIdWithUserScript) {
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();

  // Simulate a user script from `active_extension()` being injected into the
  // process.
  ScriptInjectionTracker::AddExtensionThatRanUserScriptsInProcessForTesting(
      *main_frame_process, active_extension_id());

  mojo::AssociatedRemote<mojom::EventRouter> event_router;
  EventRouter::BindForRenderer(
      main_frame_process->GetID(),
      event_router.BindNewEndpointAndPassDedicatedReceiver());

  EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
      "test.eventName"));

  {
    // Try to add a listener for `active_extension_id()`. This should be fine
    // because the extension will appear to have run a user script on the page.
    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewExtensionId(active_extension_id());
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForMainThread(std::move(event_listener));
    event_router.FlushForTesting();

    EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
        "test.eventName"));
  }

  {
    // Try to add a listener for `spoofed_extension_id()`. It should trigger a
    // bad message because the user script was injected for
    // `active_extension_id()`.
    mojo::test::BadMessageObserver bad_message_observer;
    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id());
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForMainThread(std::move(event_listener));
    event_router.FlushForTesting();

    EXPECT_EQ(
        "Tried to manipulate an event listener for an unauthorized extension "
        "ID.",
        bad_message_observer.WaitForBadMessage());
  }
}

// Tests that attempting to add a service worker listener to the process of a
// webpage and specifying an extension ID as the `listener_owners` will fail.
IN_PROC_BROWSER_TEST_F(EventRouterExploitTest,
                       AddListenerForServiceWorker_BadExtensionIdForWebPage) {
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();

  mojo::AssociatedRemote<mojom::EventRouter> event_router;
  EventRouter::BindForRenderer(
      main_frame_process->GetID(),
      event_router.BindNewEndpointAndPassDedicatedReceiver());

  mojo::test::BadMessageObserver bad_message_observer;

  auto event_listener = mojom::EventListener::New();
  event_listener->listener_owner =
      mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id());
  event_listener->event_name = "test.eventName";

  event_router->AddListenerForServiceWorker(
      std::move(event_listener),
      CreateServiceWorkerContext(spoofed_extension()));
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for an unauthorized extension ID.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that attempting to add a service worker listener to the process of an
// extension page works for that extension, but specifying an extension ID of
// another extension will fail.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    AddListenerForServiceWorker_BadExtensionIdForExtensionPage) {
  // Navigate to an extension page, so the associated process is authorized for
  // `active_extension_id()`.
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();

  mojo::AssociatedRemote<mojom::EventRouter> event_router;
  EventRouter::BindForRenderer(
      main_frame_process->GetID(),
      event_router.BindNewEndpointAndPassDedicatedReceiver());

  EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
      "test.eventName"));

  {
    // Try adding a listener for the extension that owns the page. This should
    // be fine.
    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewExtensionId(active_extension_id());
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForServiceWorker(
        std::move(event_listener),
        CreateServiceWorkerContext(active_extension()));
    event_router.FlushForTesting();

    EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
        "test.eventName"));
  }

  {
    // Try adding a listener but specifying a `listener_owner` for a different
    // extension ID. This should trigger a bad message.
    mojo::test::BadMessageObserver bad_message_observer;

    auto event_listener = mojom::EventListener::New();
    // Pass the `listener_owner` as the ID of the `spoofed_extension()`.
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id());
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForServiceWorker(
        std::move(event_listener),
        CreateServiceWorkerContext(spoofed_extension()));
    event_router.FlushForTesting();

    EXPECT_EQ(
        "Tried to manipulate an event listener for an unauthorized extension "
        "ID.",
        bad_message_observer.WaitForBadMessage());
  }
}

// Tests that attempting to add a service worker listener to the process of an
// extension page will work if the `scope_url` is for the extension that owns
// the page, but fail if the `scope_url` is for an extension it does not have
// access to.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    AddListenerForServiceWorker_BadScopeUrlForExtensionPage) {
  // Navigate to an extension page, so the associated process is authorized for
  // `active_extension_id()`.
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();

  mojo::AssociatedRemote<mojom::EventRouter> event_router;
  EventRouter::BindForRenderer(
      main_frame_process->GetID(),
      event_router.BindNewEndpointAndPassDedicatedReceiver());

  EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
      "test.eventName"));

  {
    // Try adding a listener for the extension that owns the page. This should
    // be fine.
    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewExtensionId(active_extension_id());
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForServiceWorker(
        std::move(event_listener),
        CreateServiceWorkerContext(active_extension()));
    event_router.FlushForTesting();

    EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
        "test.eventName"));
  }

  {
    // Try to add a listener with a `scope_url` belonging to another extension.
    // It should trigger a bad message.
    mojo::test::BadMessageObserver bad_message_observer;

    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewExtensionId(active_extension_id());
    event_listener->event_name = "test.eventName";

    // Pass the `scope_url` as the base URL of the `spoofed_extension()`.
    event_router->AddListenerForServiceWorker(
        std::move(event_listener),
        CreateServiceWorkerContext(spoofed_extension()));
    event_router.FlushForTesting();

    EXPECT_EQ(
        "Tried to manipulate an event listener for an unauthorized worker "
        "scope URL.",
        bad_message_observer.WaitForBadMessage());
  }
}

// Tests that a service worker listener message with an invalid worker scope
// URL is rejected as a bad message.
IN_PROC_BROWSER_TEST_F(EventRouterExploitTest,
                       AddListenerForServiceWorker_InvalidScopeUrl) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  mojo::test::BadMessageObserver bad_message_observer;
  auto event_listener = mojom::EventListener::New();
  event_listener->listener_owner =
      mojom::EventListenerOwner::NewExtensionId(active_extension_id());
  event_listener->event_name = "test.eventName";
  auto service_worker_context = mojom::ServiceWorkerContext::New(
      GURL("not a url"), /*version_id=*/1, /*thread_id=*/1);

  event_router->AddListenerForServiceWorker(std::move(event_listener),
                                            std::move(service_worker_context));
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for a service worker without a "
      "valid worker scope URL.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that a service worker listener removal with an invalid worker scope
// URL is rejected as a bad message.
IN_PROC_BROWSER_TEST_F(EventRouterExploitTest,
                       RemoveListenerForServiceWorker_InvalidScopeUrl) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  {
    auto event_listener = mojom::EventListener::New();
    event_listener->listener_owner =
        mojom::EventListenerOwner::NewExtensionId(active_extension_id());
    event_listener->event_name = "test.eventName";

    event_router->AddListenerForServiceWorker(
        std::move(event_listener),
        CreateServiceWorkerContext(active_extension()));
    event_router.FlushForTesting();
  }

  mojo::test::BadMessageObserver bad_message_observer;
  auto event_listener = mojom::EventListener::New();
  event_listener->listener_owner =
      mojom::EventListenerOwner::NewExtensionId(active_extension_id());
  event_listener->event_name = "test.eventName";
  auto service_worker_context = mojom::ServiceWorkerContext::New(
      GURL("not a url"), /*version_id=*/1, /*thread_id=*/1);

  event_router->RemoveListenerForServiceWorker(
      std::move(event_listener), std::move(service_worker_context));
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for a service worker without a "
      "valid worker scope URL.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that an extension process cannot add a filtered main thread lazy
// listener on behalf of another extension.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    AddFilteredListenerForMainThread_BadExtensionIdForExtensionPage) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  constexpr char kEventName[] = "test.eventName";

  mojo::test::BadMessageObserver bad_message_observer;
  event_router->AddFilteredListenerForMainThread(
      mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id()),
      kEventName, base::DictValue(), /*add_lazy_listener=*/true);
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for an unauthorized extension ID.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that an extension process cannot remove a filtered main thread lazy
// listener on behalf of another extension.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    RemoveFilteredListenerForMainThread_BadExtensionIdForExtensionPage) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  EventRouter::Get(profile())->AddFilteredEventListener(
      "test.eventName", main_frame_process,
      mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id()),
      nullptr, base::DictValue(), /*add_lazy_listener=*/true);

  mojo::test::BadMessageObserver bad_message_observer;
  event_router->RemoveFilteredListenerForMainThread(
      mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id()),
      "test.eventName", base::DictValue(), /*remove_lazy_listener=*/true);
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for an unauthorized extension ID.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that an extension process cannot add a filtered service worker lazy
// listener on behalf of another extension.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    AddFilteredListenerForServiceWorker_BadExtensionIdForExtensionPage) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  constexpr char kEventName[] = "test.eventName";

  mojo::test::BadMessageObserver bad_message_observer;
  event_router->AddFilteredListenerForServiceWorker(
      spoofed_extension_id(), kEventName,
      CreateServiceWorkerContext(spoofed_extension()), base::DictValue(),
      /*add_lazy_listener=*/true);
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for an unauthorized extension ID.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that an extension process cannot remove a filtered service worker lazy
// listener on behalf of another extension.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    RemoveFilteredListenerForServiceWorker_BadExtensionIdForExtensionPage) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  event_router->AddFilteredListenerForServiceWorker(
      active_extension_id(), "test.eventName",
      CreateServiceWorkerContext(active_extension()), base::DictValue(),
      /*add_lazy_listener=*/true);
  event_router.FlushForTesting();

  mojo::test::BadMessageObserver bad_message_observer;
  event_router->RemoveFilteredListenerForServiceWorker(
      spoofed_extension_id(), "test.eventName",
      CreateServiceWorkerContext(spoofed_extension()), base::DictValue(),
      /*remove_lazy_listener=*/true);
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for an unauthorized extension ID.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that an extension process cannot add a main thread lazy listener on
// behalf of another extension.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    AddLazyListenerForMainThread_BadExtensionIdForExtensionPage) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  mojo::test::BadMessageObserver bad_message_observer;
  event_router->AddLazyListenerForMainThread(spoofed_extension_id(),
                                             "test.eventName");
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for an unauthorized extension ID.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that an extension process cannot remove a main thread lazy listener on
// behalf of another extension.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    RemoveLazyListenerForMainThread_BadExtensionIdForExtensionPage) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  EventRouter::Get(profile())->listeners().LoadUnfilteredLazyListeners(
      profile(), spoofed_extension_id(), /*is_for_service_worker=*/false,
      {"test.eventName"});

  mojo::test::BadMessageObserver bad_message_observer;
  event_router->RemoveLazyListenerForMainThread(spoofed_extension_id(),
                                                "test.eventName");
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for an unauthorized extension ID.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that an extension process cannot add a service worker lazy listener on
// behalf of another extension.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    AddLazyListenerForServiceWorker_BadExtensionIdForExtensionPage) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  mojo::test::BadMessageObserver bad_message_observer;
  event_router->AddLazyListenerForServiceWorker(
      spoofed_extension_id(), spoofed_extension().url(), "test.eventName");
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for an unauthorized extension ID.",
      bad_message_observer.WaitForBadMessage());
}

// Tests that an extension process cannot remove a service worker lazy listener
// on behalf of another extension.
IN_PROC_BROWSER_TEST_F(
    EventRouterExploitTest,
    RemoveLazyListenerForServiceWorker_BadExtensionIdForExtensionPage) {
  GURL test_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  event_router->AddLazyListenerForServiceWorker(
      active_extension_id(), active_extension().url(), "test.eventName");
  event_router.FlushForTesting();

  mojo::test::BadMessageObserver bad_message_observer;
  event_router->RemoveLazyListenerForServiceWorker(
      spoofed_extension_id(), spoofed_extension().url(), "test.eventName");
  event_router.FlushForTesting();

  EXPECT_EQ(
      "Tried to manipulate an event listener for an unauthorized extension ID.",
      bad_message_observer.WaitForBadMessage());
}

void EventRouterExploitTest::TestAddWebRequestListenerKillsSender(
    const char* event_name,
    bad_message::BadMessageReason expected_reason) {
  // Navigate to a web page where active_extension() has host permissions.
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));

  // Execute a content script so ScriptInjectionTracker authorizes this process
  // for main thread listeners.
  ASSERT_TRUE(ExecuteProgrammaticContentScript(
      web_contents, active_extension_id(), "document.body.bgColor = 'red';"));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  mojo::AssociatedRemote<mojom::EventRouter> event_router =
      BindEventRouterForProcess(main_frame_process);

  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);

  base::DictValue filter =
      base::test::ParseJsonDict(R"({"urls": ["http://*/*"]})");

  event_router->AddFilteredListenerForMainThread(
      mojom::EventListenerOwner::NewExtensionId(active_extension_id()),
      event_name, std::move(filter),
      /*add_lazy_listener=*/false);

  EXPECT_EQ(expected_reason, kill_waiter.Wait());
}

// Test fixture forcing legacy dispatch (sub-event registrations).
class EventRouterLegacyDispatchExploitTest : public EventRouterExploitTest {
 public:
  EventRouterLegacyDispatchExploitTest() {
    feature_list_.InitAndDisableFeature(
        extensions_features::kWebRequestPerContextEventDispatch);
  }

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

// Test fixture forcing per-context dispatch (parent event registrations).
class EventRouterPerContextDispatchExploitTest : public EventRouterExploitTest {
 public:
  EventRouterPerContextDispatchExploitTest() {
    feature_list_.InitAndEnableFeature(
        extensions_features::kWebRequestPerContextEventDispatch);
  }

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

// Registering a parent event name under legacy dispatch should terminate
// the renderer.
IN_PROC_BROWSER_TEST_F(
    EventRouterLegacyDispatchExploitTest,
    AddFilteredListenerForMainThread_WebRequestProtocolMismatch) {
  TestAddWebRequestListenerKillsSender(
      "webRequest.onBeforeRequest",
      bad_message::WRA_INVALID_LISTENER_REGISTRATION);
}

// Registering a sub-event name under per-context dispatch should terminate
// the renderer.
IN_PROC_BROWSER_TEST_F(
    EventRouterPerContextDispatchExploitTest,
    AddFilteredListenerForMainThread_WebRequestProtocolMismatch) {
  TestAddWebRequestListenerKillsSender(
      "webRequest.onBeforeRequest/g1",
      bad_message::WRA_INVALID_LISTENER_REGISTRATION);
}

// Tests that a web page process that ran a content script cannot register an
// active webRequest listener for that extension. Each test uses its
// protocol's event name, so registration reaches the process authorization
// check.
IN_PROC_BROWSER_TEST_F(
    EventRouterLegacyDispatchExploitTest,
    AddFilteredListenerForMainThread_WebRequestFromContentScriptProcess) {
  TestAddWebRequestListenerKillsSender(
      "webRequest.onBeforeRequest/g1",
      bad_message::WRA_INVALID_EXTENSION_ID_FOR_PROCESS);
}

IN_PROC_BROWSER_TEST_F(
    EventRouterPerContextDispatchExploitTest,
    AddFilteredListenerForMainThread_WebRequestFromContentScriptProcess) {
  TestAddWebRequestListenerKillsSender(
      "webRequest.onBeforeRequest",
      bad_message::WRA_INVALID_EXTENSION_ID_FOR_PROCESS);
}

// Verifies that error pages cannot use extension APIs that might be available
// to their committed URLs. Regression test for crbug.com/516797143.
IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
                       ErrorPagesAreBlockedFromExtensionAPIs) {
  const GURL webstore_url("https://chromewebstore.google.com/");

  // Load a page that will include a subframe with a committed URL to the
  // webstore, but will be blocked by CSP.
  GURL test_page_url =
      embedded_test_server()->GetURL("a.com", "/csp_block.html");
  auto* web_contents = GetActiveWebContents();

  content::TestNavigationObserver navigation_observer(web_contents);
  // The navigation includes a frame being blocked (and thus committing to an
  // error page), so we ignore the result of NavigateToURL().
  std::ignore = NavigateToURL(web_contents, test_page_url);
  navigation_observer.Wait();

  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
  EXPECT_EQ(main_frame->GetLastCommittedURL(), test_page_url);
  content::RenderFrameHost* subframe = content::ChildFrameAt(main_frame, 0);
  ASSERT_TRUE(subframe);

  EXPECT_TRUE(subframe->IsErrorDocument());
  EXPECT_EQ(subframe->GetLastCommittedURL(), webstore_url);

  auto* extension_frame_host =
      ExtensionWebContentsObserver::GetForWebContents(web_contents)
          ->extension_frame_host_for_testing();

  extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
      subframe);

  // Impersonate a request for an API call that the webstore is allowed to make.
  mojom::RequestParamsPtr params = mojom::RequestParams::New();
  params->name = "management.getAll";
  params->arguments = base::ListValue();
  params->extension_id = "";
  params->source_url = webstore_url;
  params->context_type = mojom::ContextType::kWebPage;
  params->request_id = 1;
  params->has_callback = true;
  params->user_gesture = false;
  params->worker_thread_id = kMainThreadId;
  params->service_worker_version_id =
      blink::mojom::kInvalidServiceWorkerVersionId;

  base::RunLoop run_loop;
  bool api_success = true;
  std::string api_error;

  extension_frame_host->Request(
      std::move(params),
      base::BindOnce(
          [](base::OnceClosure quit_closure, bool* success_out,
             std::string* error_out, bool success,
             base::ListValue response_wrapper, const std::string& error,
             mojom::ExtraResponseDataPtr extra_data) {
            *success_out = success;
            *error_out = error;
            std::move(quit_closure).Run();
          },
          run_loop.QuitClosure(), &api_success, &api_error));

  run_loop.Run();

  // Clean up the target frame override.
  extension_frame_host->receivers_for_testing().SetCurrentTargetFrameForTesting(
      nullptr);

  // We expect the request to have been blocked.
  EXPECT_FALSE(api_success);
  EXPECT_EQ(api_error, "Cannot call extension APIs from error pages.");
}

// Verifies that a renderer cannot send messages to an `externally_connectable`
// extension by using a subframe error page.
// The test navigates to a page with CSP that blocks a subframe, causing it to
// commit as an error document. It then simulates a renderer attempting to
// open a channel to the extension on behalf of the subframe.
// We expect the connection to fail because the subframe is an error document.
// Regression test for crbug.com/516813317.
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
                       FromErrorPageSubframe_ExternallyConnectable) {
  // Install an extension connectable only from `chromewebstore.google.com`.
  TestExtensionDir extension_dir;
  extension_dir.WriteManifest(R"({
    "name": "Connectable Extension",
    "version": "1.0",
    "manifest_version": 3,
    "externally_connectable": {
      "matches": ["https://chromewebstore.google.com/*"]
    },
    "background": {"service_worker": "background.js"}
  })");
  extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
    chrome.runtime.onConnectExternal.addListener((port) => {
      chrome.test.sendMessage("connected");
      port.postMessage("hello");
    });
  )");
  const Extension* extension = LoadExtension(extension_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  ExtensionTestMessageListener connect_listener("connected");

  // Navigate to a.com with a CSP blocking a subframe loading
  // https://chromewebstore.google.com/.
  const GURL victim_url("https://chromewebstore.google.com/");
  GURL test_page_url =
      embedded_test_server()->GetURL("a.com", "/csp_block.html");

  auto* web_contents = GetActiveWebContents();
  // NavigateToURL() only waits for the main frame navigation to commit. We
  // must use `TestNavigationObserver` to wait for the subframe navigation to
  // finish committing as an error document.
  content::TestNavigationObserver navigation_observer(web_contents);
  std::ignore = NavigateToURL(web_contents, test_page_url);
  {
    SCOPED_TRACE("Waiting for CSP block navigation to complete");
    navigation_observer.Wait();
  }

  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
  content::RenderFrameHost* subframe =
      content::ChildFrameAt(main_frame, /*index=*/0);
  ASSERT_TRUE(subframe);

  // Verify the subframe commits as an error document in the parent's process
  // with `GetLastCommittedURL()` returning
  // "https://chromewebstore.google.com/".
  EXPECT_TRUE(subframe->IsErrorDocument());
  EXPECT_EQ(subframe->GetLastCommittedURL(), victim_url);
  EXPECT_EQ(main_frame->GetProcess(), subframe->GetProcess());

  // Simulate the renderer opening a channel to the extension on behalf of the
  // subframe using `MessageService::OpenChannelToExtension`.
  content::RenderProcessHost* process = subframe->GetProcess();
  ChannelEndpoint source(profile(), process->GetDeprecatedID(),
                         PortContext::ForFrame(subframe->GetRoutingID()));

  // Set up the message channel endpoints.
  PortId source_port_id(base::UnguessableToken::Create(), /*port_number=*/0,
                        /*is_opener=*/true, mojom::SerializationFormat::kJson);

  mojom::ExternalConnectionInfo info;
  info.target_id = extension->id();
  info.source_endpoint = MessagingEndpoint::ForWebPage();
  info.source_url = test_page_url;

  // Bind the test-side port receiver to capture the connection response.
  base::RunLoop run_loop;
  TestMessagePort test_port(run_loop.QuitClosure());
  mojo::AssociatedReceiver<mojom::MessagePort> port_receiver(&test_port);

  // Create endpoints. Use `BindNewEndpointAndPassDedicatedRemote()` to avoid
  // Mojo association crashes when simulating the IPC directly.
  mojo::PendingAssociatedRemote<mojom::MessagePort> port_remote =
      port_receiver.BindNewEndpointAndPassDedicatedRemote();

  mojo::PendingAssociatedReceiver<mojom::MessagePortHost> port_host_receiver;
  auto port_host_remote = port_host_receiver.InitWithNewEndpointAndPassRemote();

  // Simulate the renderer calling OpenChannelToExtension directly.
  MessageService::Get(profile())->OpenChannelToExtension(
      source, source_port_id, info, mojom::ChannelType::kConnect,
      "test_channel", std::move(port_remote), std::move(port_host_receiver));

  // We expect the connection to fail since error documents shouldn't be able
  // to send messages.
  {
    SCOPED_TRACE("Waiting for port disconnection");
    run_loop.Run();
  }
  EXPECT_FALSE(connect_listener.was_satisfied());
  EXPECT_FALSE(test_port.got_message());
  EXPECT_EQ("Could not establish connection. Receiving end does not exist.",
            test_port.disconnect_error());
}

// Verifies that we properly validate content script contexts in extension API
// requests.
IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
                       FromUserScriptProcess_CannotClaimContentScriptContext) {
  InstallTestExtensions();
  AllowUserScriptMessaging(active_extension());

  // Navigate to a test page.
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));
  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();

  // Execute a user script to ensure the process is registered as running a
  // user script.
  browsertest_util::ExecuteUserScript(web_contents, active_extension_id(),
                                      "console.log('user script ran');");

  // Verify the process is registered as running user script, but NOT content
  // script.
  content::RenderProcessHost* process = main_frame->GetProcess();
  EXPECT_TRUE(ScriptInjectionTracker::DidProcessRunUserScriptFromExtension(
      *process, active_extension_id()));
  EXPECT_FALSE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
      *process, active_extension_id()));

  auto interceptor =
      std::make_unique<ExtensionFrameHostInterceptor>(main_frame);

  // Mutate the request from the user script to claim it is a content script.
  interceptor->SetRequestMutator(
      base::BindLambdaForTesting([](mojom::RequestParams& request_params) {
        if (request_params.name != "test.sendMessage") {
          return;
        }
        EXPECT_EQ(mojom::ContextType::kUserScript, request_params.context_type);
        request_params.context_type = mojom::ContextType::kContentScript;
      }));

  base::RunLoop run_loop;
  interceptor->SetRequestCallbackHook(base::BindLambdaForTesting(
      [&run_loop](bool success, const std::string& error) {
        EXPECT_FALSE(success);
        EXPECT_EQ("Invalid context type provided.", error);
        run_loop.Quit();
      }));

  // Trigger the API call from the user script.
  browsertest_util::ExecuteUserScript(web_contents, active_extension_id(),
                                      "chrome.test.sendMessage('trigger');");

  run_loop.Run();
}

// Tests that attempting to bind the automation registry interface from an
// unauthorized process (such as a web page) results in the process being
// terminated.
IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
                       Automation_BindAutomationFromWebPage) {
  GURL test_page_url =
      embedded_test_server()->GetURL("foo.com", "/title1.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));

  content::RenderProcessHost* main_frame_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();

  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);

  mojo::AssociatedRemote<mojom::RendererAutomationRegistry> registry;
  AutomationEventRouter::BindForRenderer(
      main_frame_process->GetID(),
      registry.BindNewEndpointAndPassDedicatedReceiver());

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

// Tests that attempting to bind the automation registry interface from an
// extension process that does not have the "automation" manifest permission
// results in the process being terminated.
IN_PROC_BROWSER_TEST_F(
    ExtensionSecurityExploitBrowserTest,
    Automation_BindAutomationFromExtensionWithoutPermission) {
  InstallTestExtensions();
  GURL extension_page_url = active_extension().GetResourceURL("page.html");
  auto* web_contents = GetActiveWebContents();
  ASSERT_TRUE(NavigateToURL(web_contents, extension_page_url));

  content::RenderProcessHost* extension_process =
      web_contents->GetPrimaryMainFrame()->GetProcess();

  RenderProcessHostBadIpcMessageWaiter kill_waiter(extension_process);

  mojo::AssociatedRemote<mojom::RendererAutomationRegistry> registry;
  AutomationEventRouter::BindForRenderer(
      extension_process->GetID(),
      registry.BindNewEndpointAndPassDedicatedReceiver());

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

}  // namespace extensions
