// Copyright 2013 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <stdint.h> #include <optional> #include <tuple> #include "base/command_line.h" #include "base/feature_list.h" #include "base/files/file_util.h" #include "base/functional/bind.h" #include "base/functional/callback_helpers.h" #include "base/memory/ptr_util.h" #include "base/memory/raw_ptr.h" #include "base/memory/weak_ptr.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/synchronization/waitable_event.h" #include "base/test/bind.h" #include "base/test/gtest_util.h" #include "base/test/scoped_feature_list.h" #include "base/unguessable_token.h" #include "build/build_config.h" #include "content/browser/attribution_reporting/attribution_manager.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/fenced_frame/fenced_frame.h" #include "content/browser/private_aggregation/private_aggregation_manager.h" #include "content/browser/renderer_host/navigator.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/renderer_host/render_frame_proxy_host.h" #include "content/browser/renderer_host/render_process_host_impl.h" #include "content/browser/renderer_host/render_view_host_factory.h" #include "content/browser/renderer_host/render_view_host_impl.h" #include "content/browser/web_contents/file_chooser_impl.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/common/features.h" #include "content/common/frame.mojom.h" #include "content/common/frame_messages.mojom.h" #include "content/common/render_message_filter.mojom.h" #include "content/public/browser/blob_handle.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/content_browser_client.h" #include "content/public/browser/file_select_listener.h" #include "content/public/browser/navigation_handle.h" #include "content/public/browser/resource_context.h" #include "content/public/browser/storage_partition.h" #include "content/public/common/bindings_policy.h" #include "content/public/common/content_switches.h" #include "content/public/common/isolated_world_ids.h" #include "content/public/common/url_constants.h" #include "content/public/test/back_forward_cache_util.h" #include "content/public/test/browser_test.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test_utils.h" #include "content/public/test/fenced_frame_test_util.h" #include "content/public/test/navigation_handle_observer.h" #include "content/public/test/test_frame_navigation_observer.h" #include "content/public/test/test_navigation_observer.h" #include "content/public/test/test_renderer_host.h" #include "content/public/test/test_utils.h" #include "content/shell/browser/shell.h" #include "content/test/content_browser_test_utils_internal.h" #include "content/test/did_commit_navigation_interceptor.h" #include "content/test/frame_host_interceptor.h" #include "content/test/test_content_browser_client.h" #include "ipc/ipc_message.h" #include "ipc/ipc_security_test_util.h" #include "mojo/core/embedder/embedder.h" #include "mojo/public/cpp/bindings/pending_associated_remote.h" #include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_remote.h" #include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/test_support/test_utils.h" #include "net/base/features.h" #include "net/base/filename_util.h" #include "net/base/network_isolation_key.h" #include "net/dns/mock_host_resolver.h" #include "net/storage_access_api/status.h" #include "net/test/embedded_test_server/controllable_http_response.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/http_request.h" #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" #include "services/network/public/cpp/network_switches.h" #include "services/network/public/cpp/resource_request.h" #include "services/network/public/mojom/fetch_api.mojom.h" #include "services/network/public/mojom/trust_tokens.mojom.h" #include "services/network/public/mojom/url_loader.mojom.h" #include "services/network/test/test_url_loader_client.h" #include "storage/browser/blob/blob_registry_impl.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/blob/blob_utils.h" #include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h" #include "third_party/blink/public/common/frame/fenced_frame_sandbox_flags.h" #include "third_party/blink/public/common/navigation/navigation_policy.h" #include "third_party/blink/public/mojom/blob/blob_url_store.mojom.h" #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h" #include "third_party/blink/public/mojom/fenced_frame/fenced_frame.mojom.h" #include "third_party/blink/public/mojom/frame/frame.mojom-test-utils.h" #include "third_party/blink/public/mojom/frame/frame.mojom.h" #include "third_party/blink/public/mojom/frame/remote_frame.mojom-test-utils.h" #include "third_party/blink/public/mojom/loader/mixed_content.mojom.h" using IPC::IpcSecurityTestUtil; using ::testing::HasSubstr; using ::testing::Optional; namespace content { namespace { // This is a helper function for the tests which attempt to create a // duplicate RenderViewHost or RenderWidgetHost. It tries to create two objects // with the same process and routing ids, which causes a collision. // It creates a couple of windows in process 1, which causes a few routing ids // to be allocated. Then a cross-process navigation is initiated, which causes a // new process 2 to be created and have a pending RenderViewHost for it. The // routing id of the RenderViewHost which is target for a duplicate is set // into |target_routing_id| and the pending RenderFrameHost which is used for // the attempt is the return value. RenderFrameHostImpl* PrepareToDuplicateHosts(Shell* shell, net::EmbeddedTestServer* server, int* target_routing_id) { GURL foo("http://foo.com/simple_page.html"); if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) { // Isolate "bar.com" so we are guaranteed to get a different process // for navigations to this origin. IsolateOriginsForTesting(server, shell->web_contents(), {"bar.com"}); } // Start off with initial navigation, so we get the first process allocated. EXPECT_TRUE(NavigateToURL(shell, foo)); EXPECT_EQ(u"OK", shell->web_contents()->GetTitle()); // Open another window, so we generate some more routing ids. ShellAddedObserver shell2_observer; EXPECT_TRUE(ExecJs(shell, "window.open(document.URL + '#2');")); Shell* shell2 = shell2_observer.GetShell(); // The new window must be in the same process, but have a new routing id. EXPECT_EQ(shell->web_contents() ->GetPrimaryMainFrame() ->GetProcess() ->GetDeprecatedID(), shell2->web_contents() ->GetPrimaryMainFrame() ->GetProcess() ->GetDeprecatedID()); *target_routing_id = shell2->web_contents() ->GetPrimaryMainFrame() ->GetRenderViewHost() ->GetRoutingID(); EXPECT_NE(*target_routing_id, shell->web_contents() ->GetPrimaryMainFrame() ->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()); TestNavigationManager navigation_manager(wc, extension_url); wc->GetPrimaryFrameTree().root()->navigator().RequestOpenURL( wc->GetPrimaryFrameTree().root()->current_frame_host(), extension_url, nullptr /* initiator_frame_token */, ChildProcessHost::kInvalidUniqueID /* initiator_process_id */, url::Origin::Create(foo), /* initiator_base_url= */ std::nullopt, nullptr, std::string(), Referrer(), WindowOpenDisposition::CURRENT_TAB, false /* should_replace_current_entry */, true /* user_gesture */, blink::mojom::TriggeringEventInfo::kFromTrustedEvent, std::string(), nullptr /* blob_url_loader_factory */, std::nullopt /* impression */, false /* has_rel_opener */); navigation_manager.WaitForSpeculativeRenderFrameHostCreation(); // 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->GetPrimaryFrameTree() .root() ->render_manager() ->speculative_frame_host(); EXPECT_TRUE(next_rfh); EXPECT_NE(shell->web_contents() ->GetPrimaryMainFrame() ->GetProcess() ->GetDeprecatedID(), next_rfh->GetProcess()->GetDeprecatedID()); return next_rfh; } blink::mojom::OpenURLParamsPtr CreateOpenURLParams(const GURL& url) { auto params = blink::mojom::OpenURLParams::New(); params->url = url; params->disposition = WindowOpenDisposition::CURRENT_TAB; params->should_replace_current_entry = false; params->user_gesture = true; return params; } std::unique_ptr<content::BlobHandle> CreateMemoryBackedBlob( BrowserContext* browser_context, const std::string& contents, const std::string& content_type) { std::unique_ptr<content::BlobHandle> result; base::RunLoop loop; browser_context->CreateMemoryBackedBlob( base::as_byte_span(contents), 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; } // Constructs a WebContentsDelegate that mocks a file dialog. // Unlike content::FileChooserDelegate, this class doesn't make a response in // RunFileChooser(), and a user needs to call Choose(). class DelayedFileChooserDelegate : public WebContentsDelegate { public: void Choose(const base::FilePath& file) { auto file_info = blink::mojom::FileChooserFileInfo::NewNativeFile( blink::mojom::NativeFileInfo::New(file, std::u16string(), std::vector<std::u16string>())); 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, scoped_refptr<FileSelectListener> listener, const blink::mojom::FileChooserParams& params) override { listener_ = std::move(listener); } void EnumerateDirectory(WebContents* web_contents, scoped_refptr<FileSelectListener> listener, const base::FilePath& directory_path) override { listener->FileSelectionCanceled(); } private: scoped_refptr<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(); } protected: // Tests that a given file path sent in a FrameHostMsg_RunFileChooser will // cause renderer to be killed. void TestFileChooserWithPath(const base::FilePath& path); void IsolateOrigin(const std::string& hostname) { IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(), {hostname}); } }; void SecurityExploitBrowserTest::TestFileChooserWithPath( const base::FilePath& path) { GURL foo("http://foo.com/simple_page.html"); EXPECT_TRUE(NavigateToURL(shell(), foo)); EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle()); RenderFrameHost* compromised_renderer = shell()->web_contents()->GetPrimaryMainFrame(); blink::mojom::FileChooserParamsPtr params = blink::mojom::FileChooserParams::New(); params->default_file_name = path; mojo::test::BadMessageObserver bad_message_observer; mojo::Remote<blink::mojom::FileChooser> chooser = FileChooserImpl::CreateBoundForTesting( static_cast<RenderFrameHostImpl*>(compromised_renderer)); chooser->OpenFileChooser( std::move(params), blink::mojom::FileChooser::OpenFileChooserCallback()); chooser.FlushForTesting(); EXPECT_THAT(bad_message_observer.WaitForBadMessage(), ::testing::StartsWith("FileChooser: The default file name")); } // Ensure that we kill the renderer process if we try to give it WebUI // properties and it doesn't have enabled WebUI bindings. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, SetWebUIProperty) { GURL foo("http://foo.com/simple_page.html"); EXPECT_TRUE(NavigateToURL(shell(), foo)); EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle()); EXPECT_TRUE(shell() ->web_contents() ->GetPrimaryMainFrame() ->GetEnabledBindings() .empty()); RenderFrameHost* compromised_renderer = shell()->web_contents()->GetPrimaryMainFrame(); RenderProcessHostBadIpcMessageWaiter kill_waiter( compromised_renderer->GetProcess()); compromised_renderer->SetWebUIProperty("toolkit", "views"); EXPECT_EQ(bad_message::RVH_WEB_UI_BINDINGS_MISMATCH, kill_waiter.Wait()); } // This is a test for crbug.com/312016 attempting to create duplicate // RenderViewHosts. SetupForDuplicateHosts sets up this test case and leaves // it in a state with pending RenderViewHost. Before the commit of the new // pending RenderViewHost, this test case creates a new window through the new // process. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, AttemptDuplicateRenderViewHost) { int32_t duplicate_routing_id = MSG_ROUTING_NONE; RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts( shell(), embedded_test_server(), &duplicate_routing_id); EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id); mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New(); params->target_url = GURL("about:blank"); pending_rfh->CreateNewWindow( std::move(params), base::BindOnce([](mojom::CreateNewWindowStatus, mojom::CreateNewWindowReplyPtr) {})); // If the above operation doesn't cause a crash, the test has succeeded! } // This is a test for crbug.com/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 BUILDFLAG(IS_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()->GetPrimaryMainFrame(); auto delegate = std::make_unique<DelayedFileChooserDelegate>(); shell()->web_contents()->SetDelegate(delegate.get()); mojo::Remote<blink::mojom::FileChooser> chooser = FileChooserImpl::CreateBoundForTesting( static_cast<RenderFrameHostImpl*>(compromised_renderer)); base::RunLoop run_loop1; base::RunLoop run_loop2; chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(), base::BindOnce(FileChooserCallback, &run_loop2)); // The following EnumerateChosenDirectory() runs the specified callback // immediately regardless of the content of the first argument FilePath. chooser->EnumerateChosenDirectory( base::FilePath(FILE_PATH_LITERAL(":*?\"<>|")), base::BindOnce(FileChooserCallback, &run_loop1)); run_loop1.Run(); delegate->Choose(base::FilePath(FILE_PATH_LITERAL("foo.txt"))); run_loop2.Run(); // The test passes if it doesn't crash. } class CorsExploitBrowserTest : public ContentBrowserTest { public: CorsExploitBrowserTest() = default; CorsExploitBrowserTest(const CorsExploitBrowserTest&) = delete; CorsExploitBrowserTest& operator=(const CorsExploitBrowserTest&) = delete; void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); SetupCrossSiteRedirector(embedded_test_server()); } }; // 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()) ->GetPrimaryFrameTree() .root(); // Navigate to a new URL, with an interceptor that replaces the origin with // one that does not match params.url. GURL url(embedded_test_server()->GetURL("/title2.html")); PwnCommitIPC(shell()->web_contents(), url, url, url::Origin::Create(GURL("http://bar.com/"))); // Use LoadURL, as the test shouldn't wait for navigation commit. NavigationController& controller = shell()->web_contents()->GetController(); controller.LoadURL(url, Referrer(), ui::PAGE_TRANSITION_LINK, std::string()); EXPECT_NE(nullptr, controller.GetPendingEntry()); EXPECT_EQ(url, controller.GetPendingEntry()->GetURL()); RenderProcessHostBadIpcMessageWaiter kill_waiter( root->current_frame_host()->GetProcess()); // When the IPC message is received and validation fails, the process is // terminated. However, the notification for that should be processed in a // separate task of the message loop, so ensure that the process is still // considered alive. EXPECT_TRUE( root->current_frame_host()->GetProcess()->IsInitializedAndNotDead()); EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } // Test that receiving a document.open() URL update with incorrect origin // properly terminates the renderer process. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginOnDocumentOpenURLUpdate) { GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); // Simulate a document.open() URL update with incorrect origin. RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); static_cast<mojom::FrameHost*>(rfh)->DidOpenDocumentInputStream( embedded_test_server()->GetURL("evil.com", "/title1.html")); // Ensure that the renderer process gets killed. EXPECT_EQ(AreAllSitesIsolatedForTesting() ? bad_message::RFH_CAN_COMMIT_URL_BLOCKED : bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } // Test that same-document navigations cannot go cross-origin (even within the // same site). IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, CrossOriginSameDocumentCommit) { GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); // Do a same-document navigation to a cross-origin URL/Origin (which match // each other, unlike the MismatchedOriginOnCommit), using an interceptor that // replaces the origin and URL. This intentionally uses a cross-origin but // same-site destination, to avoid failing Site Isolation checks. GURL dest_url(embedded_test_server()->GetURL("bar.foo.com", "/title2.html")); PwnCommitIPC(shell()->web_contents(), start_url, dest_url, url::Origin::Create(dest_url)); RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()); // ExecJs will sometimes finish before the renderer gets killed, so we must // ignore the result. std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), "history.pushState({}, '', location.href);"); EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } // Test that same-document navigations cannot go cross-origin from about:blank // (even within the same site). Uses a subframe to inherit an existing origin. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, CrossOriginSameDocumentCommitFromAboutBlank) { GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); // Create an about:blank iframe that inherits the origin. RenderFrameHost* subframe = CreateSubframe(static_cast<WebContentsImpl*>(shell()->web_contents()), "child1", GURL(), false /* wait_for_navigation */); EXPECT_EQ(url::Origin::Create(start_url), subframe->GetLastCommittedOrigin()); // Do a same-document navigation to another about:blank URL, but using a // different origin. This intentionally uses a cross-origin but same-site // origin to avoid triggering Site Isolation checks. GURL blank_url("about:blank#foo"); GURL fake_url(embedded_test_server()->GetURL("bar.foo.com", "/")); PwnCommitIPC(shell()->web_contents(), blank_url, blank_url, url::Origin::Create(fake_url)); RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); // ExecJs will sometimes finish before the renderer gets killed, so we must // ignore the result. std::ignore = ExecJs(subframe, "location.hash='foo';"); EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } // Test that same-document navigations cannot go cross-origin (even within the // same site), in the case that allow_universal_access_from_file_urls is enabled // but the last committed origin is not a file URL. See also // RenderFrameHostManagerTest.EnsureUniversalAccessFromFileSchemeSucceeds for // the intended case that file URLs are allowed to go cross-origin. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, CrossOriginSameDocumentCommitUniversalAccessNonFile) { auto prefs = shell()->web_contents()->GetOrCreateWebPreferences(); prefs.allow_universal_access_from_file_urls = true; shell()->web_contents()->SetWebPreferences(prefs); GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); // Do a same-document navigation to a cross-origin URL, using an interceptor // that replaces the URL but not the origin (to simulate the universal access // case, but for a non-file committed origin). This intentionally uses a // cross-origin but same-site destination, to avoid failing Site Isolation // checks. GURL dest_url(embedded_test_server()->GetURL("bar.foo.com", "/title2.html")); PwnCommitIPC(shell()->web_contents(), start_url, dest_url, url::Origin::Create(start_url)); RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()); // ExecJs will sometimes finish before the renderer gets killed, so we must // ignore the result. std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), "history.pushState({}, '', location.href);"); EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); } // Test that receiving a commit with a URL with an invalid scheme properly // terminates the renderer process. See https://crbug.com/324934416. // TODO(crbug.com/40092527): This test can be removed once the browser // stops using cross-document URLs computed by the renderer process. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, BadUrlSchemeOnCommit) { GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) ->GetPrimaryFrameTree() .root(); // Navigate to a new URL, with an interceptor that replaces the URL with one // that has an illegal scheme. Note that most cross-document navigations where // the renderer's commit URL disagrees with the browser's expectation will // currently be caught by a DCHECK in debug builds, but this case still works // in release builds until the browser process becomes the authority for // cross-document URLs in https://crbug.com/888079. For now, we can test this // case and avoid the DCHECK by claiming to commit about:blank#blocked, which // is given an exception in RenderFrameHostImpl's CalculateLoadingURL. GURL url("about:blank#blocked"); GURL bad_scheme_url("bar:com"); PwnCommitIPC(shell()->web_contents(), url, bad_scheme_url, url::Origin::Create(url)); RenderProcessHost* process = root->current_frame_host()->GetProcess(); RenderProcessHostBadIpcMessageWaiter kill_waiter(process); // ExecJs will sometimes finish before the renderer gets killed, so we must // ignore the result. std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), "location.href = 'about:blank#blocked';"); // 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(process->IsInitializedAndNotDead()); EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait()); } // Test that receiving a same-document commit with a URL with an invalid scheme // properly terminates the renderer process. See https://crbug.com/324934416. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, BadUrlSchemeOnSameDocumentCommit) { GURL start_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); // Do a same-document navigation to a URL with an incorrect scheme, but with // the expected origin, using an interceptor that replaces the URL. GURL dest_url("bar:com"); PwnCommitIPC(shell()->web_contents(), start_url, dest_url, url::Origin::Create(start_url)); RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()); // ExecJs will sometimes finish before the renderer gets killed, so we must // ignore the result. std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), "history.pushState({}, '', location.href);"); EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, 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(const ScopedInterfaceParamsReplacer&) = delete; ScopedInterfaceParamsReplacer& operator=( const ScopedInterfaceParamsReplacer&) = delete; ~ScopedInterfaceParamsReplacer() override = default; protected: bool WillProcessDidCommitNavigation( RenderFrameHost* render_frame_host, NavigationRequest* navigation_request, mojom::DidCommitProvisionalLoadParamsPtr*, mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) override { interface_params->Swap(¶ms_override_); return true; } private: mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override_; }; } // 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/40519010): when all clients are converted to use // BrowserInterfaceBroker, PendingReceiver<InterfaceProvider>-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()->GetPrimaryMainFrame()); RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess()); NavigationHandleObserver navigation_observer(shell()->web_contents(), non_same_document_url); ScopedInterfaceParamsReplacer replacer(shell()->web_contents(), nullptr); EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), non_same_document_url)); EXPECT_EQ(bad_message::RFH_INTERFACE_PROVIDER_MISSING, kill_waiter.Wait()); // Verify that the death of the renderer process doesn't leave behind and // leak NavigationRequests - see https://crbug.com/869193. EXPECT_FALSE(frame->HasPendingCommitNavigation()); EXPECT_FALSE(navigation_observer.has_committed()); EXPECT_TRUE(navigation_observer.is_error()); EXPECT_TRUE(navigation_observer.last_committed_url().is_empty()); EXPECT_EQ(net::OK, navigation_observer.net_error_code()); } // Test that a compromised renderer cannot ask to upload an arbitrary file in // OpenURL. This is a regression test for https://crbug.com/726067. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, OpenUrl_ResourceRequestBody) { GURL start_url(embedded_test_server()->GetURL("/title1.html")); GURL target_url(embedded_test_server()->GetURL("/echoall")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) ->GetPrimaryFrameTree() .root(); RenderProcessHostBadIpcMessageWaiter kill_waiter( root->current_frame_host()->GetProcess()); // Prepare a file to upload. base::ScopedAllowBlockingForTesting allow_blocking; base::ScopedTempDir temp_dir; base::FilePath file_path; std::string file_content("test-file-content"); ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path)); ASSERT_TRUE(base::WriteFile(file_path, file_content)); // Simulate an OpenURL Mojo method asking to POST a file that the renderer // shouldn't have access to. auto params = CreateOpenURLParams(target_url); params->post_body = new network::ResourceRequestBody; params->post_body->AppendFileRange(file_path, 0, file_content.size(), base::Time()); params->should_replace_current_entry = true; static_cast<mojom::FrameHost*>(root->current_frame_host()) ->OpenURL(std::move(params)); // Verify that the malicious navigation did not commit the navigation to // |target_url|. EXPECT_EQ(start_url, root->current_frame_host()->GetLastCommittedURL()); // Verify that the malicious renderer got killed. EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait()); } // Forging a navigation commit after the initial empty document will result in a // renderer kill, even if the URL used is about:blank. // See https://crbug.com/766262 for an example advanced case that involves // forging a frame's unique name. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, NonInitialAboutBlankRendererKill) { // Navigate normally. GURL url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), url)); RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); // Simulate an about:blank commit without a NavigationRequest. It will fail // because only initial commits are allowed to do this. auto params = mojom::DidCommitProvisionalLoadParams::New(); params->did_create_new_entry = false; params->url = GURL("about:blank"); params->referrer = blink::mojom::Referrer::New(); params->transition = ui::PAGE_TRANSITION_LINK; params->should_update_history = false; params->method = "GET"; params->page_state = blink::PageState::CreateFromURL(GURL("about:blank")); params->origin = url::Origin::Create(GURL("about:blank")); params->embedding_token = base::UnguessableToken::Create(); RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); static_cast<mojom::FrameHost*>(rfh)->DidCommitProvisionalLoad( std::move(params), mojom::DidCommitProvisionalLoadInterfaceParams::New( mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker>() .InitWithNewPipeAndPassReceiver())); // Verify that the malicious renderer got killed. EXPECT_EQ(bad_message::RFH_NO_MATCHING_NAVIGATION_REQUEST_ON_COMMIT, kill_waiter.Wait()); } class SecurityExploitBrowserTestMojoBlobURLs : public SecurityExploitBrowserTest { public: SecurityExploitBrowserTestMojoBlobURLs() = default; void TearDown() override { storage::BlobUrlRegistry::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()->GetPrimaryMainFrame(); // 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"; base::RepeatingCallback<void(storage::BlobUrlRegistry*, mojo::ReceiverId)> blob_url_registry_intercept_hook; blob_url_registry_intercept_hook = base::BindRepeating(&BlobURLStoreInterceptor::Intercept, GURL("blob:" + target_origin + "/" + blob_path)); storage::BlobUrlRegistry::SetURLStoreCreationHookForTesting( &blob_url_registry_intercept_hook); // Register a blob URL from the a.com main frame, which will go through the // interceptor above and be rewritten to register the blob URL with the b.com // origin. This should result in a kill because a.com should not be allowed // to create blob URLs outside of its own origin. content::RenderProcessHostBadMojoMessageWaiter crash_observer( rfh->GetProcess()); // The renderer should always get killed, but sometimes ExecJs returns // true anyway, so just ignore the result. std::ignore = ExecJs(rfh, "URL.createObjectURL(new Blob(['foo']))"); // If the process is killed, this test passes. EXPECT_EQ( "Received bad user message: " "URL with invalid origin passed to BlobURLStore::Register", crash_observer.Wait()); } // Check that with site isolation enabled, an origin can't create a filesystem // URL for a different origin. See https://crbug.com/888001. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, CreateFilesystemURLInDifferentOrigin) { IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess()); GURL main_url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); // Block the renderer on operation that never completes, to shield it from // receiving unexpected browser->renderer IPCs that might CHECK. rfh->ExecuteJavaScriptWithUserGestureForTests( u"var r = new XMLHttpRequest();" u"r.open('GET', '/slow?99999', false);" u"r.send(null);" u"while (1);", base::NullCallback(), ISOLATED_WORLD_ID_GLOBAL); // 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, rfh->GetStorageKey()); // 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, rfh->GetStorageKey()); // 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()) ->GetPrimaryFrameTree() .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()) ->GetPrimaryFrameTree() .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. SiteInstanceImpl* a_com_instance = root->current_frame_host()->GetSiteInstance(); RenderFrameProxyHost* proxy = child->current_frame_host() ->browsing_context_state() ->GetRenderFrameProxyHost(a_com_instance->group()); EXPECT_TRUE(proxy); TestNavigationObserver observer(shell()->web_contents()); static_cast<mojom::FrameHost*>(proxy->frame_tree_node()->current_frame_host()) ->OpenURL(CreateOpenURLParams(GURL("file:///"))); observer.Wait(); // Verify that the malicious navigation was blocked. Currently, this happens // by rewriting the target URL to about:blank#blocked. // // TODO(alexmos): Consider killing the renderer process in this case, since // this security check is already enforced in the renderer process. EXPECT_EQ(GURL(kBlockedURL), child->current_frame_host()->GetLastCommittedURL()); // Navigate to the starting page again to recreate the proxy, then try the // same malicious navigation with a chrome:// URL. EXPECT_TRUE(NavigateToURL(shell(), main_url)); child = root->child_at(0); proxy = child->current_frame_host() ->browsing_context_state() ->GetRenderFrameProxyHost(a_com_instance->group()); EXPECT_TRUE(proxy); TestNavigationObserver observer_2(shell()->web_contents()); GURL chrome_url(std::string(kChromeUIScheme) + "://" + std::string(kChromeUIGpuHost)); static_cast<mojom::FrameHost*>(proxy->frame_tree_node()->current_frame_host()) ->OpenURL(CreateOpenURLParams(chrome_url)); observer_2.Wait(); EXPECT_EQ(GURL(kBlockedURL), child->current_frame_host()->GetLastCommittedURL()); } class RemoteFrameHostInterceptor : public blink::mojom::RemoteFrameHostInterceptorForTesting { public: explicit RemoteFrameHostInterceptor( RenderFrameProxyHost* render_frame_proxy_host, const url::Origin& evil_origin) : evil_origin_(evil_origin), swapped_impl_( render_frame_proxy_host->frame_host_receiver_for_testing(), this) {} ~RemoteFrameHostInterceptor() override = default; RemoteFrameHost* GetForwardingInterface() override { return swapped_impl_.old_impl(); } void RouteMessageEvent( const std::optional<blink::LocalFrameToken>& source_frame_token, const url::Origin& source_origin, const std::u16string& target_origin, blink::TransferableMessage message) override { // Forward the message to the actual RFPH replacing |source_origin| with the // "evil origin". GetForwardingInterface()->RouteMessageEvent( std::move(source_frame_token), evil_origin_, std::move(target_origin), std::move(message)); } void OpenURL(blink::mojom::OpenURLParamsPtr params) override { intercepted_params_ = std::move(params); } blink::mojom::OpenURLParamsPtr GetInterceptedParams() { return std::move(intercepted_params_); } private: url::Origin evil_origin_; blink::mojom::OpenURLParamsPtr intercepted_params_; mojo::test::ScopedSwapImplForTesting<blink::mojom::RemoteFrameHost> swapped_impl_; }; // Test verifying that a compromised renderer can't lie about the source_origin // passed along with the RouteMessageEvent() mojo message. See also // https://crbug.com/915721. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PostMessageSourceOrigin) { // Explicitly isolating a.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("b.com"); // Navigate to a page with an OOPIF. GURL main_url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); // Sanity check of test setup: main frame and subframe should be isolated. WebContents* web_contents = shell()->web_contents(); RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame(); RenderFrameHost* subframe = ChildFrameAt(main_frame, 0); EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess()); // We need to get ahold of the RenderFrameProxyHost representing the main // frame for the subframe's process, to install the mojo interceptor. FrameTreeNode* main_frame_node = static_cast<WebContentsImpl*>(shell()->web_contents()) ->GetPrimaryFrameTree() .root(); FrameTreeNode* subframe_node = main_frame_node->child_at(0); SiteInstanceImpl* b_com_instance = subframe_node->current_frame_host()->GetSiteInstance(); RenderFrameProxyHost* main_frame_proxy_host = main_frame_node->current_frame_host() ->browsing_context_state() ->GetRenderFrameProxyHost(b_com_instance->group()); // Prepare to intercept the RouteMessageEvent IPC message that will come // from the subframe process. url::Origin evil_source_origin = web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin(); RemoteFrameHostInterceptor mojo_interceptor(main_frame_proxy_host, evil_source_origin); // Post a message from the subframe to the cross-site parent and intercept the // associated IPC message, changing it to simulate a compromised subframe // renderer lying that the |source_origin| of the postMessage is the origin of // the parent (not of the subframe). RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')")); EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_SOURCE_ORIGIN, kill_waiter.Wait()); } // Test verifying that a compromised renderer can't lie about the source_origin // passed along with the RouteMessageEvent() mojo message. Similar to the test // above, but exercises a scenario where the source origin is opaque and the // precursor needs to be validated. This provides coverage for messages sent // from sandboxed frames; see https://crbug.com/40606810 and // https://crbug.com/325410297. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PostMessageOpaqueSourceOrigin) { // This test requires opaque origin enforcements to be turned on; otherwise, // there would be no renderer kill to check for. if (!base::FeatureList::IsEnabled( features::kAdditionalOpaqueOriginEnforcements)) { GTEST_SKIP(); } // Explicitly isolating b.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("b.com"); GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root(); RenderFrameHostImpl* main_frame = root->current_frame_host(); // Create cross-site sandboxed child frame. GURL child_url(embedded_test_server()->GetURL("b.com", "/title1.html")); { std::string js_str = base::StringPrintf( "var frame = document.createElement('iframe'); " "frame.sandbox = 'allow-scripts'; " "frame.src = '%s'; " "document.body.appendChild(frame);", child_url.spec().c_str()); EXPECT_TRUE(ExecJs(main_frame, js_str)); ASSERT_TRUE(WaitForLoadStop(web_contents)); } // Sanity check of test setup: main frame and subframe should be in separate // processes, and subframe should be sandboxed. FrameTreeNode* subframe_node = root->child_at(0); RenderFrameHostImpl* subframe = subframe_node->current_frame_host(); EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess()); EXPECT_TRUE(subframe->GetSiteInstance()->GetSiteInfo().is_sandboxed()); // Retrieve the RenderFrameProxyHost representing the main frame for the // subframe's process. RenderFrameProxyHost* main_frame_proxy_host = main_frame->browsing_context_state()->GetRenderFrameProxyHost( subframe->GetSiteInstance()->group()); // Prepare to intercept the RouteMessageEvent IPC message that will come from // the subframe process. Set the fake source origin to an opaque origin with // a.com as the precursor. url::Origin precursor_origin = main_frame->GetLastCommittedOrigin(); url::Origin evil_source_origin = precursor_origin.DeriveNewOpaqueOrigin(); EXPECT_TRUE(evil_source_origin.opaque()); EXPECT_EQ("a.com", evil_source_origin.GetTupleOrPrecursorTupleIfOpaque().host()); RemoteFrameHostInterceptor mojo_interceptor(main_frame_proxy_host, evil_source_origin); // Post a message from the subframe to the cross-site parent and intercept the // associated IPC message, changing it to simulate a compromised subframe // renderer lying that the |source_origin| of the postMessage has an incorrect // precursor of a.com, rather than b.com. This should result in a renderer // kill. RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')")); EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_SOURCE_ORIGIN, kill_waiter.Wait()); } IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, InvalidRemoteNavigationInitiator) { // Explicitly isolating a.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("a.com"); // Navigate to a test page where the subframe is cross-site (and because of // IsolateOrigin call above in a separate process) from the main frame. GURL main_url(embedded_test_server()->GetURL( "a.com", "/cross_site_iframe_factory.html?a(b)")); EXPECT_TRUE(NavigateToURL(shell(), main_url)); RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); RenderProcessHost* main_process = main_frame->GetProcess(); RenderFrameHost* subframe = ChildFrameAt(main_frame, 0); ASSERT_TRUE(subframe); RenderProcessHost* subframe_process = subframe->GetProcess(); EXPECT_NE(main_process->GetDeprecatedID(), subframe_process->GetDeprecatedID()); // Prepare to intercept OpenURL Mojo message that will come from // the main frame. FrameTreeNode* main_frame_node = static_cast<WebContentsImpl*>(shell()->web_contents()) ->GetPrimaryFrameTree() .root(); FrameTreeNode* child_node = main_frame_node->child_at(0); SiteInstanceImpl* a_com_instance = main_frame_node->current_frame_host()->GetSiteInstance(); RenderFrameProxyHost* proxy = child_node->current_frame_host() ->browsing_context_state() ->GetRenderFrameProxyHost(a_com_instance->group()); RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); { RemoteFrameHostInterceptor interceptor(proxy, url::Origin()); // Have the main frame request navigation in the "remote" subframe. This // will result in OpenURL Mojo message being sent to the // RenderFrameProxyHost. EXPECT_TRUE(ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), "window.frames[0].location = '/title1.html';")); // Change the intercepted message to simulate a compromised subframe // renderer lying that the |initiator_origin| is the origin of the // |subframe|. auto evil_params = interceptor.GetInterceptedParams(); evil_params->initiator_origin = subframe->GetLastCommittedOrigin(); // Inject the invalid IPC and verify that the renderer gets terminated. static_cast<mojom::FrameHost*>(main_frame)->OpenURL(std::move(evil_params)); } EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait()); } class BeginNavigationInitiatorReplacer : public FrameHostInterceptor { public: BeginNavigationInitiatorReplacer( WebContents* web_contents, std::optional<url::Origin> initiator_to_inject) : FrameHostInterceptor(web_contents), initiator_to_inject_(initiator_to_inject) {} BeginNavigationInitiatorReplacer(const BeginNavigationInitiatorReplacer&) = delete; BeginNavigationInitiatorReplacer& operator=( const BeginNavigationInitiatorReplacer&) = delete; bool WillDispatchBeginNavigation( RenderFrameHost* render_frame_host, blink::mojom::CommonNavigationParamsPtr* common_params, blink::mojom::BeginNavigationParamsPtr* begin_params, mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token, mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client) override { if (is_activated_) { (*common_params)->initiator_origin = initiator_to_inject_; is_activated_ = false; } return true; } void Activate() { is_activated_ = true; } private: std::optional<url::Origin> initiator_to_inject_; bool is_activated_ = false; }; 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->GetPrimaryMainFrame()->GetProcess(); RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); // Have the main frame navigate and lie that the initiator origin is b.com. injector.Activate(); // Don't expect a response for the script, as the process may be killed // before the script sends its completion message. ExecuteScriptAsync(web_contents, "window.location = '/title2.html';"); // Verify that the renderer was terminated. EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait()); } // Similar to the test above, but ensure that initiator origins are validated // even for opaque origins. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, InvalidBeginNavigationOpaqueInitiator) { // This test requires opaque origin enforcements to be turned on; otherwise, // there would be no renderer kill to check for. if (!base::FeatureList::IsEnabled( features::kAdditionalOpaqueOriginEnforcements)) { GTEST_SKIP(); } 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. url::Origin injected_origin(url::Origin::Create(GURL("http://evil.com"))); injected_origin = injected_origin.DeriveNewOpaqueOrigin(); BeginNavigationInitiatorReplacer injector(web_contents, injected_origin); // Explicitly isolating b.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("b.com"); // Navigate to a test page at a.com. GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(web_contents, main_url)); // Add a cross-site sandboxed child frame at b.com. FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root(); RenderFrameHostImpl* main_frame = root->current_frame_host(); GURL child_url(embedded_test_server()->GetURL("b.com", "/title1.html")); { std::string js_str = base::StringPrintf( "var frame = document.createElement('iframe'); " "frame.sandbox = 'allow-scripts'; " "frame.src = '%s'; " "document.body.appendChild(frame);", child_url.spec().c_str()); EXPECT_TRUE(ExecJs(main_frame, js_str)); ASSERT_TRUE(WaitForLoadStop(web_contents)); } // Sanity check of test setup: main frame and subframe should be in separate // processes, and subframe should be sandboxed. FrameTreeNode* subframe_node = root->child_at(0); RenderFrameHostImpl* subframe = subframe_node->current_frame_host(); EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess()); EXPECT_TRUE(subframe->GetSiteInstance()->GetSiteInfo().is_sandboxed()); // Start monitoring for renderer kills. RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); // Have the sandboxed subframe navigate and lie that the initiator origin is // an opaque origin with the precursor of evil.com instead of 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(subframe, "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, std::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->GetPrimaryMainFrame()->GetProcess(); RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); // Have the main frame submit a BeginNavigation IPC with a missing initiator. injector.Activate(); // Don't expect a response for the script, as the process may be killed // before the script sends its completion message. ExecuteScriptAsync(web_contents, "window.location = '/title2.html';"); // Verify that the renderer was terminated. EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_MISSING_INITIATOR_ORIGIN, kill_waiter.Wait()); } namespace { // An interceptor class that allows replacing the URL of the commit IPC from // the renderer process to the browser process. class DidCommitUrlReplacer : public DidCommitNavigationInterceptor { public: DidCommitUrlReplacer(WebContents* web_contents, const GURL& replacement_url) : DidCommitNavigationInterceptor(web_contents), replacement_url_(replacement_url) {} DidCommitUrlReplacer(const DidCommitUrlReplacer&) = delete; DidCommitUrlReplacer& operator=(const DidCommitUrlReplacer&) = delete; ~DidCommitUrlReplacer() override = default; protected: bool WillProcessDidCommitNavigation( RenderFrameHost* render_frame_host, NavigationRequest* navigation_request, mojom::DidCommitProvisionalLoadParamsPtr* params, mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) override { (**params).url = replacement_url_; return true; } private: GURL replacement_url_; }; } // namespace // Test which verifies that when an exploited renderer process sends a commit // message with URL that the process is not allowed to commit. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DidCommitInvalidURL) { // Explicitly isolating foo.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("foo.com"); RenderFrameDeletedObserver initial_frame_deleted_observer( shell()->web_contents()->GetPrimaryMainFrame()); // Test assumes the initial RenderFrameHost to be deleted. Disable // back-forward cache to ensure that it doesn't get preserved in the cache. DisableBackForwardCacheForTesting(shell()->web_contents(), BackForwardCache::TEST_REQUIRES_NO_CACHING); // Navigate to foo.com initially. GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), foo_url)); // Wait for the RenderFrameHost which was current before the navigation to // foo.com to be deleted. This is necessary, since on a slow system the // UnloadACK event can arrive after the DidCommitUrlReplacer instance below // is created. The replacer code has checks to ensure that all frames being // deleted it has seen being created, which with delayed UnloadACK is // violated. initial_frame_deleted_observer.WaitUntilDeleted(); // Create the interceptor object which will replace the URL of the subsequent // navigation with bar.com based URL. GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html")); DidCommitUrlReplacer url_replacer(shell()->web_contents(), bar_url); // Navigate to another URL within foo.com, which would usually be committed // successfully, but when the URL is modified it should result in the // termination of the renderer process. RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()); EXPECT_FALSE(NavigateToURL( shell(), embedded_test_server()->GetURL("foo.com", "/title2.html"))); EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait()); } // Test which verifies that when an exploited renderer process sends a commit // message with URL that the process is not allowed to commit. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DISABLED_DidCommitInvalidURLWithOpaqueOrigin) { // Explicitly isolating foo.com helps ensure that this test is applicable on // platforms without site-per-process. IsolateOrigin("foo.com"); RenderFrameDeletedObserver initial_frame_deleted_observer( shell()->web_contents()->GetPrimaryMainFrame()); // Test assumes the initial RenderFrameHost to be deleted. Disable // back-forward cache to ensure that it doesn't get preserved in the cache. DisableBackForwardCacheForTesting(shell()->web_contents(), BackForwardCache::TEST_REQUIRES_NO_CACHING); // Navigate to foo.com initially. GURL foo_url(embedded_test_server()->GetURL("foo.com", "/page_with_blank_iframe.html")); EXPECT_TRUE(NavigateToURL(shell(), foo_url)); // Wait for the RenderFrameHost which was current before the navigation to // foo.com to be deleted. This is necessary, since on a slow system the // UnloadACK event can arrive after the DidCommitUrlReplacer instance below // is created. The replacer code has checks to ensure that all frames being // deleted it has seen being created, which with delayed UnloadACK is // violated. initial_frame_deleted_observer.WaitUntilDeleted(); // Create the interceptor object which will replace the URL of the subsequent // navigation with bar.com based URL. GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html")); DidCommitUrlReplacer url_replacer(shell()->web_contents(), bar_url); // Navigate the subframe to a data URL, which would usually be committed // successfully in the same process as foo.com, but when the URL is modified // it should result in the termination of the renderer process. RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()); // Using BeginNavigateIframeToURL is necessary here, since the process // termination will result in DidFinishNavigation notification with the // navigation not in "committed" state. NavigateIframeToURL waits for the // navigation to complete and ignores non-committed navigations, therefore // it will wait indefinitely. GURL data_url(R"(data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E)"); EXPECT_TRUE(BeginNavigateIframeToURL(shell()->web_contents(), "test_iframe", data_url)); EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait()); } // Test which verifies that a WebUI process cannot send a commit message with // URL for a web document. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, WebUIProcessDidCommitWebURL) { // Navigate to a WebUI document. GURL webui_url(GetWebUIURL(kChromeUIGpuHost)); EXPECT_TRUE(NavigateToURL(shell(), webui_url)); // Create the interceptor object which will replace the URL of the subsequent // navigation with |web_url|. GURL web_url(embedded_test_server()->GetURL("foo.com", "/title3.html")); DidCommitUrlReplacer url_replacer(shell()->web_contents(), web_url); // Navigate to another URL within the WebUI, which would usually be committed // successfully, but when the URL is modified it should result in the // termination of the renderer process. RenderProcessHostBadIpcMessageWaiter kill_waiter( shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()); GURL second_webui_url(webui_url.Resolve("/foo")); EXPECT_FALSE(NavigateToURL(shell(), second_webui_url)); EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait()); } // Test that verifies that if a RenderFrameHost is incorrectly given WebUI // bindings the browser process crashes due to CHECK enforcements. IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, AllowBindingsForNonWebUIProcess) { // Navigate to a web URL. GURL initial_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), initial_url)); // Grant WebUI bindings to the frame to simulate a bug in the code that // incorrectly does it and verify the browser process crashes. EXPECT_NOTREACHED_DEATH( shell()->web_contents()->GetPrimaryMainFrame()->AllowBindings( BindingsPolicySet({BindingsPolicyValue::kWebUi}))); } // Tests that a web page cannot bind to a WebUI interface if a WebUI page is the // currently committed RenderFrameHost in the tab (https://crbug.com/1225929). IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, BindToWebUIFromWebViaMojo) { // Navigate to a non-privileged web page, and simulate a renderer compromise // by granting MojoJS. GURL web_url(embedded_test_server()->GetURL("a.com", "/title1.html")); TestNavigationManager navigation(shell()->web_contents(), web_url); shell()->LoadURL(web_url); EXPECT_TRUE(navigation.WaitForResponse()); RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); main_frame->GetFrameBindingsControl()->EnableMojoJsBindings(nullptr); ASSERT_TRUE(navigation.WaitForNavigationFinished()); // Open a popup so that the process won't exit on its own when leaving. OpenBlankWindow(static_cast<WebContentsImpl*>(shell()->web_contents())); // When the page unloads (after the cross-process navigation to an actual // WebUI page below), try to bind to a WebUI interface from the web // RenderFrameHost. Ensure the unload timer and bfcache are disabled so that // the handler has a chance to run. // This test uses `pagehide` rather than `unload` since they occur at the // same timing but `unload` is being deprecated. main_frame->DisableUnloadTimerForTesting(); DisableBackForwardCacheForTesting(shell()->web_contents(), BackForwardCache::TEST_REQUIRES_NO_CACHING); ASSERT_TRUE(ExecJs(main_frame, R"( // Intentionally leak pipe as a global so it doesn't get GCed. newMessagePipe = Mojo.createMessagePipe(); onpagehide = function () { Mojo.bindInterface('mojom.ProcessInternalsHandler', newMessagePipe.handle0); }; )")); // Now navigate to a WebUI page and expect the previous renderer process to be // killed when asking to bind to the WebUI interface. GURL webui_url( GetWebUIURL(kChromeUIProcessInternalsHost).Resolve("#general")); RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess()); EXPECT_TRUE(NavigateToURL(shell(), webui_url)); // Verify that the previous renderer was terminated. EXPECT_EQ(bad_message::RFH_INVALID_WEB_UI_CONTROLLER, kill_waiter.Wait()); } class BeginNavigationTransitionReplacer : public FrameHostInterceptor { public: BeginNavigationTransitionReplacer(WebContents* web_contents, ui::PageTransition transition_to_inject) : FrameHostInterceptor(web_contents), transition_to_inject_(transition_to_inject) {} BeginNavigationTransitionReplacer(const BeginNavigationTransitionReplacer&) = delete; BeginNavigationTransitionReplacer& operator=( const BeginNavigationTransitionReplacer&) = delete; bool WillDispatchBeginNavigation( RenderFrameHost* render_frame_host, blink::mojom::CommonNavigationParamsPtr* common_params, blink::mojom::BeginNavigationParamsPtr* begin_params, mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token, mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client) override { if (is_activated_) { (*common_params)->transition = transition_to_inject_; is_activated_ = false; } return true; } void Activate() { is_activated_ = true; } private: ui::PageTransition transition_to_inject_; bool is_activated_ = false; }; 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->GetPrimaryMainFrame()->GetProcess(); RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); // Have the main frame submit a BeginNavigation IPC with a missing // initiator. injector.Activate(); // Don't expect a response for the script, as the process may be killed // before the script sends its completion message. ExecuteScriptAsync(web_contents, "window.location = '/title2.html';"); // Verify that the renderer was terminated. EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_NON_WEBBY_TRANSITION, kill_waiter.Wait()); } } class SecurityExploitViaDisabledWebSecurityTest : public SecurityExploitBrowserTest { public: SecurityExploitViaDisabledWebSecurityTest() { // To get around BlockedSchemeNavigationThrottle. Other attempts at getting // around it don't work, i.e.: // -if the request is made in a child frame then the frame is torn down // immediately on process killing so the navigation doesn't complete // -if it's classified as same document, then a DCHECK in // NavigationRequest::CreateRendererInitiated fires feature_list_.InitAndEnableFeature( features::kAllowContentInitiatedDataUrlNavigations); } protected: void SetUpCommandLine(base::CommandLine* command_line) override { // Simulate a compromised renderer, otherwise the cross-origin request to // file: is blocked. command_line->AppendSwitch(switches::kDisableWebSecurity); SecurityExploitBrowserTest::SetUpCommandLine(command_line); } private: base::test::ScopedFeatureList feature_list_; }; // Test to verify that an exploited renderer process trying to specify a // non-empty URL for base_url_for_data_url on navigation is correctly // terminated. IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest, ValidateBaseUrlForDataUrl) { GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); GURL data_url("data:text/html,foo"); base::FilePath file_path = GetTestFilePath("", "simple_page.html"); GURL file_url = net::FilePathToFileURL(file_path); // Setup a BeginNavigate IPC with non-empty base_url_for_data_url. blink::mojom::CommonNavigationParamsPtr common_params = blink::mojom::CommonNavigationParams::New( data_url, url::Origin::Create(data_url), /* initiator_base_url= */ std::nullopt, blink::mojom::Referrer::New(), ui::PAGE_TRANSITION_LINK, blink::mojom::NavigationType::DIFFERENT_DOCUMENT, blink::NavigationDownloadPolicy(), false /* should_replace_current_entry */, file_url, /* base_url_for_data_url */ base::TimeTicks::Now() /* navigation_start */, "GET", nullptr /* post_data */, network::mojom::SourceLocation::New(), false /* started_from_context_menu */, false /* has_user_gesture */, false /* text_fragment_token */, network::mojom::CSPDisposition::CHECK, std::vector<int>() /* initiator_origin_trial_features */, std::string() /* href_translate */, false /* is_history_navigation_in_new_child_frame */, base::TimeTicks() /* input_start */, network::mojom::RequestDestination::kDocument); blink::mojom::BeginNavigationParamsPtr begin_params = blink::mojom::BeginNavigationParams::New( std::nullopt /* initiator_frame_token */, std::string() /* headers */, net::LOAD_NORMAL, false /* skip_service_worker */, blink::mojom::RequestContextType::LOCATION, blink::mojom::MixedContentContextType::kBlockable, false /* is_form_submission */, false /* was_initiated_by_link_click */, blink::mojom::ForceHistoryPush::kNo, GURL() /* searchable_form_url */, std::string() /* searchable_form_encoding */, GURL() /* client_side_redirect_url */, std::nullopt /* devtools_initiator_info */, nullptr /* trust_token_params */, std::nullopt /* impression */, base::TimeTicks() /* renderer_before_unload_start */, base::TimeTicks() /* renderer_before_unload_end */, blink::mojom::NavigationInitiatorActivationAndAdStatus:: kDidNotStartWithTransientActivation, false /* is_container_initiated */, net::StorageAccessApiStatus::kNone, false /* has_rel_opener */); // Receiving the invalid IPC message should lead to renderer process // termination. RenderProcessHostBadIpcMessageWaiter 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(), mojo::NullReceiver()); EXPECT_EQ(bad_message::RFH_BASE_URL_FOR_DATA_URL_SPECIFIED, process_kill_waiter.Wait()); EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile( rfh->GetProcess()->GetDeprecatedID(), file_path)); // Reload the page to create another renderer process. TestNavigationObserver tab_observer(shell()->web_contents(), 1); shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false); tab_observer.Wait(); // Make an XHR request to check if the page has access. std::string script = base::StringPrintf( "var xhr = new XMLHttpRequest()\n" "xhr.open('GET', '%s', false);\n" "try { xhr.send(); } catch (e) {}\n" "xhr.responseText;", file_url.spec().c_str()); std::string result = EvalJs(shell()->web_contents(), script).ExtractString(); EXPECT_TRUE(result.empty()); } // Test to verify that an exploited renderer process trying to specify a // empty URL for initiator_base_url on navigation is correctly terminated. IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest, ValidateInitiatorBaseUrlNotEmpty) { GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); GURL url("about:blank"); // Setup a BeginNavigate IPC with empty, but not nullopt, initiator_base_url. blink::mojom::CommonNavigationParamsPtr common_params = blink::mojom::CommonNavigationParams::New( url, url::Origin::Create(start_url), /* initiator_base_url= */ GURL(), blink::mojom::Referrer::New(), ui::PAGE_TRANSITION_LINK, blink::mojom::NavigationType::DIFFERENT_DOCUMENT, blink::NavigationDownloadPolicy(), false /* should_replace_current_entry */, GURL(), /* base_url_for_data_url */ base::TimeTicks::Now() /* navigation_start */, "GET", nullptr /* post_data */, network::mojom::SourceLocation::New(), false /* started_from_context_menu */, false /* has_user_gesture */, false /* text_fragment_token */, network::mojom::CSPDisposition::CHECK, std::vector<int>() /* initiator_origin_trial_features */, std::string() /* href_translate */, false /* is_history_navigation_in_new_child_frame */, base::TimeTicks() /* input_start */, network::mojom::RequestDestination::kDocument); blink::mojom::BeginNavigationParamsPtr begin_params = blink::mojom::BeginNavigationParams::New( std::nullopt /* initiator_frame_token */, std::string() /* headers */, net::LOAD_NORMAL, false /* skip_service_worker */, blink::mojom::RequestContextType::LOCATION, blink::mojom::MixedContentContextType::kBlockable, false /* is_form_submission */, false /* was_initiated_by_link_click */, blink::mojom::ForceHistoryPush::kNo, GURL() /* searchable_form_url */, std::string() /* searchable_form_encoding */, GURL() /* client_side_redirect_url */, std::nullopt /* devtools_initiator_info */, nullptr /* trust_token_params */, std::nullopt /* impression */, base::TimeTicks() /* renderer_before_unload_start */, base::TimeTicks() /* renderer_before_unload_end */, blink::mojom::NavigationInitiatorActivationAndAdStatus:: kDidNotStartWithTransientActivation, false /* is_container_initiated */, net::StorageAccessApiStatus::kNone, false /* has_rel_opener */); // Receiving the invalid IPC message should lead to renderer process // termination. RenderProcessHostBadIpcMessageWaiter 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(), mojo::NullReceiver()); EXPECT_EQ(bad_message::RFH_INITIATOR_BASE_URL_IS_EMPTY, process_kill_waiter.Wait()); } // 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->GetPrimaryMainFrame()->GetLastCommittedURL()); EXPECT_EQ( shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(), new_window->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), new_window->GetPrimaryMainFrame()->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->GetPrimaryMainFrame()->GetLastCommittedURL()); EXPECT_EQ( shell()->web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(), new_window->GetPrimaryMainFrame()->GetLastCommittedOrigin()); EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), new_window->GetPrimaryMainFrame()->GetProcess()); // Even though the navigation is blocked, we expect the opener relationship to // be established between the 2 windows. EXPECT_EQ(true, ExecJs(new_window, "!!window.opener")); } class BeginNavigationTrustTokenParamsReplacer : public FrameHostInterceptor { public: BeginNavigationTrustTokenParamsReplacer( WebContents* web_contents, network::mojom::TrustTokenParamsPtr params_to_inject) : FrameHostInterceptor(web_contents), params_to_inject_(std::move(params_to_inject)) {} BeginNavigationTrustTokenParamsReplacer( const BeginNavigationTrustTokenParamsReplacer&) = delete; BeginNavigationTrustTokenParamsReplacer& operator=( const BeginNavigationTrustTokenParamsReplacer&) = delete; bool WillDispatchBeginNavigation( RenderFrameHost* render_frame_host, blink::mojom::CommonNavigationParamsPtr* common_params, blink::mojom::BeginNavigationParamsPtr* begin_params, mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token, mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client) override { if (is_activated_) { (*begin_params)->trust_token_params = params_to_inject_.Clone(); is_activated_ = false; } return true; } void Activate() { is_activated_ = true; } private: network::mojom::TrustTokenParamsPtr params_to_inject_; bool is_activated_ = false; }; class SecurityExploitBrowserTestWithTrustTokensEnabled : public SecurityExploitBrowserTest { public: SecurityExploitBrowserTestWithTrustTokensEnabled() = default; }; // Test that the browser correctly reports a bad message when a child frame // attempts to navigate with a Private State Tokens redemption operation // associated with the navigation, but its parent lacks the // private-state-token-redemption Permissions Policy feature. IN_PROC_BROWSER_TEST_F( SecurityExploitBrowserTestWithTrustTokensEnabled, BrowserForbidsTrustTokenRedemptionWithoutPermissionsPolicy) { WebContents* web_contents = shell()->web_contents(); // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. auto params = network::mojom::TrustTokenParams::New(); params->operation = network::mojom::TrustTokenOperationType::kRedemption; BeginNavigationTrustTokenParamsReplacer replacer(web_contents, std::move(params)); GURL start_url(embedded_test_server()->GetURL( "/page-with-trust-token-permissions-policy-disabled.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHost* parent = web_contents->GetPrimaryMainFrame(); ASSERT_FALSE(parent->IsFeatureEnabled( network::mojom::PermissionsPolicyFeature::kTrustTokenRedemption)); replacer.Activate(); RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents) ->GetPrimaryFrameTree() .root() ->child_at(0) ->current_frame_host(); ExecuteScriptAsync(child, JsReplace("location = $1", "/title2.html")); RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess()); EXPECT_THAT(kill_waiter.Wait(), Optional(HasSubstr("Permissions Policy feature is absent"))); } // Test that the browser correctly reports a bad message when a child frame // attempts to navigate with a Private State Tokens signing operation associated // with the navigation, but its parent lacks the private-state-token-redemption // (sic) Permissions Policy feature. IN_PROC_BROWSER_TEST_F( SecurityExploitBrowserTestWithTrustTokensEnabled, BrowserForbidsTrustTokenSigningWithoutPermissionsPolicy) { WebContents* web_contents = shell()->web_contents(); // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. auto params = network::mojom::TrustTokenParams::New(); params->operation = network::mojom::TrustTokenOperationType::kSigning; BeginNavigationTrustTokenParamsReplacer replacer(web_contents, std::move(params)); GURL start_url(embedded_test_server()->GetURL( "/page-with-trust-token-permissions-policy-disabled.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHost* parent = web_contents->GetPrimaryMainFrame(); ASSERT_FALSE(parent->IsFeatureEnabled( network::mojom::PermissionsPolicyFeature::kTrustTokenRedemption)); replacer.Activate(); RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents) ->GetPrimaryFrameTree() .root() ->child_at(0) ->current_frame_host(); ExecuteScriptAsync(child, JsReplace("location = $1", "/title2.html")); RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess()); EXPECT_THAT(kill_waiter.Wait(), Optional(HasSubstr("Permissions Policy feature is absent"))); } // Test that the browser correctly reports a bad message when a child frame // attempts to navigate with a Private State Tokens issue operation // associated with the navigation, but its parent lacks the // private-state-token-issuance Permissions Policy feature. IN_PROC_BROWSER_TEST_F( SecurityExploitBrowserTestWithTrustTokensEnabled, BrowserForbidsTrustTokenIssuanceWithoutPermissionsPolicy) { WebContents* web_contents = shell()->web_contents(); // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. auto params = network::mojom::TrustTokenParams::New(); params->operation = network::mojom::TrustTokenOperationType::kIssuance; BeginNavigationTrustTokenParamsReplacer replacer(web_contents, std::move(params)); GURL start_url(embedded_test_server()->GetURL( "/page-with-trust-token-permissions-policy-disabled.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHost* parent = web_contents->GetPrimaryMainFrame(); ASSERT_FALSE(parent->IsFeatureEnabled( network::mojom::PermissionsPolicyFeature::kPrivateStateTokenIssuance)); replacer.Activate(); RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents) ->GetPrimaryFrameTree() .root() ->child_at(0) ->current_frame_host(); ExecuteScriptAsync(child, JsReplace("location = $1", "/title2.html")); RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess()); EXPECT_THAT(kill_waiter.Wait(), Optional(HasSubstr("Permissions Policy feature is absent"))); } IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWithTrustTokensEnabled, BrowserForbidsTrustTokenParamsOnMainFrameNav) { WebContents* web_contents = shell()->web_contents(); // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. BeginNavigationTrustTokenParamsReplacer replacer( web_contents, network::mojom::TrustTokenParams::New()); GURL start_url(embedded_test_server()->GetURL("/title1.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); replacer.Activate(); RenderFrameHost* compromised_renderer = web_contents->GetPrimaryMainFrame(); ExecuteScriptAsync(compromised_renderer, JsReplace("location = $1", "/title2.html")); RenderProcessHostBadMojoMessageWaiter kill_waiter( compromised_renderer->GetProcess()); EXPECT_THAT( kill_waiter.Wait(), Optional(HasSubstr("Private State Token params in main frame nav"))); } class FencedFrameSecurityExploitBrowserTestWithTrustTokensEnabled : public SecurityExploitBrowserTestWithTrustTokensEnabled { protected: FencedFrameSecurityExploitBrowserTestWithTrustTokensEnabled() = default; WebContentsImpl* web_contents() { return static_cast<WebContentsImpl*>(shell()->web_contents()); } RenderFrameHostImpl* primary_main_frame_host() { return web_contents()->GetPrimaryMainFrame(); } test::FencedFrameTestHelper& fenced_frame_test_helper() { return fenced_frame_test_helper_; } private: test::FencedFrameTestHelper fenced_frame_test_helper_; }; class FencedFrameBeginNavigationTrustTokenParamsReplacer : public BeginNavigationTrustTokenParamsReplacer { public: FencedFrameBeginNavigationTrustTokenParamsReplacer( WebContents* web_contents, network::mojom::TrustTokenParamsPtr params_to_inject) : BeginNavigationTrustTokenParamsReplacer(web_contents, std::move(params_to_inject)) {} FencedFrameBeginNavigationTrustTokenParamsReplacer( const FencedFrameBeginNavigationTrustTokenParamsReplacer&) = delete; FencedFrameBeginNavigationTrustTokenParamsReplacer& operator=( const FencedFrameBeginNavigationTrustTokenParamsReplacer&) = delete; bool WillDispatchBeginNavigation( RenderFrameHost* render_frame_host, blink::mojom::CommonNavigationParamsPtr* common_params, blink::mojom::BeginNavigationParamsPtr* begin_params, mojo::PendingRemote<blink::mojom::BlobURLToken>* blob_url_token, mojo::PendingAssociatedRemote<mojom::NavigationClient>* navigation_client) override { if (render_frame_host->IsFencedFrameRoot()) { BeginNavigationTrustTokenParamsReplacer::WillDispatchBeginNavigation( render_frame_host, common_params, begin_params, blob_url_token, navigation_client); } return true; } }; IN_PROC_BROWSER_TEST_F( FencedFrameSecurityExploitBrowserTestWithTrustTokensEnabled, BrowserForbidsTrustTokenParamsOnFencedFrameNav) { WebContents* web_contents = shell()->web_contents(); // Prepare to intercept BeginNavigation mojo IPC. This has to be done before // the test creates the RenderFrameHostImpl that is the target of the IPC. FencedFrameBeginNavigationTrustTokenParamsReplacer replacer( web_contents, network::mojom::TrustTokenParams::New()); GURL start_url(embedded_test_server()->GetURL("/empty.html")); EXPECT_TRUE(NavigateToURL(shell(), start_url)); RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); RenderFrameHostImplWrapper inner_fenced_frame_rfh( fenced_frame_test_helper().CreateFencedFrame( primary_rfh.get(), embedded_test_server()->GetURL("/fenced_frames/empty.html"))); RenderFrameHost* compromised_renderer = inner_fenced_frame_rfh.get(); RenderProcessHostBadMojoMessageWaiter kill_waiter( compromised_renderer->GetProcess()); replacer.Activate(); std::ignore = ExecJs( compromised_renderer, JsReplace("location.href=$1", embedded_test_server()->GetURL("/fenced_frames/title1.html"))); std::optional<std::string> result = kill_waiter.Wait(); EXPECT_THAT(result, Optional(HasSubstr("Private State Token params in fenced frame " "nav"))); } class SecurityExploitTestFencedFramesDisabled : public SecurityExploitBrowserTest { public: SecurityExploitTestFencedFramesDisabled() { feature_list_.InitAndDisableFeature(blink::features::kFencedFrames); } private: base::test::ScopedFeatureList feature_list_; }; // Ensure that we kill the renderer process if we try to create a // fenced-frame when the blink::features::kFencedFrames feature is not enabled. IN_PROC_BROWSER_TEST_F(SecurityExploitTestFencedFramesDisabled, CreateFencedFrameWhenFeatureDisabled) { GURL foo("http://foo.com/simple_page.html"); EXPECT_TRUE(NavigateToURL(shell(), foo)); EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle()); EXPECT_FALSE(blink::features::IsFencedFramesEnabled()); RenderFrameHostImpl* compromised_rfh = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); mojo::PendingAssociatedRemote<blink::mojom::FencedFrameOwnerHost> remote; mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost> receiver; receiver = remote.InitWithNewEndpointAndPassReceiver(); auto remote_frame_interfaces = blink::mojom::RemoteFrameInterfacesFromRenderer::New(); remote_frame_interfaces->frame_host_receiver = mojo::AssociatedRemote<blink::mojom::RemoteFrameHost>() .BindNewEndpointAndPassDedicatedReceiver(); mojo::AssociatedRemote<blink::mojom::RemoteFrame> frame; std::ignore = frame.BindNewEndpointAndPassDedicatedReceiver(); remote_frame_interfaces->frame = frame.Unbind(); RenderProcessHostBadIpcMessageWaiter kill_waiter( compromised_rfh->GetProcess()); static_cast<blink::mojom::LocalFrameHost*>(compromised_rfh) ->CreateFencedFrame( std::move(receiver), std::move(remote_frame_interfaces), blink::RemoteFrameToken(), base::UnguessableToken::Create()); EXPECT_EQ(bad_message::RFH_FENCED_FRAME_MOJO_WHEN_DISABLED, kill_waiter.Wait()); } // Ensure that we kill the renderer process if we try to do a top-level // navigation using the special _unfencedTop IPC path when we are not inside // a fenced frame. (Test from an iframe instead.) IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, UnfencedTopFromOutsideFencedFrame) { 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()) ->GetPrimaryFrameTree() .root(); RenderFrameHostImpl* compromised_rfh = root->child_at(0)->current_frame_host(); RenderProcessHostBadIpcMessageWaiter kill_waiter( compromised_rfh->GetProcess()); GURL url("http://foo.com/simple_page.html"); auto params = CreateOpenURLParams(url); params->is_unfenced_top_navigation = true; static_cast<mojom::FrameHost*>(compromised_rfh)->OpenURL(std::move(params)); EXPECT_EQ(bad_message::RFHI_UNFENCED_TOP_IPC_OUTSIDE_FENCED_FRAME, kill_waiter.Wait()); } class SecurityExploitBrowserTestFencedFrames : public SecurityExploitBrowserTest { public: void SetUpOnMainThread() override { host_resolver()->AddRule("*", "127.0.0.1"); https_server()->StartAcceptingConnections(); } void SetUpCommandLine(base::CommandLine* command_line) override { https_server()->AddDefaultHandlers(GetTestDataFilePath()); https_server()->ServeFilesFromSourceDirectory(GetTestDataFilePath()); https_server()->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES); SetupCrossSiteRedirector(https_server()); // 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(https_server()->InitializeAndListen()); } test::FencedFrameTestHelper& fenced_frame_test_helper() { return fenced_frame_test_helper_; } net::EmbeddedTestServer* https_server() { return &https_server_; } private: test::FencedFrameTestHelper fenced_frame_test_helper_{}; net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; }; IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames, NavigateFencedFrameToInvalidURL) { GURL main_frame_url(https_server()->GetURL("a.test", "/simple_page.html")); std::vector<GURL> invalid_urls = { GURL("http://example.com"), GURL("http://example.com?<\n=block"), GURL("about:srcdoc"), GURL("data:text/html,<p>foo"), GURL("blob:https://example.com/a9400bf5-aaa8-4166-86e4-492c50f4ca2b"), GURL("file://folder"), GURL("javascript:console.log('foo');"), GetWebUIURL(kChromeUIHistogramHost), GURL(blink::kChromeUIHangURL)}; for (const GURL& invalid_url : invalid_urls) { EXPECT_FALSE(blink::IsValidFencedFrameURL(invalid_url)); EXPECT_TRUE(blink::features::IsFencedFramesEnabled()); EXPECT_TRUE(NavigateToURL(shell(), main_frame_url)); EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle()); RenderFrameHostImpl* compromised_rfh = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); mojo::AssociatedRemote<blink::mojom::FencedFrameOwnerHost> remote; mojo::PendingAssociatedReceiver<blink::mojom::FencedFrameOwnerHost> pending_receiver = remote.BindNewEndpointAndPassReceiver(); auto remote_frame_interfaces = blink::mojom::RemoteFrameInterfacesFromRenderer::New(); remote_frame_interfaces->frame_host_receiver = mojo::AssociatedRemote<blink::mojom::RemoteFrameHost>() .BindNewEndpointAndPassDedicatedReceiver(); mojo::AssociatedRemote<blink::mojom::RemoteFrame> frame; std::ignore = frame.BindNewEndpointAndPassDedicatedReceiver(); remote_frame_interfaces->frame = frame.Unbind(); RenderProcessHostBadIpcMessageWaiter kill_waiter( compromised_rfh->GetProcess()); static_cast<blink::mojom::LocalFrameHost*>(compromised_rfh) ->CreateFencedFrame( std::move(pending_receiver), std::move(remote_frame_interfaces), blink::RemoteFrameToken(), base::UnguessableToken::Create()); EXPECT_EQ(compromised_rfh->GetFencedFrames().size(), 1u); FencedFrame* fenced_frame = compromised_rfh->GetFencedFrames()[0]; static_cast<blink::mojom::FencedFrameOwnerHost*>(fenced_frame) ->Navigate(invalid_url, base::TimeTicks(), /*embedder_shared_storage_context=*/std::nullopt); EXPECT_EQ(bad_message::FF_NAVIGATION_INVALID_URL, kill_waiter.Wait()); } } IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames, ChangeFencedFrameSandboxFlags) { GURL main_frame_url(https_server()->GetURL("a.test", "/simple_page.html")); EXPECT_TRUE(NavigateToURL(shell(), main_frame_url)); RenderFrameHostImpl* root_rfh = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); const GURL fenced_frame_url = https_server()->GetURL("a.test", "/fenced_frames/sandbox_flags.html"); constexpr char kAddFencedFrameScript[] = R"({ const fenced_frame = document.createElement('fencedframe'); fenced_frame.config = new FencedFrameConfig($1); document.body.appendChild(fenced_frame); })"; EXPECT_TRUE( ExecJs(root_rfh, JsReplace(kAddFencedFrameScript, fenced_frame_url))); RenderFrameHostImpl* fenced_rfh = nullptr; RenderFrameHostImpl* parent_rfh = nullptr; std::vector<FencedFrame*> fenced_frames = root_rfh->GetFencedFrames(); EXPECT_EQ(fenced_frames.size(), 1u); FencedFrame* new_fenced_frame = fenced_frames.back(); fenced_rfh = new_fenced_frame->GetInnerRoot(); parent_rfh = fenced_rfh; RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess()); blink::FramePolicy first_policy = fenced_rfh->frame_tree_node()->pending_frame_policy(); first_policy.sandbox_flags = blink::kFencedFrameMandatoryUnsandboxedFlags; static_cast<blink::mojom::LocalFrameHost*>(parent_rfh) ->DidChangeFramePolicy(std::move(fenced_rfh->GetFrameToken()), std::move(first_policy)); EXPECT_EQ(bad_message::RFH_SANDBOX_FLAGS, kill_waiter.Wait()); } IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestFencedFrames, PullFocusAcrossFencedBoundary) { base::HistogramTester histogram_tester; GURL main_frame_url(https_server()->GetURL("a.test", "/simple_page.html")); EXPECT_TRUE(NavigateToURL(shell(), main_frame_url)); RenderFrameHostImpl* root_rfh = static_cast<RenderFrameHostImpl*>( shell()->web_contents()->GetPrimaryMainFrame()); const GURL fenced_frame_url = https_server()->GetURL("a.test", "/fenced_frames/button.html"); constexpr char kAddFencedFrameScript[] = R"({ const fenced_frame = document.createElement('fencedframe'); fenced_frame.config = new FencedFrameConfig($1); document.body.appendChild(fenced_frame); })"; EXPECT_TRUE( ExecJs(root_rfh, JsReplace(kAddFencedFrameScript, fenced_frame_url))); RenderFrameHostImpl* fenced_rfh = nullptr; std::vector<FencedFrame*> fenced_frames = root_rfh->GetFencedFrames(); EXPECT_EQ(fenced_frames.size(), 1u); FencedFrame* new_fenced_frame = fenced_frames.back(); fenced_rfh = new_fenced_frame->GetInnerRoot(); root_rfh->DidFocusFrame(); root_rfh->GetRenderWidgetHost()->ResetLostFocus(); // The fenced frame should not be allowed to focus because it won't have // user activation, and the RenderWidgetHost won't have recently lost focus. RenderProcessHostBadIpcMessageWaiter kill_waiter(fenced_rfh->GetProcess()); fenced_rfh->DidFocusFrame(); EXPECT_EQ(bad_message::RFH_FOCUS_ACROSS_FENCED_BOUNDARY, kill_waiter.Wait()); } } // namespace content