| // 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, ¶m)) { |
| 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 |