|  | // 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 "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 "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()); | 
|  | } | 
|  |  | 
|  | // Asks the `extension_id` to inject `content_script` into `web_contents`. | 
|  | // Returns true if the content script execution started successfully. | 
|  | bool ExecuteProgrammaticContentScript(content::WebContents* web_contents, | 
|  | const ExtensionId& extension_id, | 
|  | const std::string& content_script) { | 
|  | DCHECK(web_contents); | 
|  | int tab_id = ExtensionTabUtil::GetTabId(web_contents); | 
|  | const char kScriptTemplate[] = R"( | 
|  | chrome.scripting.executeScript({ | 
|  | target: {tabId: %d}, | 
|  | injectImmediately: true, | 
|  | func: () => { %s } | 
|  | }); | 
|  | )"; | 
|  | std::string background_script = | 
|  | base::StringPrintf(kScriptTemplate, tab_id, content_script.c_str()); | 
|  | return BackgroundScriptExecutor::ExecuteScriptAsync(profile(), extension_id, | 
|  | background_script); | 
|  | } | 
|  |  | 
|  | // (Asynchronously) executes the given `script` in a user script world in | 
|  | // `web_contents`, associated with the given `extension_id` | 
|  | void ExecuteUserScript(content::WebContents& web_contents, | 
|  | const ExtensionId& extension_id, | 
|  | const std::string& script) { | 
|  | 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"); | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | int old_process_id = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(); | 
|  | EXPECT_TRUE(NavigateToURL(web_contents, GURL("chrome://version"))); | 
|  | EXPECT_TRUE(NavigateToURL(web_contents, test_page_url)); | 
|  | int new_process_id = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID(); | 
|  | EXPECT_NE(old_process_id, new_process_id); | 
|  |  | 
|  | // Install the extensions (and potentially spawn new RenderProcessHosts) | 
|  | // only *after* the `ipc_message_waiter_` has been constructed. | 
|  | InstallTestExtensions(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest, | 
|  | FromContentScript_BadExtensionIdInMessagingSource) { | 
|  | // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC | 
|  | // from a content script of an `active_extension_id`. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ASSERT_TRUE(ExecuteProgrammaticContentScript( | 
|  | web_contents, active_extension_id(), | 
|  | "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});")); | 
|  |  | 
|  | content::RenderProcessHost* main_frame_process = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess(); | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process); | 
|  | auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>( | 
|  | web_contents->GetPrimaryMainFrame()); | 
|  | interceptor->SetOpenChannelToExtensionMutator( | 
|  | base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) { | 
|  | EXPECT_EQ(MessagingEndpoint::Type::kContentScript, | 
|  | info.source_endpoint.type); | 
|  | EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id); | 
|  | // Mutate the IPC payload. | 
|  | info.source_endpoint.extension_id = spoofed_extension_id(); | 
|  | })); | 
|  | EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_CONTENT_SCRIPT, | 
|  | kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest, | 
|  | FromContentScript_UnexpectedNativeAppType) { | 
|  | // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC | 
|  | // from a content script of an `active_extension_id`. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ASSERT_TRUE(ExecuteProgrammaticContentScript( | 
|  | web_contents, active_extension_id(), | 
|  | "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});")); | 
|  |  | 
|  | content::RenderProcessHost* main_frame_process = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess(); | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process); | 
|  | auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>( | 
|  | web_contents->GetPrimaryMainFrame()); | 
|  | interceptor->SetOpenChannelToExtensionMutator( | 
|  | base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) { | 
|  | EXPECT_EQ(MessagingEndpoint::Type::kContentScript, | 
|  | info.source_endpoint.type); | 
|  | EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id); | 
|  | // Mutate the IPC payload. | 
|  | info.source_endpoint.type = MessagingEndpoint::Type::kNativeApp; | 
|  | })); | 
|  | EXPECT_EQ(bad_message::EMF_INVALID_CHANNEL_SOURCE_TYPE, kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest, | 
|  | FromContentScript_UnexpectedExtensionType) { | 
|  | // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC | 
|  | // from a content script of an `active_extension_id`. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ASSERT_TRUE(ExecuteProgrammaticContentScript( | 
|  | web_contents, active_extension_id(), | 
|  | "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});")); | 
|  |  | 
|  | content::RenderProcessHost* main_frame_process = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess(); | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process); | 
|  | auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>( | 
|  | web_contents->GetPrimaryMainFrame()); | 
|  | interceptor->SetOpenChannelToExtensionMutator( | 
|  | base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) { | 
|  | EXPECT_EQ(MessagingEndpoint::Type::kContentScript, | 
|  | info.source_endpoint.type); | 
|  | EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id); | 
|  | // Mutate the IPC payload. | 
|  | info.source_endpoint.type = MessagingEndpoint::Type::kExtension; | 
|  | })); | 
|  | EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_EXTENSION_SOURCE, | 
|  | kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest, | 
|  | FromContentScript_NoExtensionIdForExtensionType) { | 
|  | // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC | 
|  | // from a content script of an `active_extension_id`. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ASSERT_TRUE(ExecuteProgrammaticContentScript( | 
|  | web_contents, active_extension_id(), | 
|  | "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});")); | 
|  |  | 
|  | content::RenderProcessHost* main_frame_process = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess(); | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process); | 
|  | auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>( | 
|  | web_contents->GetPrimaryMainFrame()); | 
|  | interceptor->SetOpenChannelToExtensionMutator( | 
|  | base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) { | 
|  | EXPECT_EQ(MessagingEndpoint::Type::kContentScript, | 
|  | info.source_endpoint.type); | 
|  | EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id); | 
|  |  | 
|  | // Mutate the IPC payload. | 
|  | info.source_endpoint.type = MessagingEndpoint::Type::kExtension; | 
|  | info.source_endpoint.extension_id = std::nullopt; | 
|  | })); | 
|  | EXPECT_EQ(bad_message::EMF_NO_EXTENSION_ID_FOR_EXTENSION_SOURCE, | 
|  | kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest, | 
|  | FromContentScript_BadSourceUrl_FromFrame) { | 
|  | // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC | 
|  | // from a content script of an `active_extension_id`. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ASSERT_TRUE(ExecuteProgrammaticContentScript( | 
|  | web_contents, active_extension_id(), | 
|  | "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});")); | 
|  |  | 
|  | content::RenderProcessHost* main_frame_process = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess(); | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process); | 
|  | auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>( | 
|  | web_contents->GetPrimaryMainFrame()); | 
|  | interceptor->SetOpenChannelToExtensionMutator( | 
|  | base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) { | 
|  | EXPECT_EQ(MessagingEndpoint::Type::kContentScript, | 
|  | info.source_endpoint.type); | 
|  | EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id); | 
|  |  | 
|  | // Mutate `source_url` in the IPC payload. | 
|  | GURL actual_url = GetActiveWebContents() | 
|  | ->GetPrimaryMainFrame() | 
|  | ->GetLastCommittedURL(); | 
|  | ASSERT_EQ(actual_url, info.source_url); | 
|  | GURL spoofed_url = | 
|  | embedded_test_server()->GetURL("spoofed.com", "/title1.html"); | 
|  | ASSERT_NE(spoofed_url.GetHost(), actual_url.GetHost()); | 
|  | info.source_url = spoofed_url; | 
|  | })); | 
|  | EXPECT_EQ(bad_message::EMF_INVALID_SOURCE_URL, kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest, | 
|  | FromServiceWorker_BadSourceUrl) { | 
|  | // Navigate the test tab to an extension page. | 
|  | // TODO(crbug.com/40874764): Remove this test step - it is only here as | 
|  | // a workaround for the bug that impacts how renderer kills are detected when | 
|  | // there are no frames in a given renderer process. | 
|  | GURL test_page_url = active_extension().GetResourceURL("page.html"); | 
|  | EXPECT_TRUE(NavigateToURL(GetActiveWebContents(), test_page_url)); | 
|  |  | 
|  | // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC | 
|  | // from the service worker of an `active_extension_id`. | 
|  | ASSERT_TRUE(BackgroundScriptExecutor::ExecuteScriptAsync( | 
|  | profile(), active_extension_id(), | 
|  | "chrome.runtime.sendMessage({greeting: 'hello'});")); | 
|  |  | 
|  | std::vector<WorkerId> service_workers = | 
|  | ProcessManager::Get(profile())->GetServiceWorkersForExtension( | 
|  | active_extension_id()); | 
|  | ASSERT_EQ(1u, service_workers.size()); | 
|  |  | 
|  | content::RenderProcessHost* service_worker_process = | 
|  | content::RenderProcessHost::FromID(service_workers[0].render_process_id); | 
|  | ASSERT_TRUE(service_worker_process); | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(service_worker_process); | 
|  | auto interceptor = | 
|  | std::make_unique<ServiceWorkerHostInterceptorForProcessDeath>( | 
|  | service_workers[0]); | 
|  | interceptor->SetOpenChannelToExtensionMutator( | 
|  | base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) { | 
|  | EXPECT_EQ(MessagingEndpoint::Type::kExtension, | 
|  | info.source_endpoint.type); | 
|  | EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id); | 
|  | EXPECT_EQ(info.source_url.GetHost(), active_extension_id()); | 
|  |  | 
|  | // Mutate `source_url` in the IPC payload. | 
|  | info.source_url = | 
|  | spoofed_extension().GetResourceURL("some_resource.html"); | 
|  | EXPECT_EQ(info.source_url.GetHost(), spoofed_extension_id()); | 
|  | })); | 
|  |  | 
|  | EXPECT_EQ(bad_message::EMF_INVALID_SOURCE_URL, kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | // 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"); | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ASSERT_TRUE(NavigateToURL(web_contents, http_url)); | 
|  |  | 
|  | // Add a data: URL subframe and wait for its `source_url` to be echoed back. | 
|  | // The data: URL encodes `<p>foo</p>` HTML doc. | 
|  | const GURL kDataUrl("data:text/html;charset=utf-8;base64,PHA+Zm9vPC9wPg=="); | 
|  | ExtensionTestMessageListener listener(kDataUrl.spec()); | 
|  | const char kScriptTemplate[] = R"( | 
|  | new Promise(function (resolve, reject) { | 
|  | var iframe = document.createElement('iframe'); | 
|  | iframe.src = $1; | 
|  | iframe.onload = () => { | 
|  | resolve("onload"); | 
|  | }; | 
|  | document.body.appendChild(iframe); | 
|  | }); | 
|  | )"; | 
|  | ASSERT_EQ("onload", | 
|  | content::EvalJs(web_contents, | 
|  | content::JsReplace(kScriptTemplate, kDataUrl))); | 
|  | ASSERT_TRUE(listener.WaitUntilSatisfied()); | 
|  |  | 
|  | // The main verification here (against https://crbug.com/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"); | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ASSERT_TRUE(NavigateToURL(web_contents, test_page_url)); | 
|  | content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame(); | 
|  |  | 
|  | // Verify the test setup by checking if the non-intercepted `chrome.storage` | 
|  | // API call will succeed. | 
|  | { | 
|  | ExtensionTestMessageListener listener("Got chrome.storage response"); | 
|  | ASSERT_TRUE(ExecuteProgrammaticContentScript(web_contents, | 
|  | active_extension_id(), R"( | 
|  | chrome.storage.local.set( | 
|  | { test_key: 'test value'}, | 
|  | () => { | 
|  | chrome.test.sendMessage('Got chrome.storage response'); | 
|  | } | 
|  | ); )")); | 
|  | ASSERT_TRUE(listener.WaitUntilSatisfied()); | 
|  | } | 
|  |  | 
|  | // Prepare to mutate the extension id in the IPC associated with the | 
|  | // `chrome.storage.local.set`. | 
|  | auto interceptor = | 
|  | std::make_unique<ExtensionFrameHostInterceptor>(main_frame); | 
|  | interceptor->SetRequestMutator( | 
|  | base::BindLambdaForTesting([this](mojom::RequestParams& request_params) { | 
|  | if (request_params.name != "storage.set") | 
|  | return; | 
|  |  | 
|  | EXPECT_EQ(active_extension_id(), request_params.extension_id); | 
|  | request_params.extension_id = spoofed_extension_id(); | 
|  | })); | 
|  |  | 
|  | // Trigger an IPC associated with the `chrome.storage.local.set` API and | 
|  | // verify that the mutated/spoofed extension id is detected and leads to | 
|  | // terminating the misbehaving renderer process. | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess()); | 
|  | ASSERT_TRUE(ExecuteProgrammaticContentScript(GetActiveWebContents(), | 
|  | active_extension_id(), | 
|  | R"( | 
|  | chrome.storage.local.set({ test_key: 'test value2'}, () => {}); )")); | 
|  | EXPECT_EQ(bad_message::EFD_INVALID_EXTENSION_ID_FOR_PROCESS, | 
|  | kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest, | 
|  | FromUserScript_BadExtensionIdInMessagingSource) { | 
|  | AllowUserScriptMessaging(active_extension()); | 
|  |  | 
|  | // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC | 
|  | // from a user script of an `active_extension_id`. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ExecuteUserScript( | 
|  | *web_contents, active_extension_id(), | 
|  | "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"); | 
|  |  | 
|  | content::RenderProcessHost* main_frame_process = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess(); | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process); | 
|  | auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>( | 
|  | web_contents->GetPrimaryMainFrame()); | 
|  | interceptor->SetOpenChannelToExtensionMutator( | 
|  | base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) { | 
|  | EXPECT_EQ(MessagingEndpoint::Type::kUserScript, | 
|  | info.source_endpoint.type); | 
|  | EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id); | 
|  | // Mutate the IPC payload. | 
|  | info.source_endpoint.extension_id.reset(); | 
|  | })); | 
|  | EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_USER_SCRIPT, | 
|  | kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest, | 
|  | FromUserScript_SpoofedExtensionIdInMessagingSource) { | 
|  | AllowUserScriptMessaging(active_extension()); | 
|  |  | 
|  | // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC | 
|  | // from a user script of an `active_extension_id`. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ExecuteUserScript( | 
|  | *web_contents, active_extension_id(), | 
|  | "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"); | 
|  |  | 
|  | content::RenderProcessHost* main_frame_process = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess(); | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process); | 
|  | auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>( | 
|  | web_contents->GetPrimaryMainFrame()); | 
|  | interceptor->SetOpenChannelToExtensionMutator( | 
|  | base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) { | 
|  | EXPECT_EQ(MessagingEndpoint::Type::kUserScript, | 
|  | info.source_endpoint.type); | 
|  | EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id); | 
|  | // Mutate the IPC payload. | 
|  | info.source_endpoint.extension_id = spoofed_extension_id(); | 
|  | })); | 
|  | EXPECT_EQ(bad_message::EMF_INVALID_EXTENSION_ID_FOR_USER_SCRIPT, | 
|  | kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | IN_PROC_BROWSER_TEST_F(OpenChannelToExtensionExploitTest, | 
|  | FromUserScript_TargetingAnotherExtensionId) { | 
|  | AllowUserScriptMessaging(active_extension()); | 
|  |  | 
|  | // Trigger sending of a valid ExtensionHostMsg_OpenChannelToExtension IPC | 
|  | // from a user script of an `active_extension_id`. | 
|  | auto* web_contents = GetActiveWebContents(); | 
|  | ExecuteUserScript( | 
|  | *web_contents, active_extension_id(), | 
|  | "chrome.runtime.sendMessage({greeting: 'hello'}, (response) => {});"); | 
|  |  | 
|  | content::RenderProcessHost* main_frame_process = | 
|  | web_contents->GetPrimaryMainFrame()->GetProcess(); | 
|  | RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame_process); | 
|  | auto interceptor = std::make_unique<ExtensionFrameHostInterceptor>( | 
|  | web_contents->GetPrimaryMainFrame()); | 
|  | interceptor->SetOpenChannelToExtensionMutator( | 
|  | base::BindLambdaForTesting([this](mojom::ExternalConnectionInfo& info) { | 
|  | EXPECT_EQ(MessagingEndpoint::Type::kUserScript, | 
|  | info.source_endpoint.type); | 
|  | EXPECT_EQ(active_extension_id(), info.source_endpoint.extension_id); | 
|  | // Mutate the IPC payload. | 
|  | info.target_id = spoofed_extension_id(); | 
|  | })); | 
|  | EXPECT_EQ(bad_message::EMF_INVALID_EXTERNAL_EXTENSION_ID_FOR_USER_SCRIPT, | 
|  | kill_waiter.Wait()); | 
|  | } | 
|  |  | 
|  | }  // namespace extensions |