blob: 3eeef529fa9688be22ec663577ef39c3c7934257 [file] [log] [blame]
// 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 "build/build_config.h"
#include "chrome/browser/chrome_content_browser_client.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/browser_message_filter.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 "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/extension_frame_host.h"
#include "extensions/browser/extension_web_contents_observer.h"
#include "extensions/browser/process_manager.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_messages.h"
#include "extensions/common/mojom/frame.mojom-test-utils.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/test_extension_dir.h"
#include "ipc/ipc_security_test_util.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-forward.h"
#include "url/gurl.h"
#if !(BUILDFLAG(IS_FUCHSIA))
#include "chrome/browser/extensions/api/messaging/native_messaging_test_util.h"
#endif
namespace extensions {
// ExtensionFrameHostInterceptor is a helper for:
// - Intercepting extensions::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.
using RequestMutator =
base::RepeatingCallback<void(mojom::RequestParams& request_params)>;
ExtensionFrameHostInterceptor(content::RenderFrameHost* frame,
RequestMutator request_mutator)
: frame_(frame),
request_mutator_(std::move(request_mutator)),
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;
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().GetCurrentTargetFrame();
if (frame_ == current_target_frame)
request_mutator_.Run(*params);
GetForwardingInterface()->Request(std::move(params), std::move(callback));
}
const raw_ptr<content::RenderFrameHost> frame_ = nullptr;
const RequestMutator request_mutator_;
const raw_ptr<ExtensionFrameHost> extension_frame_host_ = nullptr;
const mojo::test::ScopedSwapImplForTesting<
content::RenderFrameHostReceiverSet<mojom::LocalFrameHost>>
scoped_swap_impl_;
};
// 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. `absl::nullopt` is returned if the
// renderer was killed outside of //extensions or exited normally.
[[nodiscard]] absl::optional<bad_message::BadMessageReason> Wait() {
absl::optional<int> internal_result = internal_waiter_.Wait();
if (!internal_result.has_value())
return absl::nullopt;
return static_cast<bad_message::BadMessageReason>(internal_result.value());
}
RenderProcessHostBadIpcMessageWaiter(
const RenderProcessHostBadIpcMessageWaiter&) = delete;
RenderProcessHostBadIpcMessageWaiter& operator=(
const RenderProcessHostBadIpcMessageWaiter&) = delete;
private:
content::RenderProcessHostKillWaiter internal_waiter_;
};
// Intercepts legacy IPC messages of type TMessageType. Only meant for
// intercepting message once.
template <typename TMessageType>
class ExtensionMessageWaiter {
public:
using MessageParamType = typename TMessageType::Param;
ExtensionMessageWaiter() : weak_ptr_factory_(this) {
test_content_browser_client_ = std::make_unique<TestContentBrowserClient>(
weak_ptr_factory_.GetWeakPtr());
}
~ExtensionMessageWaiter() = default;
ExtensionMessageWaiter(const ExtensionMessageWaiter&) = delete;
ExtensionMessageWaiter& operator=(const ExtensionMessageWaiter&) = delete;
using IpcMatcher =
base::RepeatingCallback<bool(int captured_render_process_id,
const MessageParamType& param)>;
void SetIpcMatcher(IpcMatcher ipc_matcher) { ipc_matcher_ = ipc_matcher; }
MessageParamType WaitForMessage(int* process_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(process_id);
// Wait for `captured_message_param_` in a nested message loop if needed
// (i.e. if CaptureMessageParam hasn't called `run_loop_.Quit()` yet).
run_loop_.Run();
// Return the `captured_message_param_` and `captured_render_process_id_`.
DCHECK(captured_message_param_.has_value());
DCHECK_NE(-1, captured_render_process_id_);
*process_id = captured_render_process_id_;
return *captured_message_param_;
}
private:
void CaptureMessageParam(MessageParamType param, int render_process_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Do nothing if we already captured a matching IPC.
if (captured_message_param_.has_value())
return;
// Do nothing if the IPC doesn't match.
if (ipc_matcher_.is_null() || !ipc_matcher_.Run(render_process_id, param))
return;
// Capture the IPC payload.
captured_message_param_.emplace(std::move(param));
captured_render_process_id_ = render_process_id;
// Once we have `captured_message_param_` there is no need to inject
// TestFilter into additional RenderProcessHosts.
test_content_browser_client_.reset();
// Wake up WaitForMessage if necessary.
run_loop_.Quit();
}
// A BrowserMessageFilter implementation that posts a copy of the payload of
// the TMessageType into ExtensionMessageWaiter::CaptureMessageParam, but
// otherwise leaves all the message unhandled (i.e. allows other filters to
// process the message).
class TestFilter : public content::BrowserMessageFilter {
public:
TestFilter(base::WeakPtr<ExtensionMessageWaiter> ipc_message_waiter,
int render_process_id)
: content::BrowserMessageFilter(ExtensionMsgStart),
ipc_message_waiter_(ipc_message_waiter),
render_process_id_(render_process_id) {}
private:
// content::BrowserMessageFilter overrides:
bool OnMessageReceived(const IPC::Message& message) override {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
MessageParamType param;
if (message.type() == TMessageType::ID &&
TMessageType::Read(&message, &param)) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&ExtensionMessageWaiter::CaptureMessageParam,
ipc_message_waiter_, std::move(param),
render_process_id_));
}
return false; // Not handled - let another filter handle the message.
}
~TestFilter() override = default;
base::WeakPtr<ExtensionMessageWaiter> ipc_message_waiter_;
const int render_process_id_;
};
// A content::ContentBrowserClient that injects a TestFilter (as the very
// first filter) into all new RenderProcessHost objects, but otherwise behaves
// identically to ChromeContentBrowserClient.
class TestContentBrowserClient : public ChromeContentBrowserClient {
public:
explicit TestContentBrowserClient(
base::WeakPtr<ExtensionMessageWaiter> ipc_message_waiter)
: ipc_message_waiter_(ipc_message_waiter) {
old_client_ = content::SetBrowserClientForTesting(this);
}
~TestContentBrowserClient() override {
content::SetBrowserClientForTesting(old_client_);
}
private:
// content::ContentBrowserClient overrides:
void RenderProcessWillLaunch(content::RenderProcessHost* host) override {
auto test_filter =
base::MakeRefCounted<TestFilter>(ipc_message_waiter_, host->GetID());
host->AddFilter(test_filter.get());
ChromeContentBrowserClient::RenderProcessWillLaunch(host);
}
base::WeakPtr<ExtensionMessageWaiter> ipc_message_waiter_;
raw_ptr<content::ContentBrowserClient> old_client_;
};
std::unique_ptr<TestContentBrowserClient> test_content_browser_client_;
base::RunLoop run_loop_;
IpcMatcher ipc_matcher_;
absl::optional<MessageParamType> captured_message_param_;
int captured_render_process_id_ = -1;
base::WeakPtrFactory<ExtensionMessageWaiter> weak_ptr_factory_;
};
// 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());
ASSERT_TRUE(embedded_test_server()->Start());
}
content::WebContents* active_web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
// 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(
browser()->profile(), extension_id, background_script);
}
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": "ContentScriptTrackerBrowserTest - 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());
};
#if !(BUILDFLAG(IS_FUCHSIA))
// The key below corresponds to the extension ID used by
// ScopedTestNativeMessagingHost::kExtensionId.
const char kActiveExtensionKey[] = R"(
"key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDcBHwzDvyBQ6bDppkIs9MP4ksKqCMyXQ/A52JivHZKh4YO/9vJsT3oaYhSpDCE9RPocOEQvwsHsFReW2nUEc6OLLyoCFFxIb7KkLGsmfakkut/fFdNJYh0xOTbSN8YvLWcqph09XAY2Y/f0AL7vfO1cuCqtkMt8hFrBGWxDdf9CQIDAQAB",
)";
#else
// Native messaging is not available on Fuchsia (i.e.
// //chrome/browser/extensions/BUILD.gn excludes
// api/messaging/native_messaging_test_util.h on Fuchsia).
const char kActiveExtensionKey[] = "";
#endif
active_extension_ = install_extension(active_dir_, kActiveExtensionKey);
spoofed_extension_ = install_extension(spoofed_dir_, "");
ASSERT_TRUE(active_extension_);
ASSERT_TRUE(spoofed_extension_);
#if !(BUILDFLAG(IS_FUCHSIA))
ASSERT_EQ(active_extension_id(),
ScopedTestNativeMessagingHost::kExtensionId);
#endif
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;
using OpenChannelMessageWaiter =
ExtensionMessageWaiter<ExtensionHostMsg_OpenChannelToExtension>;
void SetUpOnMainThread() override {
ExtensionSecurityExploitBrowserTest::SetUpOnMainThread();
// Start capturing IPC messages in all future/new RenderProcessHosts.
ipc_message_waiter_ = std::make_unique<OpenChannelMessageWaiter>();
// 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");
int old_process_id =
active_web_contents()->GetPrimaryMainFrame()->GetProcess()->GetID();
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL("chrome://version")));
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page_url));
int new_process_id =
active_web_contents()->GetPrimaryMainFrame()->GetProcess()->GetID();
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();
// Only intercept messages from `active_extension`'s content script running
// in the main frame's process.
ExtensionId matching_extension_id = active_extension_id();
ipc_message_waiter_->SetIpcMatcher(base::BindLambdaForTesting(
[matching_extension_id](
int captured_render_process_id,
const ExtensionHostMsg_OpenChannelToExtension::Param& param) {
auto [source_context, info, channel_name, port_id] = param;
if (info.source_endpoint.extension_id != matching_extension_id)
return false;
return true;
}));
}
// Waits for ExtensionHostMsg_OpenChannelToExtension IPC and returns its
// payload.
ExtensionHostMsg_OpenChannelToExtension::Param WaitForMessage(
int* process_id) {
return ipc_message_waiter_->WaitForMessage(process_id);
}
private:
std::unique_ptr<OpenChannelMessageWaiter> ipc_message_waiter_;
};
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`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_EQ(MessagingEndpoint::Type::kTab, 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();
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
ASSERT_EQ(process_id, main_frame_process->GetID());
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToExtension(source_context, info,
channel_name, port_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`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_EQ(MessagingEndpoint::Type::kTab, 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;
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToExtension(source_context, info,
channel_name, port_id));
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`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_EQ(MessagingEndpoint::Type::kTab, 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;
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
ASSERT_EQ(process_id, main_frame_process->GetID());
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToExtension(source_context, info,
channel_name, port_id));
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`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_EQ(MessagingEndpoint::Type::kTab, 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 = absl::nullopt;
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
ASSERT_EQ(process_id, main_frame_process->GetID());
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToExtension(source_context, info,
channel_name, port_id));
EXPECT_EQ(bad_message::EMF_NO_EXTENSION_ID_FOR_EXTENSION_SOURCE,
kill_waiter.Wait());
}
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
FromContentScript_UnexpectedWorkerContext) {
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
// from a content script of an `active_extension_id`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_TRUE(source_context.is_for_render_frame());
EXPECT_FALSE(source_context.is_for_service_worker());
// Mutate the IPC payload.
source_context.frame = absl::nullopt;
source_context.worker = PortContext::WorkerContext(
/* thread_id = */ 123, /* version_id = */ 456,
/* extension_id = */ active_extension_id());
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
ASSERT_EQ(process_id, main_frame_process->GetID());
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToExtension(source_context, info,
channel_name, port_id));
EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_WORKER_CONTEXT,
kill_waiter.Wait());
}
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
FromContentScript_BadSourceUrl_FromFrame) {
if (!base::FeatureList::IsEnabled(
extensions_features::kExtensionSourceUrlEnforcement)) {
GTEST_SKIP() << "...BadSourceUrl... tests require "
<< "the kExtensionSourceUrlEnforcement feature";
}
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
// from a content script of an `active_extension_id`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_EQ(MessagingEndpoint::Type::kTab, info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
EXPECT_TRUE(source_context.is_for_render_frame());
// Mutate `source_url` in the IPC payload.
GURL actual_url =
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL();
ASSERT_EQ(actual_url, info.source_url);
GURL spoofed_url =
embedded_test_server()->GetURL("spoofed.com", "/title1.html");
ASSERT_NE(spoofed_url.host(), actual_url.host());
info.source_url = spoofed_url;
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
ASSERT_EQ(process_id, main_frame_process->GetID());
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToExtension(source_context, info,
channel_name, port_id));
EXPECT_EQ(bad_message::EMF_INVALID_SOURCE_URL, kill_waiter.Wait());
}
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
FromServiceWorker_BadSourceUrl) {
if (!base::FeatureList::IsEnabled(
extensions_features::kExtensionSourceUrlEnforcement)) {
GTEST_SKIP() << "...BadSourceUrl... tests require "
<< "the kExtensionSourceUrlEnforcement feature";
}
// Navigate the test tab to an extension page.
// TODO(https://crbug.com/1378019): 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(ui_test_utils::NavigateToURL(browser(), test_page_url));
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
// from the service worker of an `active_extension_id`.
ASSERT_TRUE(BackgroundScriptExecutor::ExecuteScriptAsync(
browser()->profile(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'});"));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_EQ(MessagingEndpoint::Type::kExtension, info.source_endpoint.type);
EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id);
EXPECT_TRUE(source_context.is_for_service_worker());
EXPECT_EQ(info.source_url.host(), active_extension_id());
// Mutate `source_url` in the IPC payload.
info.source_url = spoofed_extension().GetResourceURL("some_resource.html");
EXPECT_EQ(info.source_url.host(), spoofed_extension_id());
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* service_worker_process =
content::RenderProcessHost::FromID(process_id);
ASSERT_TRUE(service_worker_process);
RenderProcessHostBadIpcMessageWaiter kill_waiter(service_worker_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
service_worker_process->GetChannel(),
ExtensionHostMsg_OpenChannelToExtension(source_context, info,
channel_name, port_id));
EXPECT_EQ(bad_message::EMF_INVALID_SOURCE_URL, kill_waiter.Wait());
}
IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest,
FromContentScript_SourceContextOfNativeApp) {
if (!base::FeatureList::IsEnabled(
extensions_features::kExtensionSourceUrlEnforcement)) {
GTEST_SKIP() << "This test requires "
<< "the kExtensionSourceUrlEnforcement feature";
}
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC
// from a content script of an `active_extension_id`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_TRUE(source_context.is_for_render_frame());
// Mutate the IPC payload. The `source_context` below is invalid, because
// `ExtensionHostMsg_OpenChannelToExtension` is sent in
// `//extensions/renderer/ipc_message_sender.cc` only for frames and workers
// (and never for native hosts).
source_context = PortContext::ForNativeHost();
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
ASSERT_EQ(process_id, main_frame_process->GetID());
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToExtension(source_context, info,
channel_name, port_id));
EXPECT_EQ(bad_message::EMF_INVALID_OPEN_CHANNEL_TO_EXTENSION_FROM_NATIVE_HOST,
kill_waiter.Wait());
}
// This is a regression test for https://crbug.com/1379558.
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");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), 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(active_web_contents(),
content::JsReplace(kScriptTemplate, kDataUrl)));
ASSERT_TRUE(listener.WaitUntilSatisfied());
// The main verification here (against https://crbug.com/1379558) 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).
}
// Test suite for covering a specific kind (TMsg) of IPCs from an extension tab.
template <typename TMsg>
class ExtensionTabIpcExploitTest : public ExtensionSecurityExploitBrowserTest {
public:
ExtensionTabIpcExploitTest() = default;
using MessageWaiter = ExtensionMessageWaiter<TMsg>;
void SetUpOnMainThread() override {
ExtensionSecurityExploitBrowserTest::SetUpOnMainThread();
// Set up ExtensionMessageWaiter *before* installing the extensions (i.e.
// *before* the corresponding RenderProcessHost objects are created).
ipc_message_waiter_ = std::make_unique<MessageWaiter>();
InstallTestExtensions();
// Navigate the test tab to an extension page.
GURL test_page_url = active_extension().GetResourceURL("page.html");
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page_url));
// Only intercept messages from the test process.
int matching_process_id =
active_web_contents()->GetPrimaryMainFrame()->GetProcess()->GetID();
ipc_message_waiter_->SetIpcMatcher(base::BindLambdaForTesting(
[matching_process_id](int captured_render_process_id,
const typename TMsg::Param& param) {
if (captured_render_process_id != matching_process_id)
return false;
return true;
}));
}
// Waits for ExtensionHostMsg_OpenChannelToNativeApp IPC and returns its
// payload.
typename TMsg::Param WaitForMessage(int* process_id) {
return ipc_message_waiter_->WaitForMessage(process_id);
}
private:
std::unique_ptr<MessageWaiter> ipc_message_waiter_;
};
// Native messaging is not available on Fuchsia (i.e.
// //chrome/browser/extensions/BUILD.gn excludes
// api/messaging/native_messaging_test_util.h on Fuchsia).
#if !(BUILDFLAG(IS_FUCHSIA))
// Test suite for covering ExtensionHostMsg_OpenChannelToNativeApp IPC.
class OpenChannelToNativeAppExploitTest
: public ExtensionTabIpcExploitTest<
ExtensionHostMsg_OpenChannelToNativeApp> {
public:
OpenChannelToNativeAppExploitTest() = default;
void SetUpOnMainThread() override {
ExtensionTabIpcExploitTest::SetUpOnMainThread();
test_native_messaging_host_.RegisterTestHost(/* user_level= */ false);
}
private:
ScopedTestNativeMessagingHost test_native_messaging_host_;
};
IN_PROC_BROWSER_TEST_F(OpenChannelToNativeAppExploitTest,
SourceContextWithSpoofedExtensionId) {
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToNativeApp IPC
// from a service worker of the `active_extension`.
const char kScript[] = R"(
var message = {text: 'Hello!'};
var host = $1;
chrome.runtime.sendNativeMessage(host, message);
)";
ASSERT_TRUE(BackgroundScriptExecutor::ExecuteScriptAsync(
browser()->profile(), active_extension_id(),
content::JsReplace(kScript, ScopedTestNativeMessagingHost::kHostName)));
// Capture the IPC.
int process_id = -1;
auto [source_context, native_app_name, port_id] = WaitForMessage(&process_id);
EXPECT_EQ(native_app_name, ScopedTestNativeMessagingHost::kHostName);
EXPECT_TRUE(source_context.is_for_service_worker());
EXPECT_EQ(active_extension_id(), source_context.worker->extension_id);
// Mutate the IPC payload.
source_context.worker->extension_id = spoofed_extension_id();
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
ASSERT_EQ(process_id, main_frame_process->GetID());
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToNativeApp(source_context, native_app_name,
port_id));
EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_WORKER_CONTEXT,
kill_waiter.Wait());
}
#endif // !(BUILDFLAG(IS_FUCHSIA)) - native messaging is available
using OpenChannelToTabExploitTest =
ExtensionTabIpcExploitTest<ExtensionHostMsg_OpenChannelToTab>;
IN_PROC_BROWSER_TEST_F(OpenChannelToTabExploitTest,
SpoofedExtensionIdInWorkerSourceContext) {
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToTab IPC
// from a service worker of the `active_extension`.
const char kScript[] = R"(
const message = {text: 'Hello!'};
const tabId = 123;
chrome.tabs.sendMessage(tabId, message);
)";
ASSERT_TRUE(BackgroundScriptExecutor::ExecuteScriptAsync(
browser()->profile(), active_extension_id(), kScript));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_TRUE(source_context.is_for_service_worker());
EXPECT_EQ(active_extension_id(), source_context.worker->extension_id);
// Mutate the IPC payload.
source_context.worker->extension_id = spoofed_extension_id();
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
ASSERT_EQ(process_id, main_frame_process->GetID());
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToTab(source_context, info, channel_name,
port_id));
EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_WORKER_CONTEXT,
kill_waiter.Wait());
}
IN_PROC_BROWSER_TEST_F(OpenChannelToTabExploitTest,
SpoofedNativeHostSourceContext) {
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToTab IPC
// from a service worker of the `active_extension`.
const char kScript[] = R"(
const message = {text: 'Hello!'};
const tabId = 123;
chrome.tabs.sendMessage(tabId, message);
)";
ASSERT_TRUE(BackgroundScriptExecutor::ExecuteScriptAsync(
browser()->profile(), active_extension_id(), kScript));
// Capture the IPC.
int process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&process_id);
EXPECT_TRUE(source_context.is_for_service_worker());
EXPECT_EQ(active_extension_id(), source_context.worker->extension_id);
// Mutate the IPC payload.
source_context = PortContext::ForNativeHost();
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
ASSERT_EQ(process_id, main_frame_process->GetID());
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToTab(source_context, info, channel_name,
port_id));
EXPECT_EQ(bad_message::EMF_NON_EXTENSION_SENDER_NATIVE_HOST,
kill_waiter.Wait());
}
IN_PROC_BROWSER_TEST_F(OpenChannelToTabExploitTest,
SpoofedIpcFromNonExtensionFrame) {
// Trigger sending of a valid ExtensionHostMsg_OpenChannelToTab IPC
// from a frame of an `active_extension`.
const char kScript[] = R"(
const message = {text: 'Hello!'};
const tabId = 123;
chrome.tabs.sendMessage(tabId, message);
)";
ASSERT_EQ(
active_extension().origin(),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
ASSERT_TRUE(content::ExecuteScript(active_web_contents(), kScript));
// Capture the IPC.
int ignored_process_id = -1;
auto [source_context, info, channel_name, port_id] =
WaitForMessage(&ignored_process_id);
// Navigate the test frame to a non-extension location.
GURL http_url = embedded_test_server()->GetURL("foo.com", "/title1.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), http_url));
// Mutate the IPC payload.
source_context = PortContext::ForFrame(
active_web_contents()->GetPrimaryMainFrame()->GetRoutingID());
// Inject the malformed/mutated IPC and verify that the renderer is terminated
// as expected.
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
main_frame_process->GetChannel(),
ExtensionHostMsg_OpenChannelToTab(source_context, info, channel_name,
port_id));
EXPECT_EQ(bad_message::EMF_NON_EXTENSION_SENDER_FRAME, kill_waiter.Wait());
}
// This is a regression test for https://crbug.com/1407087.
IN_PROC_BROWSER_TEST_F(OpenChannelToTabExploitTest,
LegitimateIpcFromSandboxedExtensionSubframe) {
// Verify the test setup - the tab should contain a frame from the active
// extension (which has access to the `chrome.tabs` API).
ASSERT_EQ(
active_extension().origin(),
active_web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());
EXPECT_EQ(true, content::EvalJs(active_web_contents(), "!!chrome.tabs"));
// Add a sandboxed subframe.
content::RenderFrameHost* subframe = nullptr;
{
const char kScript[] = R"(
new Promise(function (resolve, reject) {
var iframe = document.createElement('iframe');
iframe.sandbox = "allow-scripts";
iframe.src = "page.html";
iframe.onload = () => {
resolve("onload");
};
document.body.appendChild(iframe);
});
)";
ASSERT_EQ("onload", content::EvalJs(active_web_contents(), kScript));
subframe = ChildFrameAt(active_web_contents(), 0);
ASSERT_TRUE(subframe);
}
// Verify test setup - the `subframe` should be a sandboxed extension frame
// that still has access to the `chrome.tabs` API.
{
const url::Origin& origin = subframe->GetLastCommittedOrigin();
EXPECT_TRUE(origin.opaque());
const url::SchemeHostPort& shp = origin.GetTupleOrPrecursorTupleIfOpaque();
EXPECT_EQ(shp.scheme(), kExtensionScheme);
EXPECT_EQ(shp.host(), active_extension_id());
ASSERT_EQ(true, content::EvalJs(subframe, "!!chrome.tabs"));
}
// Trigger `ExtensionHostMsg_OpenChannelToTab` from the `subframe` and then
// signal completion of the test.
{
ExtensionTestMessageListener listener("Done with test");
const char kScript[] = R"(
const message = {text: 'Hello!'};
const tabId = 12345;
try {
chrome.tabs.sendMessage(tabId, message);
} catch (error) {
// The error is expected (because of a fake `tabId`)
// and can be ignored (we only want to see if the IPC
// would trigger a renderer kill).
}
chrome.test.sendMessage('Done with test'); )";
ASSERT_TRUE(content::ExecJs(subframe, kScript));
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
// Completing the test without an unexpected renderer kill (as verified by the
// test harness - see `content::NoRendererCrashesAssertion`) means that the
// test passed. In particular, this verifies that we didn't reintroduce the
// `EMF_NON_EXTENSION_SENDER_FRAME` renderer kill from
// https://crbug.com/1407087.
}
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");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page_url));
content::RenderFrameHost* main_frame =
active_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(active_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,
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.
content::RenderProcessHostBadMojoMessageWaiter kill_waiter(
main_frame->GetProcess());
ASSERT_TRUE(ExecuteProgrammaticContentScript(active_web_contents(),
active_extension_id(),
R"(
chrome.storage.local.set({ test_key: 'test value2'}, () => {}); )"));
EXPECT_EQ(
"Received bad user message: LocalFrameHost::Request: renderer never "
"hosted such extension",
kill_waiter.Wait());
}
} // namespace extensions