// 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/command_line.h" #include "base/containers/hash_tables.h" #include "base/macros.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "content/browser/bad_message.h" #include "content/browser/dom_storage/dom_storage_context_wrapper.h" #include "content/browser/dom_storage/session_storage_namespace_impl.h" #include "content/browser/frame_host/navigator.h" #include "content/browser/frame_host/render_frame_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/web_contents_impl.h" #include "content/common/frame.mojom.h" #include "content/common/frame_messages.h" #include "content/common/render_message_filter.mojom.h" #include "content/common/resource_messages.h" #include "content/common/resource_request.h" #include "content/common/view_messages.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/interstitial_page.h" #include "content/public/browser/interstitial_page_delegate.h" #include "content/public/browser/resource_context.h" #include "content/public/browser/resource_dispatcher_host.h" #include "content/public/browser/storage_partition.h" #include "content/public/common/appcache_info.h" #include "content/public/common/browser_side_navigation_policy.h" #include "content/public/common/content_switches.h" #include "content/public/common/file_chooser_params.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/test_utils.h" #include "content/shell/browser/shell.h" #include "content/test/content_browser_test_utils_internal.h" #include "content/test/test_content_browser_client.h" #include "ipc/ipc_security_test_util.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/url_request/url_request_slow_download_job.h" using IPC::IpcSecurityTestUtil; namespace content { namespace { // This request id is used by tests that craft a // ResourceHostMsg_RequestResource. The id is sufficiently large that it doesn't // collide with ids used by previous navigation requests. const int kRequestIdNotPreviouslyUsed = 10000; // 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 RenderViewHost which is used for // the attempt is the return value. RenderViewHostImpl* PrepareToDuplicateHosts(Shell* shell, int* target_routing_id) { GURL foo("http://foo.com/simple_page.html"); // Start off with initial navigation, so we get the first process allocated. NavigateToURL(shell, foo); EXPECT_EQ(base::ASCIIToUTF16("OK"), shell->web_contents()->GetTitle()); // Open another window, so we generate some more routing ids. ShellAddedObserver shell2_observer; EXPECT_TRUE(ExecuteScript(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()->GetRenderViewHost()->GetRoutingID(); EXPECT_NE(*target_routing_id, shell->web_contents()->GetRenderViewHost()->GetRoutingID()); // Now, simulate a link click coming from the renderer. GURL extension_url("https://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, false, nullptr, std::string(), Referrer(), WindowOpenDisposition::CURRENT_TAB, false, false, true); // 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; if (IsBrowserSideNavigationEnabled()) next_rfh = wc->GetRenderManagerForTesting()->speculative_frame_host(); else next_rfh = wc->GetRenderManagerForTesting()->pending_frame_host(); EXPECT_TRUE(next_rfh); EXPECT_NE(shell->web_contents()->GetRenderProcessHost()->GetID(), next_rfh->GetProcess()->GetID()); return next_rfh->render_view_host(); } ResourceRequest CreateXHRRequest(const char* url) { ResourceRequest request; request.method = "GET"; request.url = GURL(url); request.referrer_policy = blink::kWebReferrerPolicyDefault; request.request_initiator = url::Origin(); request.load_flags = 0; request.origin_pid = 0; request.resource_type = RESOURCE_TYPE_XHR; request.request_context = 0; request.appcache_host_id = kAppCacheNoHostId; request.download_to_file = false; request.should_reset_appcache = false; request.is_main_frame = true; request.parent_is_main_frame = false; request.parent_render_frame_id = -1; request.transition_type = ui::PAGE_TRANSITION_LINK; request.allow_download = true; return request; } ResourceRequest CreateXHRRequestWithOrigin(const char* origin) { ResourceRequest request = CreateXHRRequest("http://bar.com/simple_page.html"); request.first_party_for_cookies = GURL(origin); request.headers = base::StringPrintf("Origin: %s\r\n", origin); return request; } void TryCreateDuplicateRequestIds(Shell* shell, bool block_loaders) { NavigateToURL(shell, GURL("http://foo.com/simple_page.html")); RenderFrameHost* rfh = shell->web_contents()->GetMainFrame(); if (block_loaders) { // Test the case where loaders are placed into blocked_loaders_map_. rfh->BlockRequestsForFrame(); } // URLRequestSlowDownloadJob waits for another request to kFinishDownloadUrl // to finish all pending requests. It is never sent, so the following URL // blocks indefinitely, which is good because the request stays alive and the // test can try to reuse the request id without a race. const char* blocking_url = net::URLRequestSlowDownloadJob::kUnknownSizeUrl; ResourceRequest request(CreateXHRRequest(blocking_url)); // Use the same request id twice. RenderProcessHostWatcher process_killed( rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); IPC::IpcSecurityTestUtil::PwnMessageReceived( rfh->GetProcess()->GetChannel(), ResourceHostMsg_RequestResource(rfh->GetRoutingID(), kRequestIdNotPreviouslyUsed, request)); IPC::IpcSecurityTestUtil::PwnMessageReceived( rfh->GetProcess()->GetChannel(), ResourceHostMsg_RequestResource(rfh->GetRoutingID(), kRequestIdNotPreviouslyUsed, request)); process_killed.Wait(); } } // 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( 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(); BrowserThread::PostTask( BrowserThread::IO, FROM_HERE, base::Bind(&net::URLRequestSlowDownloadJob::AddUrlHandler)); } 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 SecurityExploitBrowserTest::TestFileChooserWithPath( const base::FilePath& path) { GURL foo("http://foo.com/simple_page.html"); NavigateToURL(shell(), foo); EXPECT_EQ(base::ASCIIToUTF16("OK"), shell()->web_contents()->GetTitle()); RenderFrameHost* compromised_renderer = shell()->web_contents()->GetMainFrame(); RenderProcessHostWatcher terminated( shell()->web_contents(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); FileChooserParams params; params.default_file_name = path; FrameHostMsg_RunFileChooser evil(compromised_renderer->GetRoutingID(), params); IpcSecurityTestUtil::PwnMessageReceived( compromised_renderer->GetProcess()->GetChannel(), evil); terminated.Wait(); } // 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"); NavigateToURL(shell(), foo); EXPECT_EQ(base::ASCIIToUTF16("OK"), shell()->web_contents()->GetTitle()); EXPECT_EQ(0, shell()->web_contents()->GetMainFrame()->GetEnabledBindings()); RenderProcessHostWatcher terminated( shell()->web_contents(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); shell()->web_contents()->GetRenderViewHost()->SetWebUIProperty( "toolkit", "views"); terminated.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; RenderViewHostImpl* pending_rvh = PrepareToDuplicateHosts(shell(), &duplicate_routing_id); EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id); RenderFrameHostImpl* opener = static_cast(pending_rvh->GetMainFrame()); mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New(); params->target_url = GURL("about:blank"); opener->CreateNewWindow(std::move(params), base::Bind([](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. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, AttemptDuplicateRenderWidgetHost) { #if defined(OS_WIN) // PlzNavigate // This test is failing in a flaky manner on Android as it appears to be // leaking URLRequests. See crbug.com/702584. if (IsBrowserSideNavigationEnabled()) return; #endif int duplicate_routing_id = MSG_ROUTING_NONE; RenderViewHostImpl* pending_rvh = PrepareToDuplicateHosts(shell(), &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_rvh->CreateNewWidget(duplicate_routing_id, blink::kWebPopupTypePage); // 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 } class SecurityExploitTestInterstitialPage : public InterstitialPageDelegate { public: explicit SecurityExploitTestInterstitialPage(WebContents* contents) { InterstitialPage* interstitial = InterstitialPage::Create( contents, true, contents->GetLastCommittedURL(), this); interstitial->Show(); } // InterstitialPageDelegate implementation. void CommandReceived(const std::string& command) override { last_command_ = command; } std::string GetHTMLContents() override { return "" "this page is an interstitial"; } std::string last_command() { return last_command_; } private: std::string last_command_; DISALLOW_COPY_AND_ASSIGN(SecurityExploitTestInterstitialPage); }; // Fails due to InterstitialPage's reliance on PostNonNestableTask // http://crbug.com/432737 #if defined(OS_ANDROID) #define MAYBE_InterstitialCommandFromUnderlyingContent \ DISABLED_InterstitialCommandFromUnderlyingContent #else #define MAYBE_InterstitialCommandFromUnderlyingContent \ InterstitialCommandFromUnderlyingContent #endif // The interstitial should not be controllable by the underlying content. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MAYBE_InterstitialCommandFromUnderlyingContent) { // Start off with initial navigation, to allocate the process. GURL foo("http://foo.com/simple_page.html"); NavigateToURL(shell(), foo); EXPECT_EQ(base::ASCIIToUTF16("OK"), shell()->web_contents()->GetTitle()); DOMMessageQueue message_queue; // Install and show an interstitial page. SecurityExploitTestInterstitialPage* interstitial = new SecurityExploitTestInterstitialPage(shell()->web_contents()); ASSERT_EQ("", interstitial->last_command()); WaitForInterstitialAttach(shell()->web_contents()); InterstitialPage* interstitial_page = shell()->web_contents()->GetInterstitialPage(); ASSERT_TRUE(interstitial_page != NULL); ASSERT_TRUE(shell()->web_contents()->ShowingInterstitialPage()); ASSERT_TRUE(interstitial_page->GetDelegateForTesting() == interstitial); // The interstitial page ought to be able to send a message. std::string message; ASSERT_TRUE(message_queue.WaitForMessage(&message)); ASSERT_EQ("\"okay\"", message); ASSERT_EQ("\"okay\"", interstitial->last_command()); // Send an automation message from the underlying content and wait for it to // be dispatched on this thread. This message should not be received by the // interstitial. RenderFrameHost* compromised_renderer = shell()->web_contents()->GetMainFrame(); FrameHostMsg_DomOperationResponse evil(compromised_renderer->GetRoutingID(), "evil"); IpcSecurityTestUtil::PwnMessageReceived( compromised_renderer->GetProcess()->GetChannel(), evil); ASSERT_TRUE(message_queue.WaitForMessage(&message)); ASSERT_EQ("evil", message) << "Automation message should be received by WebContents."; ASSERT_EQ("\"okay\"", interstitial->last_command()) << "Interstitial should not be affected."; // Send a second message from the interstitial page, and make sure that the // "evil" message doesn't arrive in the intervening period. ASSERT_TRUE(ExecuteScript(interstitial_page->GetMainFrame(), "window.domAutomationController.send(\"okay2\");")); ASSERT_TRUE(message_queue.WaitForMessage(&message)); ASSERT_EQ("\"okay2\"", message); ASSERT_EQ("\"okay2\"", interstitial->last_command()); } // Intercepts the HTTP origin header and on being invoked once it is found // aborts the requeest. void OnHttpHeaderReceived(const std::string& header, const std::string& value, int child_process_id, content::ResourceContext* resource_context, OnHeaderProcessedCallback callback) { callback.Run(HeaderInterceptorResult::KILL); } // Renderer processes should not be able to spoof Origin HTTP headers. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, InvalidOriginHeaders) { // Create a set of IPC messages with various Origin headers. ResourceRequest chrome_origin_msg( CreateXHRRequestWithOrigin("chrome://settings")); ResourceRequest embedder_isolated_origin_msg( CreateXHRRequestWithOrigin("https://isolated.bar.com")); ResourceRequest invalid_origin_msg(CreateXHRRequestWithOrigin("invalidurl")); ResourceRequest invalid_scheme_origin_msg( CreateXHRRequestWithOrigin("fake-scheme://foo")); GURL web_url("http://foo.com/simple_page.html"); NavigateToURL(shell(), web_url); RenderFrameHost* web_rfh = shell()->web_contents()->GetMainFrame(); // Web processes cannot make XHRs with chrome:// Origin headers. { RenderProcessHostWatcher web_process_killed( web_rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); IPC::IpcSecurityTestUtil::PwnMessageReceived( web_rfh->GetProcess()->GetChannel(), ResourceHostMsg_RequestResource(web_rfh->GetRoutingID(), kRequestIdNotPreviouslyUsed, chrome_origin_msg)); web_process_killed.Wait(); } // Web processes cannot make XHRs with URLs that the content embedder expects // to have process isolation. Ideally this would test chrome-extension:// // URLs for Chrome Apps, but those can't be tested inside content/ and the // ResourceRequest IPC can't be created in a test outside content/. NavigateToURL(shell(), web_url); { content::ResourceDispatcherHost::Get()->RegisterInterceptor( "Origin", "", base::Bind(&OnHttpHeaderReceived)); RenderProcessHostWatcher web_process_killed( web_rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); IPC::IpcSecurityTestUtil::PwnMessageReceived( web_rfh->GetProcess()->GetChannel(), ResourceHostMsg_RequestResource(web_rfh->GetRoutingID(), kRequestIdNotPreviouslyUsed, embedder_isolated_origin_msg)); web_process_killed.Wait(); } // Web processes cannot make XHRs with invalid Origin headers. NavigateToURL(shell(), web_url); { RenderProcessHostWatcher web_process_killed( web_rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); IPC::IpcSecurityTestUtil::PwnMessageReceived( web_rfh->GetProcess()->GetChannel(), ResourceHostMsg_RequestResource(web_rfh->GetRoutingID(), kRequestIdNotPreviouslyUsed, invalid_origin_msg)); web_process_killed.Wait(); } // Web processes cannot make XHRs with invalid scheme Origin headers. NavigateToURL(shell(), web_url); { RenderProcessHostWatcher web_process_killed( web_rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); IPC::IpcSecurityTestUtil::PwnMessageReceived( web_rfh->GetProcess()->GetChannel(), ResourceHostMsg_RequestResource(web_rfh->GetRoutingID(), kRequestIdNotPreviouslyUsed, invalid_scheme_origin_msg)); web_process_killed.Wait(); } } // Renderer process should not be able to create multiple requests with the same // id. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, InvalidRequestId) { // Existing loader in pending_loaders_. TryCreateDuplicateRequestIds(shell(), false); // Existing loader in blocked_loaders_map_. TryCreateDuplicateRequestIds(shell(), true); } // 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(); // Setup an URL which will never commit, allowing this test to send its own, // malformed, commit message. GURL url(embedded_test_server()->GetURL("/title2.html")); NavigationStallDelegate stall_delegate(url); ResourceDispatcherHost::Get()->SetDelegate(&stall_delegate); // 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()); RenderProcessHostWatcher exit_observer( root->current_frame_host()->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); // Create commit params with different origins in params.url and // params.origin. FrameHostMsg_DidCommitProvisionalLoad_Params params; params.nav_entry_id = 0; params.did_create_new_entry = false; params.url = url; params.transition = ui::PAGE_TRANSITION_LINK; params.should_update_history = false; params.gesture = NavigationGestureAuto; params.was_within_same_document = false; params.method = "GET"; params.page_state = PageState::CreateFromURL(url); params.origin = url::Origin(GURL("http://bar.com/")); FrameHostMsg_DidCommitProvisionalLoad msg( root->current_frame_host()->routing_id(), params); IPC::IpcSecurityTestUtil::PwnMessageReceived( root->current_frame_host()->GetProcess()->GetChannel(), msg); // 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()->HasConnection()); exit_observer.Wait(); EXPECT_FALSE(exit_observer.did_exit_normally()); ResourceDispatcherHost::Get()->SetDelegate(nullptr); } // 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(); RenderProcessHostWatcher exit_observer( root->current_frame_host()->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); // Prepare a file to upload. base::ThreadRestrictions::ScopedAllowIO allow_io_for_temp_dir; 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_LT( 0, base::WriteFile(file_path, file_content.data(), file_content.size())); // Simulate an IPC message asking to POST a file that the renderer shouldn't // have access to. FrameHostMsg_OpenURL_Params params; params.url = target_url; params.uses_post = true; params.resource_request_body = new ResourceRequestBodyImpl; params.resource_request_body->AppendFileRange( file_path, 0, file_content.size(), base::Time()); params.disposition = WindowOpenDisposition::CURRENT_TAB; params.should_replace_current_entry = true; params.user_gesture = true; params.is_history_navigation_in_new_child = false; FrameHostMsg_OpenURL msg(root->current_frame_host()->routing_id(), params); IPC::IpcSecurityTestUtil::PwnMessageReceived( root->current_frame_host()->GetProcess()->GetChannel(), msg); // Verify that the malicious navigation did not commit the navigation to // |target_url|. WaitForLoadStop(shell()->web_contents()); EXPECT_EQ(start_url, root->current_frame_host()->GetLastCommittedURL()); // Verify that the malicious renderer got killed. exit_observer.Wait(); EXPECT_FALSE(exit_observer.did_exit_normally()); } } // namespace content