| // 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), false, 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) { |
| network::mojom::URLLoaderFactoryPtr factory; |
| frame->GetProcess()->CreateURLLoaderFactory( |
| frame->GetLastCommittedOrigin(), |
| network::mojom::CrossOriginEmbedderPolicy::kNone, |
| nullptr /* preferences */, net::NetworkIsolationKey(), |
| mojo::NullRemote() /* header_client */, mojo::MakeRequest(&factory)); |
| 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->HasPendingCommitNavigationForTesting()); |
| 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.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; |
| |
| 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.uses_post = false; |
| 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) { |
| // Explicitly isolating a.com helps ensure that this test is applicable on |
| // platforms without site-per-process. |
| IsolateOrigin("a.com"); |
| |
| // IsolateOrigin internally performs navigations which get stored into the |
| // back-forward cache. It needs to be flushed. The |
| // BeginNavigationInitiatorReplacer below will simulate receiving |
| // RenderFrameCreated() on every active RenderFrameHost, but it will miss the |
| // ones in the BackForwardCache. This would causes a mismatch later when it |
| // will observe RenderFrameDeleted. |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| web_contents->GetController().GetBackForwardCache().Flush(); |
| |
| // 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"))); |
| |
| // 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; |
| if (IsPerNavigationMojoInterfaceEnabled()) { |
| 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()); |
| } else { |
| rfh->frame_host_receiver_for_testing().impl()->BeginNavigation( |
| std::move(common_params), std::move(begin_params), mojo::NullRemote(), |
| mojo::NullAssociatedRemote(), 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 |