| // 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 <stdint.h> |
| |
| #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/loader/resource_message_filter.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/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/view_messages.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.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_navigation_observer.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_provisional_load_interceptor.h" |
| #include "content/test/mock_widget_impl.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" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/network_switches.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/mojom/url_loader.mojom.h" |
| #include "services/network/test/test_url_loader_client.h" |
| #include "third_party/blink/public/web/web_triggering_event_info.h" |
| |
| using IPC::IpcSecurityTestUtil; |
| |
| namespace content { |
| |
| namespace { |
| |
| // This request id is used by tests that call CreateLoaderAndStart. 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 RenderFrameHost which is used for |
| // the attempt is the return value. |
| RenderFrameHostImpl* 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<WebContentsImpl*>(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, true, blink::WebTriggeringEventInfo::kFromTrustedEvent, |
| nullptr /* blob_url_loader_factory */); |
| |
| // 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; |
| } |
| |
| network::ResourceRequest CreateXHRRequest(const char* url) { |
| network::ResourceRequest request; |
| request.method = "GET"; |
| request.url = GURL(url); |
| request.referrer_policy = Referrer::GetDefaultReferrerPolicy(); |
| request.request_initiator = url::Origin(); |
| request.load_flags = 0; |
| request.plugin_child_id = -1; |
| request.resource_type = RESOURCE_TYPE_XHR; |
| request.appcache_host_id = kAppCacheNoHostId; |
| request.should_reset_appcache = false; |
| request.is_main_frame = true; |
| request.transition_type = ui::PAGE_TRANSITION_LINK; |
| request.allow_download = true; |
| return request; |
| } |
| |
| network::ResourceRequest CreateXHRRequestWithOrigin(const char* origin) { |
| network::ResourceRequest request = |
| CreateXHRRequest("http://bar.com/simple_page.html"); |
| request.site_for_cookies = GURL(origin); |
| request.headers.SetHeader(net::HttpRequestHeaders::kOrigin, origin); |
| return request; |
| } |
| |
| } // 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(); |
| |
| BrowserThread::PostTask( |
| BrowserThread::IO, FROM_HERE, |
| base::BindOnce(&net::URLRequestSlowDownloadJob::AddUrlHandler)); |
| } |
| |
| static void CreateLoaderAndStartOnIOThread( |
| scoped_refptr<ResourceMessageFilter> filter, |
| network::mojom::URLLoaderRequest request, |
| int route_id, |
| int request_id, |
| const network::ResourceRequest& resource_request, |
| network::mojom::URLLoaderClientPtrInfo client) { |
| filter->CreateLoaderAndStart( |
| std::move(request), route_id, request_id, |
| network::mojom::kURLLoadOptionNone, resource_request, |
| network::mojom::URLLoaderClientPtr(std::move(client)), |
| net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS)); |
| } |
| |
| static void CreateLoaderAndStart( |
| RenderProcessHost* process, |
| int route_id, |
| int request_id, |
| const network::ResourceRequest& resource_request) { |
| network::mojom::URLLoaderPtr loader; |
| network::TestURLLoaderClient client; |
| CreateLoaderAndStart(process, mojo::MakeRequest(&loader), route_id, |
| request_id, resource_request, |
| client.CreateInterfacePtr().PassInterface()); |
| } |
| |
| static void CreateLoaderAndStart( |
| RenderProcessHost* process, |
| network::mojom::URLLoaderRequest request, |
| int route_id, |
| int request_id, |
| const network::ResourceRequest& resource_request, |
| network::mojom::URLLoaderClientPtrInfo client) { |
| RenderProcessHostImpl* impl = static_cast<RenderProcessHostImpl*>(process); |
| auto filter = impl->resource_message_filter_; |
| |
| process->GetChannel()->ipc_task_runner()->PostTask( |
| FROM_HERE, base::BindOnce(CreateLoaderAndStartOnIOThread, filter, |
| std::move(request), route_id, request_id, |
| resource_request, std::move(client))); |
| } |
| |
| void TryCreateDuplicateRequestIds(Shell* shell, bool block_loaders) { |
| NavigateToURL(shell, GURL("http://foo.com/simple_page.html")); |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| 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; |
| network::ResourceRequest request(CreateXHRRequest(blocking_url)); |
| |
| // Use the same request id twice. |
| RenderProcessHostKillWaiter kill_waiter(rfh->GetProcess()); |
| // We need to keep loader and client to keep the requests alive. |
| network::mojom::URLLoaderPtr loader1, loader2; |
| network::TestURLLoaderClient client1, client2; |
| |
| CreateLoaderAndStart(rfh->GetProcess(), mojo::MakeRequest(&loader1), |
| rfh->GetRoutingID(), kRequestIdNotPreviouslyUsed, |
| request, client1.CreateInterfacePtr().PassInterface()); |
| CreateLoaderAndStart(rfh->GetProcess(), mojo::MakeRequest(&loader2), |
| rfh->GetRoutingID(), kRequestIdNotPreviouslyUsed, |
| request, client2.CreateInterfacePtr().PassInterface()); |
| EXPECT_EQ(bad_message::RDH_INVALID_REQUEST_ID, kill_waiter.Wait()); |
| } |
| |
| 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(); |
| RenderProcessHostKillWaiter kill_waiter(compromised_renderer->GetProcess()); |
| |
| FileChooserParams params; |
| params.default_file_name = path; |
| |
| FrameHostMsg_RunFileChooser evil(compromised_renderer->GetRoutingID(), |
| params); |
| |
| IpcSecurityTestUtil::PwnMessageReceived( |
| compromised_renderer->GetProcess()->GetChannel(), evil); |
| EXPECT_EQ(bad_message::RFH_FILE_CHOOSER_PATH, kill_waiter.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()); |
| |
| RenderViewHost* compromised_renderer = |
| shell()->web_contents()->GetRenderViewHost(); |
| RenderProcessHostKillWaiter 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(), &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. |
| 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; |
| RenderFrameHostImpl* pending_rfh = |
| PrepareToDuplicateHosts(shell(), &duplicate_routing_id); |
| EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id); |
| |
| mojom::WidgetPtr widget; |
| std::unique_ptr<MockWidgetImpl> widget_impl = |
| std::make_unique<MockWidgetImpl>(mojo::MakeRequest(&widget)); |
| |
| // 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->render_view_host()->CreateNewWidget( |
| duplicate_routing_id, std::move(widget), 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 "<html><head><script>" |
| "window.domAutomationController.send(\"okay\");" |
| "</script></head>" |
| "<body>this page is an interstitial</body></html>"; |
| } |
| |
| 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 != nullptr); |
| 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. |
| ExecuteScriptAsync(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) { |
| std::move(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. |
| network::ResourceRequest chrome_origin_msg( |
| CreateXHRRequestWithOrigin("chrome://settings")); |
| network::ResourceRequest embedder_isolated_origin_msg( |
| CreateXHRRequestWithOrigin("https://isolated.bar.com")); |
| network::ResourceRequest invalid_origin_msg( |
| CreateXHRRequestWithOrigin("invalidurl")); |
| network::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. |
| { |
| RenderProcessHostKillWaiter kill_waiter(web_rfh->GetProcess()); |
| |
| CreateLoaderAndStart(web_rfh->GetProcess(), web_rfh->GetRoutingID(), |
| kRequestIdNotPreviouslyUsed, chrome_origin_msg); |
| EXPECT_EQ(bad_message::RDH_ILLEGAL_ORIGIN, kill_waiter.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)); |
| |
| RenderProcessHostKillWaiter kill_waiter(web_rfh->GetProcess()); |
| CreateLoaderAndStart(web_rfh->GetProcess(), web_rfh->GetRoutingID(), |
| kRequestIdNotPreviouslyUsed, |
| embedder_isolated_origin_msg); |
| EXPECT_EQ(bad_message::RDH_ILLEGAL_ORIGIN, kill_waiter.Wait()); |
| } |
| |
| // Web processes cannot make XHRs with invalid Origin headers. |
| NavigateToURL(shell(), web_url); |
| { |
| RenderProcessHostKillWaiter kill_waiter(web_rfh->GetProcess()); |
| CreateLoaderAndStart(web_rfh->GetProcess(), web_rfh->GetRoutingID(), |
| kRequestIdNotPreviouslyUsed, invalid_origin_msg); |
| EXPECT_EQ(bad_message::RDH_ILLEGAL_ORIGIN, kill_waiter.Wait()); |
| } |
| |
| // Web processes cannot make XHRs with invalid scheme Origin headers. |
| NavigateToURL(shell(), web_url); |
| { |
| RenderProcessHostKillWaiter kill_waiter(web_rfh->GetProcess()); |
| CreateLoaderAndStart(web_rfh->GetProcess(), web_rfh->GetRoutingID(), |
| kRequestIdNotPreviouslyUsed, |
| invalid_scheme_origin_msg); |
| EXPECT_EQ(bad_message::RDH_ILLEGAL_ORIGIN, kill_waiter.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); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, InvalidBlockedRequestId) { |
| // 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<WebContentsImpl*>(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()); |
| |
| RenderProcessHostKillWaiter 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()); |
| } |
| |
| namespace { |
| |
| // Interceptor that replaces |interface_provider_request| with the specified |
| // value for the first DidCommitProvisionalLoad message it observes in the given |
| // |web_contents| while in scope. |
| class ScopedInterfaceProviderRequestReplacer |
| : public DidCommitProvisionalLoadInterceptor { |
| public: |
| ScopedInterfaceProviderRequestReplacer( |
| WebContents* web_contents, |
| service_manager::mojom::InterfaceProviderRequest |
| interface_provider_request_override) |
| : DidCommitProvisionalLoadInterceptor(web_contents), |
| interface_provider_request_override_( |
| std::move(interface_provider_request_override)) {} |
| ~ScopedInterfaceProviderRequestReplacer() override = default; |
| |
| protected: |
| bool WillDispatchDidCommitProvisionalLoad( |
| RenderFrameHost* render_frame_host, |
| ::FrameHostMsg_DidCommitProvisionalLoad_Params* params, |
| service_manager::mojom::InterfaceProviderRequest* |
| interface_provider_request) override { |
| CHECK(interface_provider_request_override_.has_value()); |
| *interface_provider_request = |
| std::move(interface_provider_request_override_).value(); |
| return true; |
| } |
| |
| private: |
| base::Optional<service_manager::mojom::InterfaceProviderRequest> |
| interface_provider_request_override_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ScopedInterfaceProviderRequestReplacer); |
| }; |
| |
| } // namespace |
| |
| // Test that, as a general rule, not receiving a new InterfaceProviderRequest |
| // for a cross-document navigation properly terminates the renderer process. |
| // There is one exception to this rule, see: RenderFrameHostImplBrowserTest. |
| // InterfaceProviderRequestIsOptionalForFirstCommit. |
| 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)); |
| |
| RenderProcessHostKillWaiter kill_waiter( |
| shell()->web_contents()->GetMainFrame()->GetProcess()); |
| |
| ScopedInterfaceProviderRequestReplacer replacer(shell()->web_contents(), |
| nullptr); |
| NavigateToURLAndExpectNoCommit(shell(), non_same_document_url); |
| EXPECT_EQ(bad_message::RFH_INTERFACE_PROVIDER_MISSING, kill_waiter.Wait()); |
| } |
| |
| // 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<WebContentsImpl*>(shell()->web_contents()) |
| ->GetFrameTree() |
| ->root(); |
| |
| RenderProcessHostKillWaiter kill_waiter( |
| root->current_frame_host()->GetProcess()); |
| |
| // 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 network::ResourceRequestBody; |
| 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. |
| EXPECT_EQ(bad_message::RFH_ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait()); |
| } |
| |
| // Test that forging a frame's unique name and commit won't allow changing the |
| // PageState of a cross-process FrameNavigationEntry. |
| // See https://crbug.com/766262. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PageStateToWrongEntry) { |
| IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); |
| |
| // Commit a page with nested iframes and a separate cross-process iframe. |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(a(a),b)")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| NavigationEntryImpl* back_entry = static_cast<NavigationEntryImpl*>( |
| shell()->web_contents()->GetController().GetLastCommittedEntry()); |
| int nav_entry_id = back_entry->GetUniqueID(); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetFrameTree() |
| ->root(); |
| FrameTreeNode* child0_0 = root->child_at(0)->child_at(0); |
| std::string child0_0_unique_name = child0_0->unique_name(); |
| FrameTreeNode* child1 = root->child_at(1); |
| GURL child1_url = child1->current_url(); |
| int child1_pid = child1->current_frame_host()->GetProcess()->GetID(); |
| PageState child1_page_state = back_entry->GetFrameEntry(child1)->page_state(); |
| |
| // Add a history item in the nested frame. It's important to do it there and |
| // not the main frame for the repro to work, since we don't walk the subtree |
| // when navigating back/forward between same document items. |
| TestNavigationObserver fragment_observer(shell()->web_contents()); |
| EXPECT_TRUE(ExecuteScript(child0_0, "location.href = '#foo';")); |
| fragment_observer.Wait(); |
| |
| // Simulate a name change IPC from the nested iframe, matching the cross-site |
| // iframe's unique name. |
| child0_0->SetFrameName("foo", child1->unique_name()); |
| |
| // Simulate a back navigation from the now renamed nested iframe, which would |
| // put a PageState on the cross-site iframe's FrameNavigationEntry. Forge a |
| // data URL within the PageState that differs from child1_url. |
| std::unique_ptr<FrameHostMsg_DidCommitProvisionalLoad_Params> params = |
| std::make_unique<FrameHostMsg_DidCommitProvisionalLoad_Params>(); |
| params->nav_entry_id = nav_entry_id; |
| params->did_create_new_entry = false; |
| params->url = GURL("about:blank"); |
| params->transition = ui::PAGE_TRANSITION_AUTO_SUBFRAME; |
| params->should_update_history = false; |
| params->gesture = NavigationGestureAuto; |
| params->method = "GET"; |
| params->page_state = PageState::CreateFromURL(GURL("data:text/html,foo")); |
| params->origin = url::Origin::Create(GURL("about:blank")); |
| |
| service_manager::mojom::InterfaceProviderPtr isolated_interface_provider; |
| static_cast<mojom::FrameHost*>(child0_0->current_frame_host()) |
| ->DidCommitProvisionalLoad( |
| std::move(params), mojo::MakeRequest(&isolated_interface_provider)); |
| |
| // Make sure we haven't changed the FrameNavigationEntry. An attack would |
| // modify the PageState but leave the SiteInstance as it was. |
| EXPECT_EQ(child1->current_frame_host()->GetSiteInstance(), |
| back_entry->GetFrameEntry(child1)->site_instance()); |
| EXPECT_EQ(child1_page_state, back_entry->GetFrameEntry(child1)->page_state()); |
| |
| // Put the frame's unique name back. |
| child0_0->SetFrameName("bar", child0_0_unique_name); |
| |
| // Go forward after the fake back navigation. |
| TestNavigationObserver forward_observer(shell()->web_contents()); |
| shell()->web_contents()->GetController().GoForward(); |
| forward_observer.Wait(); |
| |
| // Go back to the possibly corrupted entry and ensure we didn't load the data |
| // URL in the previous process. A test failure here would appear as a failure |
| // of the URL check and not the process ID check. |
| TestNavigationObserver back_observer(shell()->web_contents()); |
| shell()->web_contents()->GetController().GoBack(); |
| back_observer.Wait(); |
| EXPECT_EQ(child1_pid, child1->current_frame_host()->GetProcess()->GetID()); |
| ASSERT_EQ(child1_url, child1->current_url()); |
| } |
| |
| } // namespace content |