| // 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 |