// 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/bind.h" #include "base/command_line.h" #include "base/feature_list.h" #include "base/macros.h" #include "base/optional.h" #include "base/strings/utf_string_conversions.h" #include "base/task/post_task.h" #include "base/test/scoped_feature_list.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/frame_host/navigator.h" #include "content/browser/frame_host/render_frame_host_impl.h" #include "content/browser/frame_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/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/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/interstitial_page.h" #include "content/public/browser/interstitial_page_delegate.h" #include "content/public/browser/resource_context.h" #include "content/public/browser/storage_partition.h" #include "content/public/common/content_features.h" #include "content/public/common/content_switches.h" #include "content/public/common/navigation_policy.h" #include "content/public/common/url_constants.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_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/mock_widget_impl.h" #include "content/test/test_content_browser_client.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_remote.h" #include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/bindings/strong_associated_binding.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/test/url_request/url_request_slow_download_job.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/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/blink/public/common/blob/blob_utils.h" #include "third_party/blink/public/common/features.h" #include "third_party/blink/public/common/navigation/triggering_event_info.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" 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, net::EmbeddedTestServer* server, int* target_routing_id) { GURL foo("http://foo.com/simple_page.html"); if (AreDefaultSiteInstancesEnabled()) { // 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(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("http://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, url::Origin::Create(foo), nullptr, std::string(), Referrer(), WindowOpenDisposition::CURRENT_TAB, false, true, blink::TriggeringEventInfo::kFromTrustedEvent, std::string(), 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.resource_type = static_cast<int>(ResourceType::kXhr); request.should_reset_appcache = false; request.is_main_frame = true; request.transition_type = ui::PAGE_TRANSITION_LINK; return request; } std::unique_ptr<content::BlobHandle> CreateMemoryBackedBlob( BrowserContext* browser_context, const std::string& contents, const std::string& content_type) { std::unique_ptr<content::BlobHandle> result; base::RunLoop loop; BrowserContext::CreateMemoryBackedBlob( browser_context, contents.c_str(), contents.length(), content_type, base::BindOnce( [](std::unique_ptr<content::BlobHandle>* out_blob, base::OnceClosure done, std::unique_ptr<content::BlobHandle> 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::StrongAssociatedBindingPtr<blink::mojom::BlobURLStore> binding) { auto interceptor = base::WrapUnique(new BlobURLStoreInterceptor(target_url)); auto* raw_interceptor = interceptor.get(); auto impl = binding->SwapImplForTesting(std::move(interceptor)); raw_interceptor->url_store_ = std::move(impl); } blink::mojom::BlobURLStore* GetForwardingInterface() override { return url_store_.get(); } void Register(mojo::PendingRemote<blink::mojom::Blob> blob, const GURL& url, RegisterCallback callback) override { GetForwardingInterface()->Register(std::move(blob), target_url_, std::move(callback)); } private: explicit BlobURLStoreInterceptor(GURL target_url) : target_url_(target_url) {} std::unique_ptr<blink::mojom::BlobURLStore> 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, base::string16())); std::vector<blink::mojom::FileChooserFileInfoPtr> 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, std::unique_ptr<FileSelectListener> listener, const blink::mojom::FileChooserParams& params) override { listener_ = std::move(listener); } void EnumerateDirectory(WebContents* web_contents, std::unique_ptr<FileSelectListener> listener, const base::FilePath& directory_path) override { listener->FileSelectionCanceled(); } private: std::unique_ptr<FileSelectListener> 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(); base::PostTask( FROM_HERE, {BrowserThread::IO}, base::BindOnce(&net::URLRequestSlowDownloadJob::AddUrlHandler)); } static void CreateLoaderAndStart( RenderFrameHost* frame, int route_id, int request_id, const network::ResourceRequest& resource_request) { network::mojom::URLLoaderPtr loader; network::TestURLLoaderClient client; CreateLoaderAndStart(frame, mojo::MakeRequest(&loader), route_id, request_id, resource_request, client.CreateInterfacePtr().PassInterface()); } static void CreateLoaderAndStart( RenderFrameHost* frame, network::mojom::URLLoaderRequest request, int route_id, int request_id, const network::ResourceRequest& resource_request, network::mojom::URLLoaderClientPtrInfo client) { mojo::Remote<network::mojom::URLLoaderFactory> factory; frame->GetProcess()->CreateURLLoaderFactory( frame->GetLastCommittedOrigin(), network::mojom::CrossOriginEmbedderPolicy::kNone, nullptr /* preferences */, net::NetworkIsolationKey(), mojo::NullRemote() /* header_client */, factory.BindNewPipeAndPassReceiver()); factory->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)); } void TryCreateDuplicateRequestIds(Shell* shell, bool block_loaders) { EXPECT_TRUE(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, mojo::MakeRequest(&loader1), rfh->GetRoutingID(), kRequestIdNotPreviouslyUsed, request, client1.CreateInterfacePtr().PassInterface()); CreateLoaderAndStart(rfh, 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 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(base::ASCIIToUTF16("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<blink::mojom::FileChooser> factory = static_cast<RenderFrameHostImpl*>(compromised_renderer) ->BindFileChooserForTesting(); factory->OpenFileChooser( std::move(params), blink::mojom::FileChooser::OpenFileChooserCallback()); factory.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(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(), 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); mojo::PendingRemote<mojom::Widget> widget; std::unique_ptr<MockWidgetImpl> widget_impl = std::make_unique<MockWidgetImpl>(widget.InitWithNewPipeAndPassReceiver()); // 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)); // 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<DelayedFileChooserDelegate>(); shell()->web_contents()->SetDelegate(delegate.get()); mojo::Remote<blink::mojom::FileChooser> chooser = static_cast<RenderFrameHostImpl*>(compromised_renderer) ->BindFileChooserForTesting(); 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 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"); EXPECT_TRUE(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()); } class CorsExploitBrowserTest : public ContentBrowserTest { public: CorsExploitBrowserTest() { feature_list_.InitAndEnableFeature( blink::features::kHtmlImportsRequestInitiatorLock); } void SetUpCommandLine(base::CommandLine* command_line) override { // TODO(yoichio): This is temporary switch to support chrome internal // components migration from the old web APIs. // After completion of the migration, we should remove this. // See https://crbug.com/911943 for detail. command_line->AppendSwitchASCII("enable-blink-features", "HTMLImports"); } void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); SetupCrossSiteRedirector(embedded_test_server()); } private: base::test::ScopedFeatureList feature_list_; DISALLOW_COPY_AND_ASSIGN(CorsExploitBrowserTest); }; // This is a regression test for https://crbug.com/961614 - it makes sure that // the trustworthy |request_initiator_site_lock| takes precedent over // the untrustworthy |request.request_initiator|. // // For spoofing a |request.request_initiator| that doesn't match // |request_initiator_site_lock|, the test relies on a misfeature of HTML // Imports. It is unclear how to replicate such spoofing once HTML imports are // deprecated. IN_PROC_BROWSER_TEST_F(CorsExploitBrowserTest, OriginHeaderSpoofViaHtmlImports) { std::string victim_path = "/victim/secret.json"; net::test_server::ControllableHttpResponse victim_response( embedded_test_server(), victim_path, false); ASSERT_TRUE(embedded_test_server()->Start()); GURL attacker_url( embedded_test_server()->GetURL("attacker.com", "/title1.html")); GURL module_url(embedded_test_server()->GetURL( "module.com", "/cross_site_document_blocking/html_import3.html")); GURL victim_url(embedded_test_server()->GetURL("victim.com", victim_path)); EXPECT_TRUE(NavigateToURL(shell(), attacker_url)); // From a renderer process locked to attacker.com, load a HTML Import from // module.com. HTML Imports implementation allows attacker.com to issue // requests on behalf of the module.com module - here attacker.com initiates a // request for a victim.com resource. const char kScriptTemplate[] = R"( link = document.createElement('link'); link.rel = 'import'; link.href = $1; link.onload = () => { with(link.import.documentElement.appendChild( link.import.createElement('script'))) { crossOrigin = 'use-credentials'; src = $2 } }; document.documentElement.appendChild(link); )"; std::string script = JsReplace(kScriptTemplate, module_url, victim_url); ASSERT_TRUE(ExecJs(shell(), script)); // Verify that attacker.com-controlled request for a victim.com resource uses // CORS and has `Origin: http://attacker.com` request header (rather than // `Origin: http://module.com`). victim_response.WaitForRequest(); net::test_server::HttpRequest::HeaderMap headers = victim_response.http_request()->headers; ASSERT_TRUE(base::Contains(headers, "Origin")); EXPECT_EQ(url::Origin::Create(attacker_url).Serialize(), headers["Origin"]); } // 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_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, ::FrameHostMsg_DidCommitProvisionalLoad_Params* params, 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 // DocumentInterfaceBroker, InterfaceProviderRequest-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<RenderFrameHostImpl*>( shell()->web_contents()->GetMainFrame()); RenderProcessHostKillWaiter 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<WebContentsImpl*>(shell()->web_contents()) ->GetFrameTree() ->root(); RenderProcessHostKillWaiter 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_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.post_body = new network::ResourceRequestBody; params.post_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; 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::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), mojom::DidCommitProvisionalLoadInterfaceParams::New( mojo::MakeRequest(&isolated_interface_provider), mojo::PendingRemote<blink::mojom::DocumentInterfaceBroker>() .InitWithNewPipeAndPassReceiver(), mojo::PendingRemote<blink::mojom::DocumentInterfaceBroker>() .InitWithNewPipeAndPassReceiver(), mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker>() .InitWithNewPipeAndPassReceiver())); // 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()); } 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. base::HistogramTester histograms; RenderProcessHostWatcher crash_observer( rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT); // The renderer should always get killed, but sometimes ExecuteScript returns // true anyway, so just ignore the result. ignore_result( content::ExecuteScript(rfh, "URL.createObjectURL(new Blob(['foo']))")); // If the process is killed, this test passes. crash_observer.Wait(); histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 123, 1); } // 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( base::ASCIIToUTF16("var r = new XMLHttpRequest();" "r.open('GET', '/slow?99999', false);" "r.send(null);" "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 = "<html><body>pwned.</body></html>"; std::string payload_type = "text/html"; std::unique_ptr<content::BlobHandle> 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<WebContentsImpl*>(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<WebContentsImpl*>(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. GURL file_url("file:///"); FrameHostMsg_OpenURL_Params params; params.url = file_url; params.disposition = WindowOpenDisposition::CURRENT_TAB; params.should_replace_current_entry = false; params.user_gesture = true; SiteInstance* a_com_instance = root->current_frame_host()->GetSiteInstance(); RenderFrameProxyHost* proxy = child->render_manager()->GetRenderFrameProxyHost(a_com_instance); EXPECT_TRUE(proxy); { FrameHostMsg_OpenURL msg(proxy->GetRoutingID(), params); IPC::IpcSecurityTestUtil::PwnMessageReceived( proxy->GetProcess()->GetChannel(), msg); } // 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_TRUE(WaitForLoadStop(shell()->web_contents())); 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); GURL chrome_url(std::string(kChromeUIScheme) + "://" + std::string(kChromeUIGpuHost)); params.url = chrome_url; { FrameHostMsg_OpenURL msg(proxy->GetRoutingID(), params); IPC::IpcSecurityTestUtil::PwnMessageReceived( proxy->GetProcess()->GetChannel(), msg); } EXPECT_TRUE(WaitForLoadStop(shell()->web_contents())); EXPECT_EQ(GURL(kBlockedURL), child->current_frame_host()->GetLastCommittedURL()); } class PostMessageIpcInterceptor : public BrowserMessageFilter { public: // Starts listening for IPC messages to |process|, intercepting // FrameHostMsg_RouteMessageEvent and storing once it comes. explicit PostMessageIpcInterceptor(RenderProcessHost* process) : BrowserMessageFilter(FrameMsgStart) { process->AddFilter(this); } // Waits for FrameMsg_PostMessage_Params (if it didn't come yet) and returns // message payload to the caller. void WaitForMessage(int32_t* out_routing_id, FrameMsg_PostMessage_Params* out_params) { run_loop_.Run(); *out_routing_id = intercepted_routing_id_; *out_params = intercepted_params_; } private: ~PostMessageIpcInterceptor() override = default; void OnRouteMessageEvent(const FrameMsg_PostMessage_Params& params) { intercepted_params_ = params; // UaF would have happened without the call below - the call ensures that // the data is still valid even once the original message is destroyed. intercepted_params_.message->data.EnsureDataIsOwned(); } bool OnMessageReceived(const IPC::Message& message) override { // Only intercept one message. if (already_intercepted_) return false; // See if we got FrameHostMsg_RouteMessageEvent and if so unpack and store // its payload in OnRouteMessageEvent. bool handled = true; IPC_BEGIN_MESSAGE_MAP(PostMessageIpcInterceptor, message) IPC_MESSAGE_HANDLER(FrameHostMsg_RouteMessageEvent, OnRouteMessageEvent) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() // If we got FrameHostMsg_RouteMessageEvent, then also store the routing ID // and signal to the main test thread that it can stop waiting. if (handled) { already_intercepted_ = true; intercepted_routing_id_ = message.routing_id(); run_loop_.Quit(); } return handled; } bool already_intercepted_ = false; int32_t intercepted_routing_id_; FrameMsg_PostMessage_Params intercepted_params_; base::RunLoop run_loop_; DISALLOW_COPY_AND_ASSIGN(PostMessageIpcInterceptor); }; // Test verifying that a compromised renderer can't lie about // FrameMsg_PostMessage_Params::source_origin. 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()); // Prepare to intercept FrameHostMsg_RouteMessageEvent IPC message that will // come from the subframe process. RenderProcessHost* subframe_process = subframe->GetProcess(); auto ipc_interceptor = base::MakeRefCounted<PostMessageIpcInterceptor>(subframe_process); // Post a message from the subframe to the cross-site parent and intercept the // associated IPC message. EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')")); int intercepted_routing_id; FrameMsg_PostMessage_Params intercepted_params; ipc_interceptor->WaitForMessage(&intercepted_routing_id, &intercepted_params); // Change the intercepted message to simulate a compromised subframe renderer // lying that the |source_origin| of the postMessage is the origin of the // parent (not of the subframe). url::Origin invalid_origin = web_contents->GetMainFrame()->GetLastCommittedOrigin(); FrameMsg_PostMessage_Params evil_params = intercepted_params; evil_params.source_origin = base::UTF8ToUTF16(invalid_origin.Serialize()); FrameHostMsg_RouteMessageEvent evil_msg(intercepted_routing_id, evil_params); // Inject the invalid IPC and verify that the renderer gets terminated. RenderProcessHostKillWaiter kill_waiter(subframe_process); IpcSecurityTestUtil::PwnMessageReceived(subframe_process->GetChannel(), evil_msg); EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_SOURCE_ORIGIN, kill_waiter.Wait()); } class OpenUrlIpcInterceptor : public BrowserMessageFilter { public: // Starts listening for IPC messages to |process|, intercepting // FrameHostMsg_OpenURL IPC message and storing once it comes. explicit OpenUrlIpcInterceptor(RenderProcessHost* process) : BrowserMessageFilter(FrameMsgStart) { process->AddFilter(this); } // Waits for FrameHostMsg_OpenURL (if it didn't come yet) and returns // message payload to the caller. void WaitForMessage(int32_t* out_routing_id, FrameHostMsg_OpenURL_Params* out_params) { run_loop_.Run(); *out_routing_id = intercepted_routing_id_; *out_params = intercepted_params_; } private: ~OpenUrlIpcInterceptor() override = default; void OnOpenURL(const FrameHostMsg_OpenURL_Params& params) { intercepted_params_ = params; } bool OnMessageReceived(const IPC::Message& message) override { // Only intercept one message. if (already_intercepted_) return false; // See if we got FrameHostMsg_RouteMessageEvent and if so unpack and store // its payload in OnRouteMessageEvent. bool handled = true; IPC_BEGIN_MESSAGE_MAP(OpenUrlIpcInterceptor, message) IPC_MESSAGE_HANDLER(FrameHostMsg_OpenURL, OnOpenURL) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() // If we got FrameHostMsg_RouteMessageEvent, then also store the routing ID // and signal to the main test thread that it can stop waiting. if (handled) { already_intercepted_ = true; intercepted_routing_id_ = message.routing_id(); run_loop_.Quit(); } return handled; } bool already_intercepted_ = false; int32_t intercepted_routing_id_; FrameHostMsg_OpenURL_Params intercepted_params_; base::RunLoop run_loop_; DISALLOW_COPY_AND_ASSIGN(OpenUrlIpcInterceptor); }; 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)); RenderFrameHost* main_frame = 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 FrameHostMsg_OpenURL IPC message that will come from // the main frame process. auto ipc_interceptor = base::MakeRefCounted<OpenUrlIpcInterceptor>(main_process); // Have the main frame request navigation in the "remote" subframe. This will // result in FrameHostMsg_OpenURL IPC being sent to the RenderFrameProxyHost. EXPECT_TRUE(ExecJs(shell()->web_contents()->GetMainFrame(), "window.frames[0].location = '/title1.html';")); int intercepted_routing_id; FrameHostMsg_OpenURL_Params intercepted_params; ipc_interceptor->WaitForMessage(&intercepted_routing_id, &intercepted_params); // Change the intercepted message to simulate a compromised subframe renderer // lying that the |initiator_origin| is the origin of the |subframe|. FrameHostMsg_OpenURL_Params evil_params = intercepted_params; evil_params.initiator_origin = subframe->GetLastCommittedOrigin(); FrameHostMsg_OpenURL evil_msg(intercepted_routing_id, evil_params); // Inject the invalid IPC and verify that the renderer gets terminated. RenderProcessHostKillWaiter kill_waiter(main_process); IpcSecurityTestUtil::PwnMessageReceived(main_process->GetChannel(), evil_msg); EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait()); } class BeginNavigationInitiatorReplacer : public FrameHostInterceptor { public: BeginNavigationInitiatorReplacer( WebContents* web_contents, base::Optional<url::Origin> initiator_to_inject) : FrameHostInterceptor(web_contents), initiator_to_inject_(initiator_to_inject) {} bool WillDispatchBeginNavigation( RenderFrameHost* render_frame_host, mojom::CommonNavigationParamsPtr* common_params, mojom::BeginNavigationParamsPtr* begin_params, mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token, mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client, mojo::PendingRemote<blink::mojom::NavigationInitiator>* navigation_initiator) override { if (is_activated_) { (*common_params)->initiator_origin = initiator_to_inject_; is_activated_ = false; } return true; } void Activate() { is_activated_ = true; } private: base::Optional<url::Origin> initiator_to_inject_; bool is_activated_ = false; DISALLOW_COPY_AND_ASSIGN(BeginNavigationInitiatorReplacer); }; IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, InvalidBeginNavigationInitiator) { WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(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(); RenderProcessHostKillWaiter 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, base::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(); RenderProcessHostKillWaiter 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, ::FrameHostMsg_DidCommitProvisionalLoad_Params* 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"); // Navigate to foo.com initially. GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), foo_url)); // 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. RenderProcessHostKillWaiter 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()); } 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, mojom::CommonNavigationParamsPtr* common_params, mojom::BeginNavigationParamsPtr* begin_params, mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token, mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client, mojo::PendingRemote<blink::mojom::NavigationInitiator>* navigation_initiator) 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(); RenderProcessHostKillWaiter 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<RenderFrameHostImpl*>( 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. mojom::CommonNavigationParamsPtr common_params = mojom::CommonNavigationParams::New( data_url, url::Origin::Create(data_url), blink::mojom::Referrer::New(), ui::PAGE_TRANSITION_LINK, mojom::NavigationType::DIFFERENT_DOCUMENT, NavigationDownloadPolicy(), false /* should_replace_current_entry */, file_url, /* base_url_for_data_url */ GURL() /* history_url_for_data_url */, PREVIEWS_UNSPECIFIED, base::TimeTicks::Now() /* navigation_start */, "GET", nullptr /* post_data */, base::Optional<SourceLocation>(), false /* started_from_context_menu */, false /* has_user_gesture */, InitiatorCSPInfo(), std::vector<int>() /* initiator_origin_trial_features */, std::string() /* href_translate */, false /* is_history_navigation_in_new_child_frame */, base::TimeTicks()); mojom::BeginNavigationParamsPtr begin_params = mojom::BeginNavigationParams::New( std::string() /* headers */, net::LOAD_NORMAL, false /* skip_service_worker */, blink::mojom::RequestContextType::LOCATION, blink::WebMixedContentContextType::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 */, base::nullopt /* devtools_initiator_info */); // Receiving the invalid IPC message should lead to renderer process // termination. RenderProcessHostKillWaiter process_kill_waiter(rfh->GetProcess()); mojo::PendingAssociatedRemote<mojom::NavigationClient> 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" "window.domAutomationController.send(xhr.responseText);", file_url.spec().c_str()); std::string result; EXPECT_TRUE( ExecuteScriptAndExtractString(shell()->web_contents(), script, &result)); 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")); } } // namespace content