// Copyright (c) 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include "base/bind.h" #include "base/command_line.h" #include "base/feature_list.h" #include "base/files/file_util.h" #include "base/macros.h" #include "base/memory/ptr_util.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/test/scoped_feature_list.h" #include "base/unguessable_token.h" #include "build/build_config.h" #include "content/browser/bad_message.h" #include "content/browser/child_process_security_policy_impl.h" #include "content/browser/dom_storage/dom_storage_context_wrapper.h" #include "content/browser/dom_storage/session_storage_namespace_impl.h" #include "content/browser/renderer_host/navigator.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/renderer_host/render_frame_proxy_host.h" #include "content/browser/renderer_host/render_process_host_impl.h" #include "content/browser/renderer_host/render_view_host_factory.h" #include "content/browser/renderer_host/render_view_host_impl.h" #include "content/browser/web_contents/file_chooser_impl.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/common/frame.mojom.h" #include "content/common/frame_messages.mojom.h" #include "content/common/render_message_filter.mojom.h" #include "content/public/browser/blob_handle.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/file_select_listener.h" #include "content/public/browser/navigation_handle.h" #include "content/public/browser/resource_context.h" #include "content/public/browser/storage_partition.h" #include "content/public/common/bindings_policy.h" #include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" #include "content/public/common/url_constants.h" #include "content/public/test/back_forward_cache_util.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test_utils.h" #include "content/public/test/navigation_handle_observer.h" #include "content/public/test/test_navigation_observer.h" #include "content/public/test/test_renderer_host.h" #include "content/public/test/test_utils.h" #include "content/shell/browser/shell.h" #include "content/test/content_browser_test_utils_internal.h" #include "content/test/did_commit_navigation_interceptor.h" #include "content/test/frame_host_interceptor.h" #include "content/test/test_content_browser_client.h" #include "ipc/ipc_message.h" #include "ipc/ipc_security_test_util.h" #include "mojo/core/embedder/embedder.h" #include "mojo/public/cpp/bindings/pending_associated_remote.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/bindings/self_owned_associated_receiver.h" #include "mojo/public/cpp/test_support/test_utils.h" #include "net/base/filename_util.h" #include "net/base/network_isolation_key.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/controllable_http_response.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/http_request.h" #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" #include "services/network/public/cpp/features.h" #include "services/network/public/cpp/network_switches.h" #include "services/network/public/cpp/resource_request.h" #include "services/network/public/mojom/trust_tokens.mojom.h" #include "services/network/public/mojom/url_loader.mojom.h" #include "services/network/test/test_url_loader_client.h" #include "storage/browser/blob/blob_registry_impl.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/abseil-cpp/absl/types/optional.h" #include "third_party/blink/public/common/blob/blob_utils.h" #include "third_party/blink/public/common/navigation/navigation_policy.h" #include "third_party/blink/public/mojom/appcache/appcache.mojom.h" #include "third_party/blink/public/mojom/blob/blob_url_store.mojom-test-utils.h" #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h" #include "third_party/blink/public/mojom/frame/frame.mojom-test-utils.h" #include "third_party/blink/public/mojom/frame/frame.mojom.h" #include "third_party/blink/public/mojom/loader/mixed_content.mojom.h" using IPC::IpcSecurityTestUtil; using ::testing::HasSubstr; using ::testing::Optional; namespace content { namespace { // This is a helper function for the tests which attempt to create a // duplicate RenderViewHost or RenderWidgetHost. It tries to create two objects // with the same process and routing ids, which causes a collision. // It creates a couple of windows in process 1, which causes a few routing ids // to be allocated. Then a cross-process navigation is initiated, which causes a // new process 2 to be created and have a pending RenderViewHost for it. The // routing id of the RenderViewHost which is target for a duplicate is set // into |target_routing_id| and the pending RenderFrameHost which is used for // the attempt is the return value. RenderFrameHostImpl* PrepareToDuplicateHosts(Shell* shell, net::EmbeddedTestServer* server, int* target_routing_id) { GURL foo("http://foo.com/simple_page.html"); if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) { // Isolate "bar.com" so we are guaranteed to get a different process // for navigations to this origin. IsolateOriginsForTesting(server, shell->web_contents(), {"bar.com"}); } // Start off with initial navigation, so we get the first process allocated. EXPECT_TRUE(NavigateToURL(shell, foo)); EXPECT_EQ(u"OK", shell->web_contents()->GetTitle()); // Open another window, so we generate some more routing ids. ShellAddedObserver shell2_observer; EXPECT_TRUE(ExecJs(shell, "window.open(document.URL + '#2');")); Shell* shell2 = shell2_observer.GetShell(); // The new window must be in the same process, but have a new routing id. EXPECT_EQ(shell->web_contents()->GetMainFrame()->GetProcess()->GetID(), shell2->web_contents()->GetMainFrame()->GetProcess()->GetID()); *target_routing_id = shell2->web_contents() ->GetMainFrame() ->GetRenderViewHost() ->GetRoutingID(); EXPECT_NE(*target_routing_id, shell->web_contents() ->GetMainFrame() ->GetRenderViewHost() ->GetRoutingID()); // Now, simulate a link click coming from the renderer. GURL extension_url("http://bar.com/simple_page.html"); WebContentsImpl* wc = static_cast(shell->web_contents()); wc->GetFrameTree()->root()->navigator().RequestOpenURL( wc->GetFrameTree()->root()->current_frame_host(), extension_url, nullptr /* initiator_frame_token */, ChildProcessHost::kInvalidUniqueID /* initiator_process_id */, url::Origin::Create(foo), nullptr, std::string(), Referrer(), WindowOpenDisposition::CURRENT_TAB, false, true, blink::mojom::TriggeringEventInfo::kFromTrustedEvent, std::string(), nullptr /* blob_url_loader_factory */, absl::nullopt /* impression */); // Since the navigation above requires a cross-process swap, there will be a // speculative/pending RenderFrameHost. Ensure it exists and is in a different // process than the initial page. RenderFrameHostImpl* next_rfh = wc->GetRenderManagerForTesting()->speculative_frame_host(); EXPECT_TRUE(next_rfh); EXPECT_NE(shell->web_contents()->GetMainFrame()->GetProcess()->GetID(), next_rfh->GetProcess()->GetID()); return next_rfh; } blink::mojom::OpenURLParamsPtr CreateOpenURLParams(const GURL& url) { auto params = blink::mojom::OpenURLParams::New(); params->url = url; params->disposition = WindowOpenDisposition::CURRENT_TAB; params->should_replace_current_entry = false; params->user_gesture = true; return params; } std::unique_ptr CreateMemoryBackedBlob( BrowserContext* browser_context, const std::string& contents, const std::string& content_type) { std::unique_ptr result; base::RunLoop loop; browser_context->CreateMemoryBackedBlob( base::as_bytes(base::make_span(contents)), content_type, base::BindOnce( [](std::unique_ptr* out_blob, base::OnceClosure done, std::unique_ptr blob) { *out_blob = std::move(blob); std::move(done).Run(); }, &result, loop.QuitClosure())); loop.Run(); EXPECT_TRUE(result); return result; } // Helper class to interpose on Blob URL registrations, replacing the URL // contained in incoming registration requests with the specified URL. class BlobURLStoreInterceptor : public blink::mojom::BlobURLStoreInterceptorForTesting { public: static void Intercept( GURL target_url, mojo::SelfOwnedAssociatedReceiverRef receiver) { auto interceptor = base::WrapUnique(new BlobURLStoreInterceptor(target_url)); auto* raw_interceptor = interceptor.get(); auto impl = receiver->SwapImplForTesting(std::move(interceptor)); raw_interceptor->url_store_ = std::move(impl); } blink::mojom::BlobURLStore* GetForwardingInterface() override { return url_store_.get(); } void Register( mojo::PendingRemote blob, const GURL& url, // TODO(https://crbug.com/1224926): Remove this once experiment is over. const base::UnguessableToken& unsafe_agent_cluster_id, RegisterCallback callback) override { GetForwardingInterface()->Register(std::move(blob), target_url_, unsafe_agent_cluster_id, std::move(callback)); } private: explicit BlobURLStoreInterceptor(GURL target_url) : target_url_(target_url) {} std::unique_ptr url_store_; GURL target_url_; }; // Constructs a WebContentsDelegate that mocks a file dialog. // Unlike content::FileChooserDelegate, this class doesn't make a response in // RunFileChooser(), and a user needs to call Choose(). class DelayedFileChooserDelegate : public WebContentsDelegate { public: void Choose(const base::FilePath& file) { auto file_info = blink::mojom::FileChooserFileInfo::NewNativeFile( blink::mojom::NativeFileInfo::New(file, std::u16string())); std::vector files; files.push_back(std::move(file_info)); listener_->FileSelected(std::move(files), base::FilePath(), blink::mojom::FileChooserParams::Mode::kOpen); listener_.reset(); } // WebContentsDelegate overrides void RunFileChooser(RenderFrameHost* render_frame_host, scoped_refptr listener, const blink::mojom::FileChooserParams& params) override { listener_ = std::move(listener); } void EnumerateDirectory(WebContents* web_contents, scoped_refptr listener, const base::FilePath& directory_path) override { listener->FileSelectionCanceled(); } private: scoped_refptr listener_; }; void FileChooserCallback(base::RunLoop* run_loop, blink::mojom::FileChooserResultPtr result) { run_loop->Quit(); } } // namespace // The goal of these tests will be to "simulate" exploited renderer processes, // which can send arbitrary IPC messages and confuse browser process internal // state, leading to security bugs. We are trying to verify that the browser // doesn't perform any dangerous operations in such cases. class SecurityExploitBrowserTest : public ContentBrowserTest { public: SecurityExploitBrowserTest() {} void SetUpCommandLine(base::CommandLine* command_line) override { // EmbeddedTestServer::InitializeAndListen() initializes its |base_url_| // which is required below. This cannot invoke Start() however as that kicks // off the "EmbeddedTestServer IO Thread" which then races with // initialization in ContentBrowserTest::SetUp(), http://crbug.com/674545. ASSERT_TRUE(embedded_test_server()->InitializeAndListen()); // Add a host resolver rule to map all outgoing requests to the test server. // This allows us to use "real" hostnames in URLs, which we can use to // create arbitrary SiteInstances. command_line->AppendSwitchASCII( network::switches::kHostResolverRules, "MAP * " + net::HostPortPair::FromURL(embedded_test_server()->base_url()) .ToString() + ",EXCLUDE localhost"); } void SetUpOnMainThread() override { // Complete the manual Start() after ContentBrowserTest's own // initialization, ref. comment on InitializeAndListen() above. embedded_test_server()->StartAcceptingConnections(); } protected: // Tests that a given file path sent in a FrameHostMsg_RunFileChooser will // cause renderer to be killed. void TestFileChooserWithPath(const base::FilePath& path); void IsolateOrigin(const std::string& hostname) { IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(), {hostname}); } }; void SecurityExploitBrowserTest::TestFileChooserWithPath( const base::FilePath& path) { GURL foo("http://foo.com/simple_page.html"); EXPECT_TRUE(NavigateToURL(shell(), foo)); EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle()); RenderFrameHost* compromised_renderer = shell()->web_contents()->GetMainFrame(); blink::mojom::FileChooserParamsPtr params = blink::mojom::FileChooserParams::New(); params->default_file_name = path; mojo::test::BadMessageObserver bad_message_observer; mojo::Remote chooser = FileChooserImpl::CreateBoundForTesting( static_cast(compromised_renderer)); chooser->OpenFileChooser( std::move(params), blink::mojom::FileChooser::OpenFileChooserCallback()); chooser.FlushForTesting(); EXPECT_THAT(bad_message_observer.WaitForBadMessage(), ::testing::StartsWith("FileChooser: The default file name")); } // Ensure that we kill the renderer process if we try to give it WebUI // properties and it doesn't have enabled WebUI bindings. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, SetWebUIProperty) { GURL foo("http://foo.com/simple_page.html"); EXPECT_TRUE(NavigateToURL(shell(), foo)); EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle()); EXPECT_EQ(0, shell()->web_contents()->GetMainFrame()->GetEnabledBindings()); RenderFrameHost* compromised_renderer = shell()->web_contents()->GetMainFrame(); RenderProcessHostBadIpcMessageWaiter kill_waiter( compromised_renderer->GetProcess()); compromised_renderer->SetWebUIProperty("toolkit", "views"); EXPECT_EQ(bad_message::RVH_WEB_UI_BINDINGS_MISMATCH, kill_waiter.Wait()); } // This is a test for crbug.com/312016 attempting to create duplicate // RenderViewHosts. SetupForDuplicateHosts sets up this test case and leaves // it in a state with pending RenderViewHost. Before the commit of the new // pending RenderViewHost, this test case creates a new window through the new // process. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, AttemptDuplicateRenderViewHost) { int32_t duplicate_routing_id = MSG_ROUTING_NONE; RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts( shell(), embedded_test_server(), &duplicate_routing_id); EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id); mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New(); params->target_url = GURL("about:blank"); pending_rfh->CreateNewWindow( std::move(params), base::BindOnce([](mojom::CreateNewWindowStatus, mojom::CreateNewWindowReplyPtr) {})); // If the above operation doesn't cause a crash, the test has succeeded! } // This is a test for crbug.com/312016. It tries to create two RenderWidgetHosts // with the same process and routing ids, which causes a collision. It is almost // identical to the AttemptDuplicateRenderViewHost test case. // Crashes on all platforms. http://crbug.com/939338 IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DISABLED_AttemptDuplicateRenderWidgetHost) { int duplicate_routing_id = MSG_ROUTING_NONE; RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts( shell(), embedded_test_server(), &duplicate_routing_id); EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id); // Since this test executes on the UI thread and hopping threads might cause // different timing in the test, let's simulate a CreateNewWidget call coming // from the IO thread. Use the existing window routing id to cause a // deliberate collision. pending_rfh->CreateNewPopupWidget( mojo::PendingAssociatedReceiver(), mojo::PendingAssociatedReceiver(), mojo::PendingAssociatedRemote()); // If the above operation doesn't crash, the test has succeeded! } // This is a test for crbug.com/444198. It tries to send a // FrameHostMsg_RunFileChooser containing an invalid path. The browser should // correctly terminate the renderer in these cases. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, AttemptRunFileChoosers) { TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("../../*.txt"))); TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("/etc/*.conf"))); #if defined(OS_WIN) TestFileChooserWithPath( base::FilePath(FILE_PATH_LITERAL("\\\\evilserver\\evilshare\\*.txt"))); TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("c:\\*.txt"))); TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("..\\..\\*.txt"))); #endif } // A test for crbug.com/941008. // Calling OpenFileChooser() and EnumerateChosenDirectory() for a single // FileChooser instance had a problem. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, UnexpectedMethodsSequence) { EXPECT_TRUE(NavigateToURL(shell(), GURL("http://foo.com/simple_page.html"))); RenderFrameHost* compromised_renderer = shell()->web_contents()->GetMainFrame(); auto delegate = std::make_unique(); shell()->web_contents()->SetDelegate(delegate.get()); mojo::Remote chooser = FileChooserImpl::CreateBoundForTesting( static_cast(compromised_renderer)); base::RunLoop run_loop1; base::RunLoop run_loop2; chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(), base::BindOnce(FileChooserCallback, &run_loop2)); // The following EnumerateChosenDirectory() runs the specified callback // immediately regardless of the content of the first argument FilePath. chooser->EnumerateChosenDirectory( base::FilePath(FILE_PATH_LITERAL(":*?\"<>|")), base::BindOnce(FileChooserCallback, &run_loop1)); run_loop1.Run(); delegate->Choose(base::FilePath(FILE_PATH_LITERAL("foo.txt"))); run_loop2.Run(); // The test passes if it doesn't crash. } class CorsExploitBrowserTest : public ContentBrowserTest { public: CorsExploitBrowserTest() = default; void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); SetupCrossSiteRedirector(embedded_test_server()); } private: DISALLOW_COPY_AND_ASSIGN(CorsExploitBrowserTest); }; // Test that receiving a commit with incorrect origin properly terminates the // renderer process. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginOnCommit) { GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); FrameTreeNode* root = static_cast(shell()->web_contents()) ->GetFrameTree() ->root(); // Navigate to a new URL, with an interceptor that replaces the origin with // one that does not match params.url. GURL url(embedded_test_server()->GetURL("/title2.html")); PwnCommitIPC(shell()->web_contents(), url, url, url::Origin::Create(GURL("http://bar.com/"))); // Use LoadURL, as the test shouldn't wait for navigation commit. NavigationController& controller = shell()->web_contents()->GetController(); controller.LoadURL(url, Referrer(), ui::PAGE_TRANSITION_LINK, std::string()); EXPECT_NE(nullptr, controller.GetPendingEntry()); EXPECT_EQ(url, controller.GetPendingEntry()->GetURL()); RenderProcessHostBadIpcMessageWaiter kill_waiter( root->current_frame_host()->GetProcess()); // When the IPC message is received and validation fails, the process is // terminated. However, the notification for that should be processed in a // separate task of the message loop, so ensure that the process is still // considered alive. EXPECT_TRUE( root->current_frame_host()->GetProcess()->IsInitializedAndNotDead()); EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } // Test that receiving a document.open() URL update with incorrect origin // properly terminates the renderer process. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginOnDocumentOpenURLUpdate) { GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHostImpl* rfh = static_cast( shell()->web_contents()->GetMainFrame()); // Simulate a document.open() URL update with incorrect origin. RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); static_cast(rfh)->DidOpenDocumentInputStream( embedded_test_server()->GetURL("evil.com", "/title1.html")); // Ensure that the renderer process gets killed. EXPECT_EQ(AreAllSitesIsolatedForTesting() ? bad_message::RFH_CAN_COMMIT_URL_BLOCKED : bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } // Test that same-document navigations cannot go cross-origin (even within the // same site). IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, CrossOriginSameDocumentCommit) { GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); // Do a same-document navigation to a cross-origin URL/Origin (which match // each other, unlike the MismatchedOriginOnCommit), using an interceptor that // replaces the origin and URL. This intentionally uses a cross-origin but // same-site destination, to avoid failing Site Isolation checks. GURL dest_url(embedded_test_server()->GetURL("bar.foo.com", "/title2.html")); PwnCommitIPC(shell()->web_contents(), start_url, dest_url, url::Origin::Create(dest_url)); RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetMainFrame()->GetProcess()); // ExecJs will sometimes finish before the renderer gets killed, so we must // ignore the result. ignore_result(ExecJs(shell()->web_contents()->GetMainFrame(), "history.pushState({}, '', location.href);")); EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } // Test that same-document navigations cannot go cross-origin from about:blank // (even within the same site). Uses a subframe to inherit an existing origin. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, CrossOriginSameDocumentCommitFromAboutBlank) { GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); // Create an about:blank iframe that inherits the origin. RenderFrameHost* subframe = CreateSubframe(static_cast(shell()->web_contents()), "child1", GURL(), false /* wait_for_navigation */); EXPECT_EQ(url::Origin::Create(start_url), subframe->GetLastCommittedOrigin()); // Do a same-document navigation to another about:blank URL, but using a // different origin. This intentionally uses a cross-origin but same-site // origin to avoid triggering Site Isolation checks. GURL blank_url("about:blank#foo"); GURL fake_url(embedded_test_server()->GetURL("bar.foo.com", "/")); PwnCommitIPC(shell()->web_contents(), blank_url, blank_url, url::Origin::Create(fake_url)); RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); // ExecJs will sometimes finish before the renderer gets killed, so we must // ignore the result. ignore_result(ExecJs(subframe, "location.hash='foo';")); EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } // Test that same-document navigations cannot go cross-origin (even within the // same site), in the case that allow_universal_access_from_file_urls is enabled // but the last committed origin is not a file URL. See also // RenderFrameHostManagerTest.EnsureUniversalAccessFromFileSchemeSucceeds for // the intended case that file URLs are allowed to go cross-origin. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, CrossOriginSameDocumentCommitUniversalAccessNonFile) { auto prefs = shell()->web_contents()->GetOrCreateWebPreferences(); prefs.allow_universal_access_from_file_urls = true; shell()->web_contents()->SetWebPreferences(prefs); GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); // Do a same-document navigation to a cross-origin URL, using an interceptor // that replaces the URL but not the origin (to simulate the universal access // case, but for a non-file committed origin). This intentionally uses a // cross-origin but same-site destination, to avoid failing Site Isolation // checks. GURL dest_url(embedded_test_server()->GetURL("bar.foo.com", "/title2.html")); PwnCommitIPC(shell()->web_contents(), start_url, dest_url, url::Origin::Create(start_url)); RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetMainFrame()->GetProcess()); // ExecJs will sometimes finish before the renderer gets killed, so we must // ignore the result. ignore_result(ExecJs(shell()->web_contents()->GetMainFrame(), "history.pushState({}, '', location.href);")); EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } namespace { // Interceptor that replaces |interface_params| with the specified // value for the first DidCommitProvisionalLoad message it observes in the given // |web_contents| while in scope. class ScopedInterfaceParamsReplacer : public DidCommitNavigationInterceptor { public: ScopedInterfaceParamsReplacer( WebContents* web_contents, mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override) : DidCommitNavigationInterceptor(web_contents), params_override_(std::move(params_override)) {} ~ScopedInterfaceParamsReplacer() override = default; protected: bool WillProcessDidCommitNavigation( RenderFrameHost* render_frame_host, NavigationRequest* navigation_request, mojom::DidCommitProvisionalLoadParamsPtr*, mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) override { interface_params->Swap(¶ms_override_); return true; } private: mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override_; DISALLOW_COPY_AND_ASSIGN(ScopedInterfaceParamsReplacer); }; } // namespace // Test that, as a general rule, not receiving new // DidCommitProvisionalLoadInterfaceParamsPtr for a cross-document navigation // properly terminates the renderer process. There is one exception to this // rule, see: RenderFrameHostImplBrowserTest. // InterfaceProviderRequestIsOptionalForFirstCommit. // TODO(crbug.com/718652): when all clients are converted to use // BrowserInterfaceBroker, PendingReceiver-related code will // be removed. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MissingInterfaceProviderOnNonSameDocumentCommit) { const GURL start_url(embedded_test_server()->GetURL("/title1.html")); const GURL non_same_document_url( embedded_test_server()->GetURL("/title2.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHostImpl* frame = static_cast( shell()->web_contents()->GetMainFrame()); RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess()); NavigationHandleObserver navigation_observer(shell()->web_contents(), non_same_document_url); ScopedInterfaceParamsReplacer replacer(shell()->web_contents(), nullptr); EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), non_same_document_url)); EXPECT_EQ(bad_message::RFH_INTERFACE_PROVIDER_MISSING, kill_waiter.Wait()); // Verify that the death of the renderer process doesn't leave behind and // leak NavigationRequests - see https://crbug.com/869193. EXPECT_FALSE(frame->HasPendingCommitNavigation()); EXPECT_FALSE(navigation_observer.has_committed()); EXPECT_TRUE(navigation_observer.is_error()); EXPECT_TRUE(navigation_observer.last_committed_url().is_empty()); EXPECT_EQ(net::OK, navigation_observer.net_error_code()); } // Test that a compromised renderer cannot ask to upload an arbitrary file in // OpenURL. This is a regression test for https://crbug.com/726067. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, OpenUrl_ResourceRequestBody) { GURL start_url(embedded_test_server()->GetURL("/title1.html")); GURL target_url(embedded_test_server()->GetURL("/echoall")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); FrameTreeNode* root = static_cast(shell()->web_contents()) ->GetFrameTree() ->root(); RenderProcessHostBadIpcMessageWaiter kill_waiter( root->current_frame_host()->GetProcess()); // Prepare a file to upload. base::ScopedAllowBlockingForTesting allow_blocking; base::ScopedTempDir temp_dir; base::FilePath file_path; std::string file_content("test-file-content"); ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path)); ASSERT_TRUE(base::WriteFile(file_path, file_content)); // Simulate an OpenURL Mojo method asking to POST a file that the renderer // shouldn't have access to. auto params = CreateOpenURLParams(target_url); params->post_body = new network::ResourceRequestBody; params->post_body->AppendFileRange(file_path, 0, file_content.size(), base::Time()); params->should_replace_current_entry = true; static_cast(root->current_frame_host()) ->OpenURL(std::move(params)); // Verify that the malicious navigation did not commit the navigation to // |target_url|. EXPECT_EQ(start_url, root->current_frame_host()->GetLastCommittedURL()); // Verify that the malicious renderer got killed. EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait()); } // Forging a navigation commit after the initial empty document will result in a // renderer kill, even if the URL used is about:blank. // See https://crbug.com/766262 for an example advanced case that involves // forging a frame's unique name. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, NonInitialAboutBlankRendererKill) { // Navigate normally. GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); RenderFrameHostImpl* rfh = static_cast( shell()->web_contents()->GetMainFrame()); // Simulate an about:blank commit without a NavigationRequest. It will fail // because only initial commits are allowed to do this. auto params = mojom::DidCommitProvisionalLoadParams::New(); params->did_create_new_entry = false; params->url = GURL("about:blank"); params->referrer = blink::mojom::Referrer::New(); params->transition = ui::PAGE_TRANSITION_LINK; params->should_update_history = false; params->gesture = NavigationGestureAuto; params->method = "GET"; params->page_state = blink::PageState::CreateFromURL(GURL("about:blank")); params->origin = url::Origin::Create(GURL("about:blank")); params->embedding_token = base::UnguessableToken::Create(); RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); static_cast(rfh)->DidCommitProvisionalLoad( std::move(params), mojom::DidCommitProvisionalLoadInterfaceParams::New( mojo::PendingRemote() .InitWithNewPipeAndPassReceiver())); // Verify that the malicious renderer got killed. EXPECT_EQ(bad_message::RFH_NO_MATCHING_NAVIGATION_REQUEST_ON_COMMIT, kill_waiter.Wait()); } class SecurityExploitBrowserTestMojoBlobURLs : public SecurityExploitBrowserTest { public: SecurityExploitBrowserTestMojoBlobURLs() = default; void TearDown() override { storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(nullptr); } }; // Check that when site isolation is enabled, an origin can't create a blob URL // for a different origin. Similar to the test above, but checks the // mojo-based Blob URL implementation. See https://crbug.com/886976. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestMojoBlobURLs, CreateMojoBlobURLInDifferentOrigin) { IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); RenderFrameHost* rfh = shell()->web_contents()->GetMainFrame(); // Intercept future blob URL registrations and overwrite the blob URL origin // with b.com. std::string target_origin = "http://b.com"; std::string blob_path = "5881f76e-10d2-410d-8c61-ef210502acfd"; auto intercept_hook = base::BindRepeating(&BlobURLStoreInterceptor::Intercept, GURL("blob:" + target_origin + "/" + blob_path)); storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(&intercept_hook); // Register a blob URL from the a.com main frame, which will go through the // interceptor above and be rewritten to register the blob URL with the b.com // origin. This should result in a kill because a.com should not be allowed // to create blob URLs outside of its own origin. content::RenderProcessHostBadMojoMessageWaiter crash_observer( rfh->GetProcess()); // The renderer should always get killed, but sometimes ExecuteScript returns // true anyway, so just ignore the result. ignore_result(ExecJs(rfh, "URL.createObjectURL(new Blob(['foo']))")); // If the process is killed, this test passes. EXPECT_EQ( "Received bad user message: " "Non committable URL passed to BlobURLStore::Register", crash_observer.Wait()); } // Check that with site isolation enabled, an origin can't create a filesystem // URL for a different origin. See https://crbug.com/888001. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, CreateFilesystemURLInDifferentOrigin) { IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); GURL main_url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); RenderFrameHost* rfh = shell()->web_contents()->GetMainFrame(); // Block the renderer on operation that never completes, to shield it from // receiving unexpected browser->renderer IPCs that might CHECK. rfh->ExecuteJavaScriptWithUserGestureForTests( u"var r = new XMLHttpRequest();" u"r.open('GET', '/slow?99999', false);" u"r.send(null);" u"while (1);"); // Set up a blob ID and populate it with attacker-controlled value. This // is just using the blob APIs directly since creating arbitrary blobs is not // what is prohibited; this data is not in any origin. std::string payload = "pwned."; std::string payload_type = "text/html"; std::unique_ptr blob = CreateMemoryBackedBlob( rfh->GetSiteInstance()->GetBrowserContext(), payload, payload_type); std::string blob_id = blob->GetUUID(); // Target a different origin. std::string target_origin = "http://b.com"; GURL target_url = GURL("filesystem:" + target_origin + "/temporary/exploit.html"); // Note: a well-behaved renderer would always call Open first before calling // Create and Write, but it's actually not necessary for the original attack // to succeed, so we omit it. As a result there are some log warnings from the // quota observer. PwnMessageHelper::FileSystemCreate(rfh->GetProcess(), 23, target_url, false, false, false); // Write the blob into the file. If successful, this places an // attacker-controlled value in a resource on the target origin. PwnMessageHelper::FileSystemWrite(rfh->GetProcess(), 24, target_url, blob_id, 0); // Now navigate to |target_url| in a subframe. It should not succeed, and the // subframe should not contain |payload|. TestNavigationObserver observer(shell()->web_contents()); FrameTreeNode* root = static_cast(shell()->web_contents()) ->GetFrameTree() ->root(); NavigateFrameToURL(root->child_at(0), target_url); EXPECT_FALSE(observer.last_navigation_succeeded()); EXPECT_EQ(net::ERR_FILE_NOT_FOUND, observer.last_net_error_code()); RenderFrameHost* attacked_rfh = root->child_at(0)->current_frame_host(); std::string body = EvalJs(attacked_rfh, "document.body.innerText").ExtractString(); EXPECT_TRUE(base::StartsWith(body, "Could not load the requested resource", base::CompareCase::INSENSITIVE_ASCII)) << " body=" << body; } // Verify that when a compromised renderer tries to navigate a remote frame to // a disallowed URL (e.g., file URL), that navigation is blocked. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, BlockIllegalOpenURLFromRemoteFrame) { // Explicitly isolating a.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("a.com"); GURL main_url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); FrameTreeNode* root = static_cast(shell()->web_contents()) ->GetFrameTree() ->root(); FrameTreeNode* child = root->child_at(0); // Simulate an IPC message where the top frame asks the remote subframe to // navigate to a file: URL. SiteInstance* a_com_instance = root->current_frame_host()->GetSiteInstance(); RenderFrameProxyHost* proxy = child->render_manager()->GetRenderFrameProxyHost(a_com_instance); EXPECT_TRUE(proxy); TestNavigationObserver observer(shell()->web_contents()); static_cast(proxy->frame_tree_node()->current_frame_host()) ->OpenURL(CreateOpenURLParams(GURL("file:///"))); observer.Wait(); // Verify that the malicious navigation was blocked. Currently, this happens // by rewriting the target URL to about:blank#blocked. // // TODO(alexmos): Consider killing the renderer process in this case, since // this security check is already enforced in the renderer process. EXPECT_EQ(GURL(kBlockedURL), child->current_frame_host()->GetLastCommittedURL()); // Navigate to the starting page again to recreate the proxy, then try the // same malicious navigation with a chrome:// URL. EXPECT_TRUE(NavigateToURL(shell(), main_url)); child = root->child_at(0); proxy = child->render_manager()->GetRenderFrameProxyHost(a_com_instance); EXPECT_TRUE(proxy); TestNavigationObserver observer_2(shell()->web_contents()); GURL chrome_url(std::string(kChromeUIScheme) + "://" + std::string(kChromeUIGpuHost)); static_cast(proxy->frame_tree_node()->current_frame_host()) ->OpenURL(CreateOpenURLParams(chrome_url)); observer_2.Wait(); EXPECT_EQ(GURL(kBlockedURL), child->current_frame_host()->GetLastCommittedURL()); } class RemoteFrameHostInterceptor : public blink::mojom::RemoteFrameHostInterceptorForTesting { public: explicit RemoteFrameHostInterceptor( RenderFrameProxyHost* render_frame_proxy_host, const std::u16string& evil_origin) : render_frame_proxy_host_(render_frame_proxy_host), evil_origin_(evil_origin) { render_frame_proxy_host_->frame_host_receiver_for_testing() .SwapImplForTesting(this); } RemoteFrameHost* GetForwardingInterface() override { return render_frame_proxy_host_; } void RouteMessageEvent( const absl::optional& source_frame_token, const std::u16string& source_origin, const std::u16string& target_origin, blink::TransferableMessage message) override { // Forward the message to the actual RFPH replacing |source_origin| with the // "evil origin" as especified in SetEvilSourceOriginAndWaitForMessage(). GetForwardingInterface()->RouteMessageEvent( std::move(source_frame_token), std::move(evil_origin_), std::move(target_origin), std::move(message)); } void OpenURL(blink::mojom::OpenURLParamsPtr params) override { intercepted_params_ = std::move(params); } blink::mojom::OpenURLParamsPtr GetInterceptedParams() { return std::move(intercepted_params_); } private: RenderFrameProxyHost* render_frame_proxy_host_; std::u16string evil_origin_; blink::mojom::OpenURLParamsPtr intercepted_params_; }; // Test verifying that a compromised renderer can't lie about the source_origin // passed along with the RouteMessageEvent() mojo message. See also // https://crbug.com/915721. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PostMessageSourceOrigin) { // Explicitly isolating a.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("b.com"); // Navigate to a page with an OOPIF. GURL main_url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); // Sanity check of test setup: main frame and subframe should be isolated. WebContents* web_contents = shell()->web_contents(); RenderFrameHost* main_frame = web_contents->GetMainFrame(); RenderFrameHost* subframe = web_contents->GetAllFrames()[1]; EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess()); // We need to get ahold of the RenderFrameProxyHost representing the main // frame for the subframe's process, to install the mojo interceptor. FrameTreeNode* main_frame_node = static_cast(shell()->web_contents()) ->GetFrameTree() ->root(); FrameTreeNode* subframe_node = main_frame_node->child_at(0); SiteInstance* b_com_instance = subframe_node->current_frame_host()->GetSiteInstance(); RenderFrameProxyHost* main_frame_proxy_host = main_frame_node->render_manager()->GetRenderFrameProxyHost( b_com_instance); // Prepare to intercept the RouteMessageEvent IPC message that will come // from the subframe process. url::Origin invalid_origin = web_contents->GetMainFrame()->GetLastCommittedOrigin(); std::u16string evil_source_origin = base::UTF8ToUTF16(invalid_origin.Serialize()); RemoteFrameHostInterceptor mojo_interceptor(main_frame_proxy_host, evil_source_origin); // Post a message from the subframe to the cross-site parent and intercept the // associated IPC message, changing it to simulate a compromised subframe // renderer lying that the |source_origin| of the postMessage is the origin of // the parent (not of the subframe). RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')")); EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_SOURCE_ORIGIN, kill_waiter.Wait()); } IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, InvalidRemoteNavigationInitiator) { // Explicitly isolating a.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("a.com"); // Navigate to a test page where the subframe is cross-site (and because of // IsolateOrigin call above in a separate process) from the main frame. GURL main_url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); RenderFrameHostImpl* main_frame = static_cast( shell()->web_contents()->GetMainFrame()); RenderProcessHost* main_process = main_frame->GetProcess(); EXPECT_EQ(2u, shell()->web_contents()->GetAllFrames().size()); RenderFrameHost* subframe = shell()->web_contents()->GetAllFrames()[1]; RenderProcessHost* subframe_process = subframe->GetProcess(); EXPECT_NE(main_process->GetID(), subframe_process->GetID()); // Prepare to intercept OpenURL Mojo message that will come from // the main frame. FrameTreeNode* main_frame_node = static_cast(shell()->web_contents()) ->GetFrameTree() ->root(); FrameTreeNode* child_node = main_frame_node->child_at(0); SiteInstance* a_com_instance = main_frame_node->current_frame_host()->GetSiteInstance(); RenderFrameProxyHost* proxy = child_node->render_manager()->GetRenderFrameProxyHost(a_com_instance); RemoteFrameHostInterceptor interceptor(proxy, std::u16string()); // Have the main frame request navigation in the "remote" subframe. This will // result in OpenURL Mojo message being sent to the RenderFrameProxyHost. EXPECT_TRUE(ExecJs(shell()->web_contents()->GetMainFrame(), "window.frames[0].location = '/title1.html';")); // Change the intercepted message to simulate a compromised subframe renderer // lying that the |initiator_origin| is the origin of the |subframe|. auto evil_params = interceptor.GetInterceptedParams(); evil_params->initiator_origin = subframe->GetLastCommittedOrigin(); // Inject the invalid IPC and verify that the renderer gets terminated. RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); static_cast(main_frame)->OpenURL(std::move(evil_params)); EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait()); } class BeginNavigationInitiatorReplacer : public FrameHostInterceptor { public: BeginNavigationInitiatorReplacer( WebContents* web_contents, absl::optional initiator_to_inject) : FrameHostInterceptor(web_contents), initiator_to_inject_(initiator_to_inject) {} bool WillDispatchBeginNavigation( RenderFrameHost* render_frame_host, blink::mojom::CommonNavigationParamsPtr* common_params, blink::mojom::BeginNavigationParamsPtr* begin_params, mojo::PendingRemote* blob_url_token, mojo::PendingAssociatedRemote* navigation_client) override { if (is_activated_) { (*common_params)->initiator_origin = initiator_to_inject_; is_activated_ = false; } return true; } void Activate() { is_activated_ = true; } private: absl::optional initiator_to_inject_; bool is_activated_ = false; DISALLOW_COPY_AND_ASSIGN(BeginNavigationInitiatorReplacer); }; IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, InvalidBeginNavigationInitiator) { WebContentsImpl* web_contents = static_cast(shell()->web_contents()); // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. BeginNavigationInitiatorReplacer injector( web_contents, url::Origin::Create(GURL("http://b.com"))); // Explicitly isolating a.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("a.com"); // Navigate to a test page that will be locked to a.com. GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(web_contents, main_url)); // Start monitoring for renderer kills. RenderProcessHost* main_process = web_contents->GetMainFrame()->GetProcess(); RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); // Have the main frame navigate and lie that the initiator origin is b.com. injector.Activate(); // Don't expect a response for the script, as the process may be killed // before the script sends its completion message. ExecuteScriptAsync(web_contents, "window.location = '/title2.html';"); // Verify that the renderer was terminated. EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait()); } IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MissingBeginNavigationInitiator) { // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. WebContents* web_contents = shell()->web_contents(); BeginNavigationInitiatorReplacer injector(web_contents, absl::nullopt); // Navigate to a test page. GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(web_contents, main_url)); // Start monitoring for renderer kills. RenderProcessHost* main_process = web_contents->GetMainFrame()->GetProcess(); RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); // Have the main frame submit a BeginNavigation IPC with a missing initiator. injector.Activate(); // Don't expect a response for the script, as the process may be killed // before the script sends its completion message. ExecuteScriptAsync(web_contents, "window.location = '/title2.html';"); // Verify that the renderer was terminated. EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_MISSING_INITIATOR_ORIGIN, kill_waiter.Wait()); } namespace { // An interceptor class that allows replacing the URL of the commit IPC from // the renderer process to the browser process. class DidCommitUrlReplacer : public DidCommitNavigationInterceptor { public: DidCommitUrlReplacer(WebContents* web_contents, const GURL& replacement_url) : DidCommitNavigationInterceptor(web_contents), replacement_url_(replacement_url) {} ~DidCommitUrlReplacer() override = default; protected: bool WillProcessDidCommitNavigation( RenderFrameHost* render_frame_host, NavigationRequest* navigation_request, mojom::DidCommitProvisionalLoadParamsPtr* params, mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) override { (**params).url = replacement_url_; return true; } private: GURL replacement_url_; DISALLOW_COPY_AND_ASSIGN(DidCommitUrlReplacer); }; } // namespace // Test which verifies that when an exploited renderer process sends a commit // message with URL that the process is not allowed to commit. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DidCommitInvalidURL) { // Explicitly isolating foo.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("foo.com"); RenderFrameDeletedObserver initial_frame_deleted_observer( shell()->web_contents()->GetMainFrame()); // Test assumes the initial RenderFrameHost to be deleted. Disable // back-forward cache to ensure that it doesn't get preserved in the cache. DisableBackForwardCacheForTesting(shell()->web_contents(), BackForwardCache::TEST_ASSUMES_NO_CACHING); // Navigate to foo.com initially. GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), foo_url)); // Wait for the RenderFrameHost which was current before the navigation to // foo.com to be deleted. This is necessary, since on a slow system the // UnloadACK event can arrive after the DidCommitUrlReplacer instance below // is created. The replacer code has checks to ensure that all frames being // deleted it has seen being created, which with delayed UnloadACK is // violated. initial_frame_deleted_observer.WaitUntilDeleted(); // Create the interceptor object which will replace the URL of the subsequent // navigation with bar.com based URL. GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html")); DidCommitUrlReplacer url_replacer(shell()->web_contents(), bar_url); // Navigate to another URL within foo.com, which would usually be committed // successfully, but when the URL is modified it should result in the // termination of the renderer process. RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetMainFrame()->GetProcess()); EXPECT_FALSE(NavigateToURL( shell(), embedded_test_server()->GetURL("foo.com", "/title2.html"))); EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait()); } // Test which verifies that when an exploited renderer process sends a commit // message with URL that the process is not allowed to commit. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DidCommitInvalidURLWithOpaqueOrigin) { // Explicitly isolating foo.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("foo.com"); RenderFrameDeletedObserver initial_frame_deleted_observer( shell()->web_contents()->GetMainFrame()); // Test assumes the initial RenderFrameHost to be deleted. Disable // back-forward cache to ensure that it doesn't get preserved in the cache. DisableBackForwardCacheForTesting(shell()->web_contents(), BackForwardCache::TEST_ASSUMES_NO_CACHING); // Navigate to foo.com initially. GURL foo_url(embedded_test_server()->GetURL("foo.com", "/page_with_blank_iframe.html")); EXPECT_TRUE(NavigateToURL(shell(), foo_url)); // Wait for the RenderFrameHost which was current before the navigation to // foo.com to be deleted. This is necessary, since on a slow system the // UnloadACK event can arrive after the DidCommitUrlReplacer instance below // is created. The replacer code has checks to ensure that all frames being // deleted it has seen being created, which with delayed UnloadACK is // violated. initial_frame_deleted_observer.WaitUntilDeleted(); // Create the interceptor object which will replace the URL of the subsequent // navigation with bar.com based URL. GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html")); DidCommitUrlReplacer url_replacer(shell()->web_contents(), bar_url); // Navigate the subframe to a data URL, which would usually be committed // successfully in the same process as foo.com, but when the URL is modified // it should result in the termination of the renderer process. RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetMainFrame()->GetProcess()); GURL data_url(R"(data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E)"); EXPECT_TRUE( NavigateIframeToURL(shell()->web_contents(), "test_iframe", data_url)); EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait()); } // Test which verifies that a WebUI process cannot send a commit message with // URL for a web document. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, WebUIProcessDidCommitWebURL) { // Navigate to a WebUI document. GURL webui_url(GetWebUIURL(kChromeUIGpuHost)); EXPECT_TRUE(NavigateToURL(shell(), webui_url)); // Create the interceptor object which will replace the URL of the subsequent // navigation with |web_url|. GURL web_url(embedded_test_server()->GetURL("foo.com", "/title3.html")); DidCommitUrlReplacer url_replacer(shell()->web_contents(), web_url); // Navigate to another URL within the WebUI, which would usually be committed // successfully, but when the URL is modified it should result in the // termination of the renderer process. RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetMainFrame()->GetProcess()); GURL second_webui_url(webui_url.Resolve("/foo")); EXPECT_FALSE(NavigateToURL(shell(), second_webui_url)); EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait()); } // Test that verifies that if a RenderFrameHost is incorrectly given WebUI // bindings, committing a non-WebUI URL in it is detected and the process is // correctly terminated. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DidCommitNonWebUIURLInProcessWithBindings) { // Navigate to a web URL. GURL initial_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), initial_url)); // Start a second navigation. GURL web_url(embedded_test_server()->GetURL("foo.com", "/title2.html")); TestNavigationManager navigation(shell()->web_contents(), web_url); RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetMainFrame()->GetProcess()); shell()->LoadURL(web_url); EXPECT_TRUE(navigation.WaitForResponse()); // Grant WebUI bindings to the navigated frame to simulate a bug in the code // that incorrectly does it for a navigation that does not require it. navigation.GetNavigationHandle()->GetRenderFrameHost()->AllowBindings( BINDINGS_POLICY_WEB_UI); // Resume the navigation and upon receiving the commit message the renderer // process will be terminated. navigation.ResumeNavigation(); EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait()); } // Tests that a web page cannot bind to a WebUI interface if a WebUI page is the // currently committed RenderFrameHost in the tab (https://crbug.com/1225929). IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, BindToWebUIFromWebViaMojo) { // Navigate to a non-privileged web page, and simulate a renderer compromise // by granting MojoJS. GURL web_url(embedded_test_server()->GetURL("a.com", "/title1.html")); TestNavigationManager navigation(shell()->web_contents(), web_url); shell()->LoadURL(web_url); EXPECT_TRUE(navigation.WaitForResponse()); RenderFrameHostImpl* main_frame = static_cast( shell()->web_contents()->GetMainFrame()); main_frame->GetFrameBindingsControl()->EnableMojoJsBindings(); navigation.WaitForNavigationFinished(); // Open a popup so that the process won't exit on its own when leaving. OpenBlankWindow(static_cast(shell()->web_contents())); // When the page unloads (after the cross-process navigation to an actual // WebUI page below), try to bind to a WebUI interface from the web // RenderFrameHost. Ensure the unload timer and bfcache are disabled so that // the handler has a chance to run. main_frame->DisableUnloadTimerForTesting(); DisableBackForwardCacheForTesting(shell()->web_contents(), BackForwardCache::TEST_USES_UNLOAD_EVENT); ASSERT_TRUE(ExecJs(main_frame, R"( // Intentionally leak pipe as a global so it doesn't get GCed. newMessagePipe = Mojo.createMessagePipe(); onunload = function () { Mojo.bindInterface('mojom.ProcessInternalsHandler', newMessagePipe.handle0); }; )")); // Now navigate to a WebUI page and expect the previous renderer process to be // killed when asking to bind to the WebUI interface. GURL webui_url( GetWebUIURL(kChromeUIProcessInternalsHost).Resolve("#general")); RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess()); EXPECT_TRUE(NavigateToURL(shell(), webui_url)); // Verify that the previous renderer was terminated. EXPECT_EQ(bad_message::RFH_INVALID_WEB_UI_CONTROLLER, kill_waiter.Wait()); } class BeginNavigationTransitionReplacer : public FrameHostInterceptor { public: BeginNavigationTransitionReplacer(WebContents* web_contents, ui::PageTransition transition_to_inject) : FrameHostInterceptor(web_contents), transition_to_inject_(transition_to_inject) {} bool WillDispatchBeginNavigation( RenderFrameHost* render_frame_host, blink::mojom::CommonNavigationParamsPtr* common_params, blink::mojom::BeginNavigationParamsPtr* begin_params, mojo::PendingRemote* blob_url_token, mojo::PendingAssociatedRemote* navigation_client) override { if (is_activated_) { (*common_params)->transition = transition_to_inject_; is_activated_ = false; } return true; } void Activate() { is_activated_ = true; } private: ui::PageTransition transition_to_inject_; bool is_activated_ = false; DISALLOW_COPY_AND_ASSIGN(BeginNavigationTransitionReplacer); }; IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, NonWebbyTransition) { const ui::PageTransition test_cases[] = { ui::PAGE_TRANSITION_TYPED, ui::PAGE_TRANSITION_AUTO_BOOKMARK, ui::PAGE_TRANSITION_GENERATED, ui::PAGE_TRANSITION_AUTO_TOPLEVEL, ui::PAGE_TRANSITION_RELOAD, ui::PAGE_TRANSITION_KEYWORD, ui::PAGE_TRANSITION_KEYWORD_GENERATED}; for (ui::PageTransition transition : test_cases) { // Prepare to intercept BeginNavigation mojo IPC. This has to be done // before the test creates the RenderFrameHostImpl that is the target of the // IPC. WebContents* web_contents = shell()->web_contents(); BeginNavigationTransitionReplacer injector(web_contents, transition); // Navigate to a test page. GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(web_contents, main_url)); // Start monitoring for renderer kills. RenderProcessHost* main_process = web_contents->GetMainFrame()->GetProcess(); RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); // Have the main frame submit a BeginNavigation IPC with a missing // initiator. injector.Activate(); // Don't expect a response for the script, as the process may be killed // before the script sends its completion message. ExecuteScriptAsync(web_contents, "window.location = '/title2.html';"); // Verify that the renderer was terminated. EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_NON_WEBBY_TRANSITION, kill_waiter.Wait()); } } class SecurityExploitViaDisabledWebSecurityTest : public SecurityExploitBrowserTest { public: SecurityExploitViaDisabledWebSecurityTest() { // To get around BlockedSchemeNavigationThrottle. Other attempts at getting // around it don't work, i.e.: // -if the request is made in a child frame then the frame is torn down // immediately on process killing so the navigation doesn't complete // -if it's classified as same document, then a DCHECK in // NavigationRequest::CreateRendererInitiated fires feature_list_.InitAndEnableFeature( features::kAllowContentInitiatedDataUrlNavigations); } protected: void SetUpCommandLine(base::CommandLine* command_line) override { // Simulate a compromised renderer, otherwise the cross-origin request to // file: is blocked. command_line->AppendSwitch(switches::kDisableWebSecurity); SecurityExploitBrowserTest::SetUpCommandLine(command_line); } private: base::test::ScopedFeatureList feature_list_; }; // Test to verify that an exploited renderer process trying to specify a // non-empty URL for base_url_for_data_url on navigation is correctly // terminated. IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest, ValidateBaseUrlForDataUrl) { GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHostImpl* rfh = static_cast( shell()->web_contents()->GetMainFrame()); GURL data_url("data:text/html,foo"); base::FilePath file_path = GetTestFilePath("", "simple_page.html"); GURL file_url = net::FilePathToFileURL(file_path); // Setup a BeginNavigate IPC with non-empty base_url_for_data_url. blink::mojom::CommonNavigationParamsPtr common_params = blink::mojom::CommonNavigationParams::New( data_url, url::Origin::Create(data_url), blink::mojom::Referrer::New(), ui::PAGE_TRANSITION_LINK, blink::mojom::NavigationType::DIFFERENT_DOCUMENT, blink::NavigationDownloadPolicy(), false /* should_replace_current_entry */, file_url, /* base_url_for_data_url */ GURL() /* history_url_for_data_url */, blink::PreviewsTypes::PREVIEWS_UNSPECIFIED, base::TimeTicks::Now() /* navigation_start */, "GET", nullptr /* post_data */, network::mojom::SourceLocation::New(), false /* started_from_context_menu */, false /* has_user_gesture */, false /* text_fragment_token */, network::mojom::CSPDisposition::CHECK, std::vector() /* initiator_origin_trial_features */, std::string() /* href_translate */, false /* is_history_navigation_in_new_child_frame */, base::TimeTicks() /* input_start */); blink::mojom::BeginNavigationParamsPtr begin_params = blink::mojom::BeginNavigationParams::New( absl::nullopt /* initiator_frame_token */, std::string() /* headers */, net::LOAD_NORMAL, false /* skip_service_worker */, blink::mojom::RequestContextType::LOCATION, network::mojom::RequestDestination::kDocument, blink::mojom::MixedContentContextType::kBlockable, false /* is_form_submission */, false /* was_initiated_by_link_click */, GURL() /* searchable_form_url */, std::string() /* searchable_form_encoding */, GURL() /* client_side_redirect_url */, absl::nullopt /* devtools_initiator_info */, nullptr /* trust_token_params */, absl::nullopt /* impression */, base::TimeTicks() /* renderer_before_unload_start */, base::TimeTicks() /* renderer_before_unload_end */, absl::nullopt /* web_bundle_token */); // Receiving the invalid IPC message should lead to renderer process // termination. RenderProcessHostBadIpcMessageWaiter process_kill_waiter(rfh->GetProcess()); mojo::PendingAssociatedRemote navigation_client; auto navigation_client_receiver = navigation_client.InitWithNewEndpointAndPassReceiver(); rfh->frame_host_receiver_for_testing().impl()->BeginNavigation( std::move(common_params), std::move(begin_params), mojo::NullRemote(), std::move(navigation_client), mojo::NullRemote()); EXPECT_EQ(bad_message::RFH_BASE_URL_FOR_DATA_URL_SPECIFIED, process_kill_waiter.Wait()); EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile( rfh->GetProcess()->GetID(), file_path)); // Reload the page to create another renderer process. TestNavigationObserver tab_observer(shell()->web_contents(), 1); shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false); tab_observer.Wait(); // Make an XHR request to check if the page has access. std::string script = base::StringPrintf( "var xhr = new XMLHttpRequest()\n" "xhr.open('GET', '%s', false);\n" "try { xhr.send(); } catch (e) {}\n" "xhr.responseText;", file_url.spec().c_str()); std::string result = EvalJs(shell()->web_contents(), script).ExtractString(); EXPECT_TRUE(result.empty()); } // Tests what happens when a web renderer asks to begin navigating to a file // url. IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest, WebToFileNavigation) { // Navigate to a web page. GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); // Have the webpage attempt to open a window with a file URL. // // Note that such attempt would normally be blocked in the renderer ("Not // allowed to load local resource: file:///..."), but the test here simulates // a compromised renderer by using --disable-web-security cmdline flag. GURL file_url = GetTestUrl("", "simple_page.html"); WebContentsAddedObserver new_window_observer; TestNavigationObserver nav_observer(nullptr); nav_observer.StartWatchingNewWebContents(); ASSERT_TRUE(ExecJs(shell()->web_contents(), JsReplace("window.open($1, '_blank')", file_url))); WebContents* new_window = new_window_observer.GetWebContents(); nav_observer.WaitForNavigationFinished(); // Verify that the navigation got blocked. EXPECT_TRUE(nav_observer.last_navigation_succeeded()); EXPECT_EQ(GURL(kBlockedURL), nav_observer.last_navigation_url()); EXPECT_EQ(GURL(kBlockedURL), new_window->GetMainFrame()->GetLastCommittedURL()); EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetLastCommittedOrigin(), new_window->GetMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetProcess(), new_window->GetMainFrame()->GetProcess()); // Even though the navigation is blocked, we expect the opener relationship to // be established between the 2 windows. EXPECT_EQ(true, ExecJs(new_window, "!!window.opener")); } // Tests what happens when a web renderer asks to begin navigating to a // view-source url. IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest, WebToViewSourceNavigation) { // Navigate to a web page. GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); // Have the webpage attempt to open a window with a view-source URL. // // Note that such attempt would normally be blocked in the renderer ("Not // allowed to load local resource: view-source:///..."), but the test here // simulates a compromised renderer by using --disable-web-security flag. base::FilePath file_path = GetTestFilePath("", "simple_page.html"); GURL view_source_url = GURL(std::string(kViewSourceScheme) + ":" + start_url.spec()); WebContentsAddedObserver new_window_observer; TestNavigationObserver nav_observer(nullptr); nav_observer.StartWatchingNewWebContents(); ASSERT_TRUE(ExecJs(shell()->web_contents(), JsReplace("window.open($1, '_blank')", view_source_url))); WebContents* new_window = new_window_observer.GetWebContents(); nav_observer.WaitForNavigationFinished(); // Verify that the navigation got blocked. EXPECT_TRUE(nav_observer.last_navigation_succeeded()); EXPECT_EQ(GURL(kBlockedURL), nav_observer.last_navigation_url()); EXPECT_EQ(GURL(kBlockedURL), new_window->GetMainFrame()->GetLastCommittedURL()); EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetLastCommittedOrigin(), new_window->GetMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetProcess(), new_window->GetMainFrame()->GetProcess()); // Even though the navigation is blocked, we expect the opener relationship to // be established between the 2 windows. EXPECT_EQ(true, ExecJs(new_window, "!!window.opener")); } class BeginNavigationTrustTokenParamsReplacer : public FrameHostInterceptor { public: BeginNavigationTrustTokenParamsReplacer( WebContents* web_contents, network::mojom::TrustTokenParamsPtr params_to_inject) : FrameHostInterceptor(web_contents), params_to_inject_(std::move(params_to_inject)) {} BeginNavigationTrustTokenParamsReplacer( const BeginNavigationTrustTokenParamsReplacer&) = delete; BeginNavigationTrustTokenParamsReplacer& operator=( const BeginNavigationTrustTokenParamsReplacer&) = delete; bool WillDispatchBeginNavigation( RenderFrameHost* render_frame_host, blink::mojom::CommonNavigationParamsPtr* common_params, blink::mojom::BeginNavigationParamsPtr* begin_params, mojo::PendingRemote* blob_url_token, mojo::PendingAssociatedRemote* navigation_client) override { if (is_activated_) { (*begin_params)->trust_token_params = params_to_inject_.Clone(); is_activated_ = false; } return true; } void Activate() { is_activated_ = true; } private: network::mojom::TrustTokenParamsPtr params_to_inject_; bool is_activated_ = false; }; class SecurityExploitBrowserTestWithTrustTokensEnabled : public SecurityExploitBrowserTest { public: SecurityExploitBrowserTestWithTrustTokensEnabled() { feature_list_.InitAndEnableFeature(network::features::kTrustTokens); } private: base::test::ScopedFeatureList feature_list_; }; // Test that the browser correctly reports a bad message when a child frame // attempts to navigate with a Trust Tokens redemption operation associated with // the navigation, but its parent lacks the trust-token-redemption Permissions // Policy feature. IN_PROC_BROWSER_TEST_F( SecurityExploitBrowserTestWithTrustTokensEnabled, BrowserForbidsTrustTokenRedemptionWithoutPermissionsPolicy) { WebContents* web_contents = shell()->web_contents(); // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. auto params = network::mojom::TrustTokenParams::New(); params->type = network::mojom::TrustTokenOperationType::kRedemption; BeginNavigationTrustTokenParamsReplacer replacer(web_contents, std::move(params)); GURL start_url(embedded_test_server()->GetURL( "/page-with-trust-token-permissions-policy-disabled.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHost* parent = web_contents->GetMainFrame(); ASSERT_FALSE(parent->IsFeatureEnabled( blink::mojom::PermissionsPolicyFeature::kTrustTokenRedemption)); RenderFrameHost* child = static_cast(web_contents) ->GetFrameTree() ->root() ->child_at(0) ->current_frame_host(); RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess()); replacer.Activate(); // Note: this can't use NavigateFrameToURL, because that method doesn't // route through RFHI::BeginNavigation. It also can't use NavigateIframeToURL, // because that navigation will hang. // // It also can't EXPECT_TRUE or EXPECT_FALSE: sometimes the ExecJs call will // finish before the renderer gets killed, and sometimes it won't. ignore_result( ExecJs(child, JsReplace("location.href=$1;", GURL("/title2.html")))); EXPECT_THAT(kill_waiter.Wait(), Optional(HasSubstr("Permissions Policy feature is absent"))); } // Test that the browser correctly reports a bad message when a child frame // attempts to navigate with a Trust Tokens signing operation associated with // the navigation, but its parent lacks the trust-token-redemption (sic) // Permissions Policy feature. IN_PROC_BROWSER_TEST_F( SecurityExploitBrowserTestWithTrustTokensEnabled, BrowserForbidsTrustTokenSigningWithoutPermissionsPolicy) { WebContents* web_contents = shell()->web_contents(); // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. auto params = network::mojom::TrustTokenParams::New(); params->type = network::mojom::TrustTokenOperationType::kSigning; BeginNavigationTrustTokenParamsReplacer replacer(web_contents, std::move(params)); GURL start_url(embedded_test_server()->GetURL( "/page-with-trust-token-permissions-policy-disabled.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHost* parent = web_contents->GetMainFrame(); ASSERT_FALSE(parent->IsFeatureEnabled( blink::mojom::PermissionsPolicyFeature::kTrustTokenRedemption)); RenderFrameHost* child = static_cast(web_contents) ->GetFrameTree() ->root() ->child_at(0) ->current_frame_host(); RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess()); replacer.Activate(); // Note: this can't use NavigateFrameToURL, because that method doesn't // route through RFHI::BeginNavigation. It also can't use NavigateIframeToURL, // because that navigation will hang. // // It also can't EXPECT_TRUE or EXPECT_FALSE: sometimes the ExecJs call will // finish before the renderer gets killed, and sometimes it won't. ignore_result( ExecJs(child, JsReplace("location.href=$1;", GURL("/title2.html")))); EXPECT_THAT(kill_waiter.Wait(), Optional(HasSubstr("Permissions Policy feature is absent"))); } IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWithTrustTokensEnabled, BrowserForbidsTrustTokenParamsOnMainFrameNav) { WebContents* web_contents = shell()->web_contents(); // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. BeginNavigationTrustTokenParamsReplacer replacer( web_contents, network::mojom::TrustTokenParams::New()); GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHost* compromised_renderer = web_contents->GetMainFrame(); RenderProcessHostBadMojoMessageWaiter kill_waiter( compromised_renderer->GetProcess()); replacer.Activate(); // Can't use NavigateToURL here because it would hang. Additionally, we can't // EXPECT_TRUE or EXPECT_FALSE: sometimes the ExecJs call will finish // before the renderer gets killed, and sometimes it won't. ignore_result(ExecJs(compromised_renderer, JsReplace("location.href=$1", GURL("/title2.html")))); EXPECT_THAT(kill_waiter.Wait(), Optional(HasSubstr("Trust Token params in main frame nav"))); } } // namespace content