blob: a2c9e10bd259e9f29d6f3c505f167fe01c4b3dfd [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/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.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.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/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_executor.h"
#include "extensions/browser/service_worker/service_worker_host.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/frame.mojom-test-utils.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 "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"
namespace extensions {
// 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 OpenChannelToExtensionMutator =
base::RepeatingCallback<void(mojom::ExternalConnectionInfo& info)>;
void SetOpenChannelToExtensionMutator(
OpenChannelToExtensionMutator open_extension_mutator) {
open_extension_mutator_ = std::move(open_extension_mutator);
}
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 (request_mutator_ && frame_ == current_target_frame) {
request_mutator_.Run(*params);
}
GetForwardingInterface()->Request(std::move(params), std::move(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().GetCurrentTargetFrame();
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));
}
const raw_ptr<content::RenderFrameHost> frame_ = nullptr;
RequestMutator request_mutator_;
OpenChannelToExtensionMutator open_extension_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());
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);
}
// (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) {
ScriptExecutor script_executor(&web_contents);
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(mojom::JSSource::New(script, GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension_id),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kUserScript,
/*world_id=*/std::nullopt,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::SPECIFIED_FRAMES, {ExtensionApiFrameIdMap::kTopFrameId},
mojom::MatchOriginAsFallbackBehavior::kNever,
mojom::RunLocation::kDocumentIdle, ScriptExecutor::DEFAULT_PROCESS,
GURL() /* webview_src */, base::DoNothing());
}
// 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");
int old_process_id = active_web_contents()
->GetPrimaryMainFrame()
->GetProcess()
->GetDeprecatedID();
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()
->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();
}
};
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) => {});"));
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
active_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`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
active_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`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
active_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`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
active_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`.
ASSERT_TRUE(ExecuteProgrammaticContentScript(
active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"));
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
active_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 =
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;
}));
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().ResolveExtensionURL("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'});"));
std::vector<WorkerId> service_workers =
ProcessManager::Get(browser()->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.host(), active_extension_id());
// Mutate `source_url` in the IPC payload.
info.source_url =
spoofed_extension().ResolveExtensionURL("some_resource.html");
EXPECT_EQ(info.source_url.host(), spoofed_extension_id());
}));
EXPECT_EQ(bad_message::EMF_INVALID_SOURCE_URL, 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).
}
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);
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(active_web_contents(),
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`.
ExecuteUserScript(
*active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});");
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
active_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`.
ExecuteUserScript(
*active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});");
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
active_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`.
ExecuteUserScript(
*active_web_contents(), active_extension_id(),
"chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});");
content::RenderProcessHost* main_frame_process =
active_web_contents()->GetPrimaryMainFrame()->GetProcess();
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process);
auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>(
active_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());
}
} // namespace extensions