| // 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 <atomic> |
| #include <optional> |
| #include <tuple> |
| |
| #include "base/command_line.h" |
| #include "base/debug/dump_without_crashing.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/ref_counted.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_util.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/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/test_future.h" |
| #include "base/unguessable_token.h" |
| #include "build/build_config.h" |
| #include "content/browser/bad_message.h" |
| #include "content/browser/browser_interface_broker_impl.h" |
| #include "content/browser/dom_storage/dom_storage_context_wrapper.h" |
| #include "content/browser/dom_storage/session_storage_namespace_handle_impl.h" |
| #include "content/browser/permissions/permission_service_context.h" |
| #include "content/browser/renderer_host/file_utilities_host_impl.h" |
| #include "content/browser/renderer_host/initiator_navigation_state_impl.h" |
| #include "content/browser/renderer_host/navigation_request.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/screen_orientation/screen_orientation_provider.h" |
| #include "content/browser/security/cpsp/child_process_security_policy_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/site_isolation_policy.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/navigation_handle_observer.h" |
| #include "content/public/test/render_frame_host_test_support.h" |
| #include "content/public/test/test_content_browser_client.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/public/test/url_loader_interceptor.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 "device/gamepad/public/mojom/gamepad.mojom.h" |
| #include "ipc/constants.mojom.h" |
| #include "mojo/core/embedder/embedder.h" |
| #include "mojo/public/cpp/bindings/associated_receiver.h" |
| #include "mojo/public/cpp/bindings/associated_remote.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/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/system/data_pipe_utils.h" |
| #include "mojo/public/cpp/test_support/test_utils.h" |
| #include "net/base/features.h" |
| #include "net/base/filename_util.h" |
| #include "net/base/net_errors.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/device/public/mojom/battery_monitor.mojom.h" |
| #include "services/device/public/mojom/screen_orientation.mojom.h" |
| #include "services/device/public/mojom/vibration_manager.mojom.h" |
| #include "services/network/public/cpp/network_switches.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/resource_request_body.h" |
| #include "services/network/public/mojom/fetch_api.mojom.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.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 "storage/browser/blob/blob_url_registry.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/blob/blob_utils.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/navigation/navigation_policy.h" |
| #include "third_party/blink/public/common/page_state/page_state_serialization.h" |
| #include "third_party/blink/public/mojom/blob/blob.mojom.h" |
| #include "third_party/blink/public/mojom/blob/blob_url_store.mojom.h" |
| #include "third_party/blink/public/mojom/blob/data_element.mojom.h" |
| #include "third_party/blink/public/mojom/blob/file_backed_blob_factory.mojom.h" |
| #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h" |
| #include "third_party/blink/public/mojom/content_extraction/script_tools.mojom.h" |
| #include "third_party/blink/public/mojom/file/file_utilities.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/frame_replication_state.mojom.h" |
| #include "third_party/blink/public/mojom/frame/fullscreen.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" |
| #include "third_party/blink/public/mojom/permissions/permission.mojom.h" |
| |
| 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, |
| /* initiator_navigation_state= */ nullptr, 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 */, false /* has_rel_opener */, |
| false /* started_by_ad */); |
| 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, |
| const RenderFrameHostImpl* initiator) { |
| auto params = blink::mojom::OpenURLParams::New(); |
| params->url = url; |
| params->disposition = WindowOpenDisposition::CURRENT_TAB; |
| params->should_replace_current_entry = false; |
| params->user_gesture = true; |
| params->initiator_state_token = initiator->current_initiator_state_token(); |
| params->initiator_document_token = initiator->GetDocumentToken(); |
| 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() { |
| feature_list_.InitWithFeatures({features::kKillOnUnexpectedOriginHeader}, |
| {}); |
| } |
| |
| 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}); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| class SecurityExploitBrowserTestWebMCPDisabled |
| : public SecurityExploitBrowserTest { |
| public: |
| SecurityExploitBrowserTestWebMCPDisabled() { |
| scoped_feature_list_.InitAndDisableFeature(blink::features::kWebMCP); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| class SecurityExploitBrowserTestWebMCPEnabled |
| : public SecurityExploitBrowserTest { |
| public: |
| SecurityExploitBrowserTestWebMCPEnabled() { |
| scoped_feature_list_.InitAndEnableFeature(blink::features::kWebMCP); |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| 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. |
| // |
| // TODO(crbug.com/485081313): Flaky. Reenable it. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| DISABLED_AttemptDuplicateRenderViewHost) { |
| int32_t duplicate_routing_id = IPC::mojom::kRoutingIdNone; |
| RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts( |
| shell(), embedded_test_server(), &duplicate_routing_id); |
| EXPECT_NE(IPC::mojom::kRoutingIdNone, 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 |
| } |
| |
| // Ensure that we kill the renderer process if it tries to open a file chooser |
| // with Mode::kSave, which is not allowed for renderer-initiated choosers. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, FileChooserWithSaveMode) { |
| GURL foo("http://foo.com/simple_page.html"); |
| EXPECT_TRUE(NavigateToURL(shell(), foo)); |
| |
| RenderFrameHost* compromised_renderer = |
| shell()->web_contents()->GetPrimaryMainFrame(); |
| blink::mojom::FileChooserParamsPtr params = |
| blink::mojom::FileChooserParams::New(); |
| params->mode = blink::mojom::FileChooserParams::Mode::kSave; |
| params->default_file_name = base::FilePath(FILE_PATH_LITERAL("file.txt")); |
| |
| 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_EQ("FileChooser: Save mode is not allowed.", |
| bad_message_observer.WaitForBadMessage()); |
| } |
| |
| // 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. |
| } |
| |
| // Test that calling `RegisterScriptTool()` when the `tools` permission policy |
| // is not enabled properly terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| RegisterScriptToolPermissionPolicyDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| // Create a cross-origin iframe to b.com. `tools` permission is enabled for |
| // 'self' by default, so it will be disabled for the cross-origin frame. |
| GURL subframe_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| RenderFrameHostImpl* subframe = static_cast<RenderFrameHostImpl*>( |
| CreateSubframe(static_cast<WebContentsImpl*>(shell()->web_contents()), |
| "child1", subframe_url, true /* wait_for_navigation */)); |
| |
| EXPECT_FALSE(subframe->IsFeatureEnabled( |
| network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| |
| auto tool = blink::mojom::ScriptTool::New(); |
| tool->name = "test-tool"; |
| tool->description = "test description"; |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| subframe->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing()); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `RegisterScriptTool()` when the `WebMCP` base::Feature is |
| // disabled (but permissions policy is enabled) properly terminates the |
| // renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPDisabled, |
| RegisterScriptToolWebMCPDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| auto tool = blink::mojom::ScriptTool::New(); |
| tool->name = "test-tool"; |
| tool->description = "test description"; |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing()); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `UnregisterScriptTool()` when the `WebMCP` base::Feature is |
| // disabled (but permissions policy is enabled) properly terminates the |
| // renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPDisabled, |
| UnregisterScriptToolWebMCPDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->UnregisterScriptTool("some-tool"); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `GetScriptTools()` when the `WebMCP` base::Feature is |
| // disabled (but permissions policy is enabled) properly terminates the |
| // renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPDisabled, |
| GetScriptToolsWebMCPDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->GetScriptTools({}, base::DoNothing()); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `ExecuteRemoteScriptTool()` when the `WebMCP` base::Feature |
| // is disabled (but permissions policy is enabled) properly terminates the |
| // renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPDisabled, |
| ExecuteRemoteScriptToolWebMCPDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| blink::FrameToken tool_owner_frame_token = rfh->GetFrameToken(); |
| |
| script_tool_host->ExecuteRemoteScriptTool( |
| base::UnguessableToken::Create(), tool_owner_frame_token, |
| rfh->GetLastCommittedOrigin(), "test-tool", "[]", |
| base::BindOnce( |
| [](const std::optional<std::string>& result, bool success) {})); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `RegisterScriptTool()` with an invalid |
| // `tool_owner_frame_token` properly terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| RegisterScriptToolInvalidToolOwnerToken) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| auto tool = blink::mojom::ScriptTool::New(); |
| tool->name = "test-tool"; |
| tool->description = "test description"; |
| tool->tool_owner_frame_token = blink::LocalFrameToken(); |
| tool->origin = rfh->GetLastCommittedOrigin(); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing()); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_INVALID_TOOL_OWNER, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `RegisterScriptTool()` with an invalid `origin` |
| // properly terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| RegisterScriptToolInvalidToolOwnerOrigin) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| auto tool = blink::mojom::ScriptTool::New(); |
| tool->name = "test-tool"; |
| tool->description = "test description"; |
| tool->tool_owner_frame_token = rfh->GetFrameToken(); |
| tool->origin = url::Origin::Create(GURL("https://b.com")); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing()); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_INVALID_TOOL_OWNER, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `UnregisterScriptTool()` when the `tools` permission policy |
| // is not enabled properly terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| UnregisterScriptToolPermissionPolicyDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| GURL subframe_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| RenderFrameHostImpl* subframe = static_cast<RenderFrameHostImpl*>( |
| CreateSubframe(static_cast<WebContentsImpl*>(shell()->web_contents()), |
| "child1", subframe_url, true /* wait_for_navigation */)); |
| |
| EXPECT_FALSE(subframe->IsFeatureEnabled( |
| network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| subframe->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->UnregisterScriptTool("test-tool"); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait()); |
| } |
| |
| // Test that binding `ModelContextHost` twice for the same document properly |
| // terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| ModelContextHostDuplicateBind) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> host1; |
| rfh->BindModelContextHost(host1.BindNewPipeAndPassReceiver()); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> host2; |
| rfh->BindModelContextHost(host2.BindNewPipeAndPassReceiver()); |
| |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_DUPLICATE_BIND, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `BindModelContext()` twice for the same document properly |
| // terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| ModelContextHostDuplicateBindModelContext) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| mojo::PendingRemote<blink::mojom::ModelContext> remote1; |
| auto receiver1 = remote1.InitWithNewPipeAndPassReceiver(); |
| script_tool_host->BindModelContext(std::move(remote1)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| mojo::PendingRemote<blink::mojom::ModelContext> remote2; |
| auto receiver2 = remote2.InitWithNewPipeAndPassReceiver(); |
| script_tool_host->BindModelContext(std::move(remote2)); |
| |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_DUPLICATE_SET_RECEIVER, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that calling `GetScriptTools()` when the `tools` permission policy |
| // is not enabled properly terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| GetScriptToolsPermissionPolicyDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| GURL subframe_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| RenderFrameHostImpl* subframe = static_cast<RenderFrameHostImpl*>( |
| CreateSubframe(static_cast<WebContentsImpl*>(shell()->web_contents()), |
| "child1", subframe_url, true /* wait_for_navigation */)); |
| |
| EXPECT_FALSE(subframe->IsFeatureEnabled( |
| network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| subframe->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->GetScriptTools({}, base::DoNothing()); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_NOT_ENABLED, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `UnregisterScriptTool()` with an unknown name properly |
| // terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled, |
| UnregisterUnknownTool) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->UnregisterScriptTool("unknown-tool"); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_UNKNOWN_TOOL_NAME, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `RegisterScriptTool()` with a non-HTTPS origin in |
| // `exposedTo` properly terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled, |
| ModelContextHostExposedNonHttpsOrigin) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| auto tool = blink::mojom::ScriptTool::New(); |
| tool->name = "test-tool"; |
| tool->description = "test description"; |
| tool->exposed_origins.push_back(url::Origin::Create(GURL("http://b.com"))); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing()); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_EXPOSED_UNTRUSTWORTHY_ORIGIN, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that calling `RegisterScriptTool()` with a duplicate name properly |
| // terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled, |
| RegisterDuplicateScriptTool) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| auto tool1 = blink::mojom::ScriptTool::New(); |
| tool1->name = "test-tool"; |
| tool1->description = "test description"; |
| tool1->tool_owner_frame_token = rfh->GetFrameToken(); |
| tool1->origin = rfh->GetLastCommittedOrigin(); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| script_tool_host->RegisterScriptTool(std::move(tool1), base::DoNothing()); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| auto tool2 = blink::mojom::ScriptTool::New(); |
| tool2->name = "test-tool"; // Duplicate name |
| tool2->description = "test description"; |
| tool2->tool_owner_frame_token = rfh->GetFrameToken(); |
| tool2->origin = rfh->GetLastCommittedOrigin(); |
| |
| script_tool_host->RegisterScriptTool(std::move(tool2), base::DoNothing()); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_REGISTER_DUPLICATE_TOOL_NAME, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that calling `RegisterScriptTool()` with an invalid tool name |
| // properly terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled, |
| RegisterScriptToolInvalidToolName) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| auto tool = blink::mojom::ScriptTool::New(); |
| tool->name = "invalid!tool"; |
| tool->description = "test description"; |
| tool->tool_owner_frame_token = rfh->GetFrameToken(); |
| tool->origin = rfh->GetLastCommittedOrigin(); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| script_tool_host->RegisterScriptTool(std::move(tool), base::DoNothing()); |
| |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_INVALID_TOOL_NAME, kill_waiter.Wait()); |
| } |
| |
| // Test that calling `ExecuteRemoteScriptTool()` with an opaque target origin |
| // properly terminates the renderer process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled, |
| ExecuteRemoteScriptToolOpaqueTargetOrigin) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| blink::FrameToken tool_owner_frame_token = rfh->GetFrameToken(); |
| |
| script_tool_host->ExecuteRemoteScriptTool( |
| base::UnguessableToken::Create(), tool_owner_frame_token, url::Origin(), |
| "test-tool", "[]", |
| base::BindOnce( |
| [](const std::optional<std::string>& result, bool success) {})); |
| EXPECT_EQ(bad_message::RFHI_WEBMCP_OPAQUE_TARGET_ORIGIN, kill_waiter.Wait()); |
| } |
| |
| // Test that `GetScriptTools()` clears `exposed_origins` so that the full ACL |
| // list is not leaked to caller renderers. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestWebMCPEnabled, |
| GetScriptToolsExposedOriginsRedacted) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| EXPECT_TRUE( |
| rfh->IsFeatureEnabled(network::mojom::PermissionsPolicyFeature::kTools)); |
| |
| auto tool = blink::mojom::ScriptTool::New(); |
| tool->name = "test-tool"; |
| tool->description = "test description"; |
| tool->tool_owner_frame_token = rfh->GetFrameToken(); |
| tool->origin = rfh->GetLastCommittedOrigin(); |
| tool->exposed_origins.push_back( |
| url::Origin::Create(GURL("https://friend.example"))); |
| tool->exposed_origins.push_back( |
| url::Origin::Create(GURL("https://attacker.example"))); |
| |
| mojo::Remote<blink::mojom::ModelContextHost> script_tool_host; |
| rfh->BindModelContextHost(script_tool_host.BindNewPipeAndPassReceiver()); |
| |
| base::RunLoop register_loop; |
| script_tool_host->RegisterScriptTool(std::move(tool), |
| register_loop.QuitClosure()); |
| register_loop.Run(); |
| |
| base::RunLoop get_tools_loop; |
| std::vector<blink::mojom::ScriptToolPtr> result_tools; |
| script_tool_host->GetScriptTools( |
| {}, base::BindLambdaForTesting( |
| [&](std::vector<blink::mojom::ScriptToolPtr> tools) { |
| result_tools = std::move(tools); |
| get_tools_loop.Quit(); |
| })); |
| get_tools_loop.Run(); |
| |
| ASSERT_EQ(1u, result_tools.size()); |
| EXPECT_EQ("test-tool", result_tools[0]->name); |
| EXPECT_TRUE(result_tools[0]->exposed_origins.empty()); |
| } |
| |
| 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 same-document navigations cannot claim to commit a file: origin |
| // when the last committed origin is not a file URL, even if |
| // allow_universal_access_from_file_urls is enabled. The universal access |
| // setting allows file: documents to navigate to other origins, but does not |
| // allow non-file documents to become file: documents. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| CrossOriginSameDocumentCommitUniversalAccessFileOrigin) { |
| 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)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| url::Origin file_origin = url::Origin::Create(GURL("file:///")); |
| EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance() |
| ->HasOriginCheckExemptionForWebView( |
| rfh->GetProcess()->GetDeprecatedID(), file_origin)); |
| |
| // Do a same-document navigation, using an interceptor that replaces the |
| // origin with a file: origin while keeping the original URL. The browser |
| // should reject this because the previously committed origin is not file:. |
| PwnCommitIPC(shell()->web_contents(), start_url, start_url, file_origin); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| // ExecJs will sometimes finish before the renderer gets killed, so we must |
| // ignore the result. |
| std::ignore = ExecJs(rfh, "history.pushState({}, '', location.href);"); |
| EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait()); |
| |
| // The process should not have been granted an origin check exemption based |
| // on the rejected commit's origin. |
| EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance() |
| ->HasOriginCheckExemptionForWebView( |
| rfh->GetProcess()->GetDeprecatedID(), file_origin)); |
| } |
| |
| // Test that a file: URL document with allow_universal_access_from_file_urls |
| // enabled can perform multiple same-document pushState navigations to different |
| // cross-origin URLs without losing its file: committed origin or causing |
| // browser-side validation failures on subsequent pushState calls. |
| // NOTE: Universal access from file scheme behaves differently on macOS, so the |
| // test is disabled on macOS (crbug.com/981018). |
| #if BUILDFLAG(IS_MAC) |
| #define MAYBE_MultipleCrossOriginSameDocumentPushStateFromFileUrl \ |
| DISABLED_MultipleCrossOriginSameDocumentPushStateFromFileUrl |
| #else |
| #define MAYBE_MultipleCrossOriginSameDocumentPushStateFromFileUrl \ |
| MultipleCrossOriginSameDocumentPushStateFromFileUrl |
| #endif |
| IN_PROC_BROWSER_TEST_F( |
| SecurityExploitBrowserTest, |
| MAYBE_MultipleCrossOriginSameDocumentPushStateFromFileUrl) { |
| auto prefs = shell()->web_contents()->GetOrCreateWebPreferences(); |
| prefs.allow_universal_access_from_file_urls = true; |
| shell()->web_contents()->SetWebPreferences(prefs); |
| |
| GURL file_url = GetTestUrl("", "simple_page.html"); |
| ASSERT_TRUE(file_url.SchemeIsFile()); |
| ASSERT_TRUE(NavigateToURL(shell(), file_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| url::Origin initial_origin = rfh->GetLastCommittedOrigin(); |
| EXPECT_EQ(url::kFileScheme, initial_origin.scheme()); |
| |
| // Perform first pushState to a cross-origin HTTPS URL. |
| EXPECT_TRUE( |
| ExecJs(rfh, "history.pushState({}, '', 'https://example.com/1');")); |
| EXPECT_TRUE(rfh->IsRenderFrameLive()); |
| EXPECT_EQ(initial_origin, rfh->GetLastCommittedOrigin()); |
| |
| // Perform second pushState to a different cross-origin HTTPS URL. |
| // Verify that the browser still retains the file: origin on |
| // last_committed_origin_ and allows the second same-document navigation |
| // to succeed. |
| EXPECT_TRUE( |
| ExecJs(rfh, "history.pushState({}, '', 'https://example.org/2');")); |
| EXPECT_TRUE(rfh->IsRenderFrameLive()); |
| EXPECT_EQ(initial_origin, rfh->GetLastCommittedOrigin()); |
| } |
| |
| // 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::ERR_ABORTED, 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, root->current_frame_host()); |
| 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()); |
| } |
| |
| // Tests that a compromised renderer cannot probe parent directories using |
| // Windows path traversal variants like ".. " via FileUtilitiesHost. See |
| // https://crbug.com/498887785. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| PathTraversalWithDotDotSpace) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| |
| // Create a temp directory with a granted dir. |
| base::ScopedTempDir temp_dir; |
| ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); |
| |
| base::FilePath granted_dir = temp_dir.GetPath().AppendASCII("granted"); |
| ASSERT_TRUE(base::CreateDirectory(granted_dir)); |
| |
| // Grant the renderer access to the granted directory. |
| RenderProcessHost* process = |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(); |
| ChildProcessId child_id = process->GetID(); |
| |
| auto* security_policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| security_policy->GrantReadFile(child_id, granted_dir); |
| |
| EXPECT_TRUE(security_policy->CanReadFile(child_id, granted_dir)); |
| |
| // Construct a malicious path that traverses out of the granted dir. |
| base::FilePath malicious_path = granted_dir.Append(FILE_PATH_LITERAL(".. ")); |
| |
| // Bind the FileUtilitiesHost interface. |
| mojo::Remote<blink::mojom::FileUtilitiesHost> file_utilities; |
| FileUtilitiesHostImpl::Create(child_id, |
| file_utilities.BindNewPipeAndPassReceiver()); |
| |
| // Ask for file info on the malicious path. |
| std::optional<base::File::Info> file_info; |
| base::RunLoop run_loop; |
| file_utilities->GetFileInfo( |
| malicious_path, base::BindLambdaForTesting( |
| [&](const std::optional<base::File::Info>& info) { |
| file_info = info; |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| |
| // The FileInfo request should fail. Prior to the fix in crbug.com/498887785, |
| // this worked on Windows where ".. " at the end of the path was not |
| // recognized as a parent directory traversal in ChildProcessSecurityPolicy |
| // security checks. |
| EXPECT_FALSE(file_info.has_value()); |
| } |
| |
| // A simple Mojo client that reads a Blob's contents and captures the |
| // final net::Error status code upon completion. This is used to verify |
| // whether the browser process blocked a file read (e.g., ERR_FAILED) |
| // or if the read proceeded normally. |
| class TestBlobReaderClient : public blink::mojom::BlobReaderClient { |
| public: |
| explicit TestBlobReaderClient( |
| mojo::PendingReceiver<blink::mojom::BlobReaderClient> receiver) |
| : receiver_(this, std::move(receiver)) {} |
| |
| void OnCalculatedSize(uint64_t total_size, |
| uint64_t expected_content_size) override {} |
| |
| void OnComplete(int32_t status, uint64_t data_length) override { |
| status_ = status; |
| run_loop_.Quit(); |
| } |
| |
| void Wait() { run_loop_.Run(); } |
| |
| int32_t status_ = net::OK; |
| |
| private: |
| mojo::Receiver<blink::mojom::BlobReaderClient> receiver_; |
| base::RunLoop run_loop_; |
| }; |
| |
| // Tests that a compromised renderer cannot access actual files in a non-granted |
| // parent directory using corner case parent traversal variants like ".. ". See |
| // https://crbug.com/498887785. Note that this test passes both before and after |
| // the fix for that bug, because it turned out that Windows's corner case |
| // behavior for ".. " only occurs at the end of a path and not in the middle, |
| // but the test is still useful to catch regressions. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| FileReadBlockedForInvalidPathTraversalWithDotDotSpace) { |
| base::ScopedAllowBlockingForTesting allow_blocking; |
| |
| // Create a temp directory with a granted dir and a secret file. |
| base::ScopedTempDir temp_dir; |
| ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); |
| |
| base::FilePath granted_dir = temp_dir.GetPath().AppendASCII("granted"); |
| base::FilePath secret_file = temp_dir.GetPath().AppendASCII("secret.txt"); |
| |
| ASSERT_TRUE(base::CreateDirectory(granted_dir)); |
| ASSERT_TRUE(base::WriteFile(secret_file, "secret")); |
| |
| // Grant the renderer access to the granted directory. |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| ChildProcessId child_id = rfh->GetProcess()->GetID(); |
| |
| auto* security_policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| security_policy->GrantReadFile(child_id, granted_dir); |
| |
| // Verify the setup assumptions. |
| ASSERT_TRUE(security_policy->CanReadFile(child_id, granted_dir)); |
| ASSERT_FALSE(security_policy->CanReadFile(child_id, secret_file)); |
| |
| // Construct a malicious path that traverses out of the granted dir |
| // using ".. " (dot-dot-space). |
| base::FilePath malicious_path = |
| granted_dir.AppendASCII(".. ").AppendASCII("secret.txt"); |
| |
| // Try to read the secret file via Blob interfaces in the rest of this test |
| // and ensure that this fails (i.e., that the dot-dot-space pattern doesn't |
| // accidentally grant a compromised renderer access to a file outside |
| // `granted_dir`). First, register a file-backed blob with the malicious path |
| // via Mojo. |
| mojo::AssociatedRemote<blink::mojom::FileBackedBlobFactory> factory; |
| rfh->BindFileBackedBlobFactory( |
| factory.BindNewEndpointAndPassDedicatedReceiver()); |
| |
| auto element = blink::mojom::DataElementFile::New( |
| malicious_path, /*offset=*/0, /*length=*/6, |
| /*expected_modification_time=*/std::nullopt); |
| |
| mojo::Remote<blink::mojom::Blob> blob; |
| factory->RegisterBlob(blob.BindNewPipeAndPassReceiver(), "uuid-1234", |
| "text/plain", std::move(element)); |
| |
| // Attempt to read the Blob's contents. |
| mojo::ScopedDataPipeProducerHandle producer; |
| mojo::ScopedDataPipeConsumerHandle consumer; |
| ASSERT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(nullptr, producer, consumer)); |
| |
| mojo::PendingRemote<blink::mojom::BlobReaderClient> client_remote; |
| TestBlobReaderClient client(client_remote.InitWithNewPipeAndPassReceiver()); |
| |
| blob->ReadAll(std::move(producer), std::move(client_remote)); |
| client.Wait(); |
| |
| std::string blob_contents; |
| EXPECT_TRUE(mojo::BlockingCopyToString(std::move(consumer), &blob_contents)); |
| |
| // Verify the read results and make sure this fails. |
| EXPECT_NE(net::OK, client.status_); |
| EXPECT_EQ("", blob_contents); |
| } |
| |
| // 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(); |
| params->document_sequence_number = 1; |
| 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()); |
| } |
| |
| namespace { |
| |
| // Interceptor that replaces the embedding token in the |
| // DidCommitProvisionalLoadParams. |
| class DidCommitEmbeddingTokenReplacer : public DidCommitNavigationInterceptor { |
| public: |
| DidCommitEmbeddingTokenReplacer(WebContents* web_contents, |
| base::UnguessableToken embedding_token) |
| : DidCommitNavigationInterceptor(web_contents), |
| embedding_token_(std::move(embedding_token)) {} |
| |
| DidCommitEmbeddingTokenReplacer(const DidCommitEmbeddingTokenReplacer&) = |
| delete; |
| DidCommitEmbeddingTokenReplacer& operator=( |
| const DidCommitEmbeddingTokenReplacer&) = delete; |
| |
| ~DidCommitEmbeddingTokenReplacer() override = default; |
| |
| private: |
| // DidCommitNavigationInterceptor: |
| bool WillProcessDidCommitNavigation( |
| RenderFrameHost* render_frame_host, |
| NavigationRequest* navigation_request, |
| mojom::DidCommitProvisionalLoadParamsPtr* params, |
| mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) |
| override { |
| (*params)->embedding_token = embedding_token_; |
| return true; |
| } |
| |
| base::UnguessableToken embedding_token_; |
| }; |
| |
| } // namespace |
| |
| // TODO(crbug.com/516558664): Test is failing on Linux. |
| #if BUILDFLAG(IS_LINUX) |
| #define MAYBE_AttemptUseStolenEmbedderToken \ |
| DISABLED_AttemptUseStolenEmbedderToken |
| #else |
| #define MAYBE_AttemptUseStolenEmbedderToken AttemptUseStolenEmbedderToken |
| #endif |
| // Verify that the renderer is terminated if a compromised renderer passes an |
| // embedding token used by another frame to |
| // RenderFrameHostImpl::DidCommitProvisionalLoad(). |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| MAYBE_AttemptUseStolenEmbedderToken) { |
| // Explicitly isolating a.com helps ensure that this test is applicable on |
| // platforms without site-per-process. |
| IsolateOrigin("a.com"); |
| |
| const GURL kUrl(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), kUrl)); |
| WebContents* web_contents = shell()->web_contents(); |
| |
| // Create a separate window and navigate it to b.com to get a valid, |
| // registered embedding token from another frame. |
| GURL kPopupUrl(embedded_test_server()->GetURL("b.com", "/title.html")); |
| Shell* new_window = OpenPopup(shell(), kPopupUrl, "popup"); |
| std::optional<base::UnguessableToken> stolen_token = |
| new_window->web_contents()->GetPrimaryMainFrame()->GetEmbeddingToken(); |
| ASSERT_TRUE(stolen_token.has_value()); |
| |
| // Check that replacing the embedding token of the next navigation |
| // with a fresh, unused embedding token doesn't terminate the renderer. |
| { |
| DidCommitEmbeddingTokenReplacer interceptor( |
| web_contents, base::UnguessableToken::Create()); |
| const GURL kUrl2(embedded_test_server()->GetURL("a.com", "/title2.html")); |
| EXPECT_TRUE(NavigateToURL(web_contents, kUrl2)); |
| } |
| |
| // Test that overwriting the embedding token of the next navigation |
| // with a stolen token from another frame causes the navigation to fail and |
| // terminates the compromised renderer process. |
| { |
| RenderProcessHostBadIpcMessageWaiter kill_waiter( |
| web_contents->GetPrimaryMainFrame()->GetProcess()); |
| DidCommitEmbeddingTokenReplacer interceptor(web_contents, |
| stolen_token.value()); |
| const GURL kUrl3(embedded_test_server()->GetURL("a.com", "/title3.html")); |
| EXPECT_FALSE(NavigateToURL(shell(), kUrl3)); |
| EXPECT_EQ(bad_message::RFH_UNEXPECTED_EMBEDDING_TOKEN, kill_waiter.Wait()); |
| } |
| } |
| |
| // Make sure that a renderer is terminated if it sends an invalid net error code |
| // in a DidFailLoadWithError() IPC. See https://crbug.com/407069514. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| DidFailLoadWithInvalidErrorCode) { |
| GURL url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Navigate normally and wait for commit (but not full load). |
| TestFrameNavigationObserver commit_observer(shell()); |
| GURL failed_url(embedded_test_server()->GetURL("/title2.html")); |
| ASSERT_TRUE(ExecJs(shell(), JsReplace("location.href = $1;", failed_url))); |
| commit_observer.WaitForCommit(); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| // Simulate a DidFailLoadWithError message with an invalid error code. |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| int32_t invalid_error_code = INT_MAX; |
| ASSERT_FALSE(net::IsOkOrDefinedError(invalid_error_code)); |
| static_cast<blink::mojom::LocalFrameHost*>(rfh)->DidFailLoadWithError( |
| failed_url, invalid_error_code); |
| |
| // Verify that the malicious renderer got killed. |
| EXPECT_EQ(bad_message::RFHI_INVALID_NET_ERROR_CODE, 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:///"), proxy->frame_tree_node()->current_frame_host())); |
| 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, proxy->frame_tree_node()->current_frame_host())); |
| observer_2.Wait(); |
| EXPECT_EQ(GURL(kBlockedURL), |
| child->current_frame_host()->GetLastCommittedURL()); |
| } |
| |
| // Helper class to replace a particular URL as the client_side_redirect_url |
| // in BeginNavigationParams. |
| class ClientSideRedirectUrlReplacer : public FrameHostInterceptor { |
| public: |
| ClientSideRedirectUrlReplacer(WebContents* web_contents, |
| const GURL& url_to_inject) |
| : FrameHostInterceptor(web_contents), url_to_inject_(url_to_inject) {} |
| |
| ClientSideRedirectUrlReplacer(const ClientSideRedirectUrlReplacer&) = delete; |
| ClientSideRedirectUrlReplacer& operator=( |
| const ClientSideRedirectUrlReplacer&) = 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)->client_side_redirect_url = url_to_inject_; |
| is_activated_ = false; |
| } |
| return true; |
| } |
| |
| void Activate() { is_activated_ = true; } |
| |
| private: |
| GURL url_to_inject_; |
| bool is_activated_ = false; |
| }; |
| |
| // Verify that a compromised renderer can't poison client_side_redirect_url |
| // with a privileged URL (e.g. to navigate to it on reload / |
| // LoadOriginalRequestURL). |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| BlockIllegalClientSideRedirectUrl) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL webui_url(GetWebUIURL(kChromeUIGpuHost)); |
| ClientSideRedirectUrlReplacer injector(web_contents, webui_url); |
| |
| // Setup the interceptor to inject the WebUI URL into the next |
| // BeginNavigation's client_side_redirect_url. |
| injector.Activate(); |
| |
| // Trigger a normal renderer-initiated navigation to a benign URL. The |
| // injector will poison the client_side_redirect_url in that IPC's |
| // BeginNavigationParams. |
| GURL next_url(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| TestNavigationManager nav_manager(web_contents, next_url); |
| EXPECT_TRUE(ExecJs(web_contents, JsReplace("location.href = $1;", next_url))); |
| |
| // Wait for the navigation to finish. |
| ASSERT_TRUE(nav_manager.WaitForNavigationFinished()); |
| EXPECT_TRUE(nav_manager.was_successful()); |
| EXPECT_EQ(next_url, web_contents->GetLastCommittedURL()); |
| |
| // At this point, the WebUI URL should've been filtered out and replaced |
| // with about:blank#blocked. |
| EXPECT_EQ(GURL(kBlockedURL), web_contents->GetController() |
| .GetLastCommittedEntry() |
| ->GetOriginalRequestURL()); |
| |
| // Simulate the user clicking "Request Desktop Site" or similar, |
| // triggering a reload of the original request URL. |
| TestNavigationObserver reload_observer(web_contents); |
| web_contents->GetController().LoadOriginalRequestURL(); |
| reload_observer.Wait(); |
| |
| // Ensure that the browser doesn't navigate to the WebUI URL, which should've |
| // been filtered out when processing the corresponding BeginNavigation IPC. |
| EXPECT_NE(webui_url, web_contents->GetLastCommittedURL()); |
| EXPECT_EQ(GURL(kBlockedURL), web_contents->GetLastCommittedURL()); |
| } |
| |
| // Verify that a compromised renderer can't set client_side_redirect_url to a |
| // cross-origin web URL that is not hosted by the process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| BlockCrossOriginClientSideRedirectUrl) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| |
| GURL cross_origin_url( |
| embedded_test_server()->GetURL("b.com", "/title2.html")); |
| ClientSideRedirectUrlReplacer injector(web_contents, cross_origin_url); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter( |
| web_contents->GetPrimaryMainFrame()->GetProcess()); |
| |
| injector.Activate(); |
| |
| GURL next_url(embedded_test_server()->GetURL("a.com", "/title3.html")); |
| ExecuteScriptAsync(web_contents, JsReplace("location.href = $1;", next_url)); |
| |
| EXPECT_EQ(bad_message::RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL, |
| kill_waiter.Wait()); |
| } |
| |
| // Verify that a sandboxed frame can legitimately perform client-side redirects |
| // without being terminated, but is terminated if it attempts to spoof a |
| // client_side_redirect_url that it does not host. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| SandboxedFrameClientSideRedirectUrl) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root(); |
| |
| // Create a sandboxed child frame at a.com. |
| GURL child_url(embedded_test_server()->GetURL("a.com", "/title2.html")); |
| std::string js_str = base::StringPrintf( |
| "var frame = document.createElement('iframe'); " |
| "frame.id = 'sandboxed_child'; " |
| "frame.sandbox = 'allow-scripts'; " |
| "frame.src = '%s'; " |
| "document.body.appendChild(frame);", |
| child_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(root->current_frame_host(), js_str)); |
| ASSERT_TRUE(WaitForLoadStop(web_contents)); |
| |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| ASSERT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kOrigin)); |
| |
| // Legitimate client-side redirect in sandboxed frame: the sandboxed frame |
| // navigates itself, and Blink populates client_side_redirect_url with |
| // child_url. Verify that the navigation succeeds and the renderer is not |
| // terminated. |
| GURL next_url(embedded_test_server()->GetURL("a.com", "/title3.html")); |
| TestNavigationObserver nav_observer(web_contents); |
| EXPECT_TRUE(ExecJs(subframe, JsReplace("location.href = $1;", next_url))); |
| nav_observer.Wait(); |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| |
| // Malicious attempt: a compromised sandboxed renderer attempts to set |
| // client_side_redirect_url to a cross-origin URL. |
| subframe = root->child_at(0)->current_frame_host(); |
| GURL cross_origin_url( |
| embedded_test_server()->GetURL("b.com", "/title2.html")); |
| ClientSideRedirectUrlReplacer injector(web_contents, cross_origin_url); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| injector.Activate(); |
| |
| GURL final_url(embedded_test_server()->GetURL("a.com", "/empty.html")); |
| ExecuteScriptAsync(subframe, JsReplace("location.href = $1;", final_url)); |
| |
| EXPECT_EQ(bad_message::RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL, |
| kill_waiter.Wait()); |
| } |
| |
| // Regression test for crbug.com/495529018. |
| // Verify that a speculative RenderFrameHost cannot update FrameReplicationState |
| // in the BrowsingContextState that it shares with the FrameTreeNode's current |
| // cross-origin document. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| SpeculativeFrameCannotUpdateFrameReplicationState) { |
| 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)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* child = |
| web_contents->GetPrimaryFrameTree().root()->child_at(0); |
| RenderFrameHostImpl* current_rfh = child->current_frame_host(); |
| |
| // Start a cross-process navigation in the subframe so that a speculative |
| // RenderFrameHost is created. The speculative RenderFrameHost shares the |
| // FrameTreeNode's BrowsingContextState with the current document. |
| GURL cross_site_url(embedded_test_server()->GetURL("c.com", "/title1.html")); |
| TestNavigationManager nav_manager(web_contents, cross_site_url); |
| EXPECT_TRUE(BeginNavigateToURLFromRenderer(child, cross_site_url)); |
| nav_manager.WaitForSpeculativeRenderFrameHostCreation(); |
| |
| RenderFrameHostImpl* speculative_rfh = |
| child->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_rfh); |
| ASSERT_EQ(RenderFrameHostLifecycleStateImpl::kSpeculative, |
| speculative_rfh->lifecycle_state()); |
| EXPECT_NE(speculative_rfh->GetProcess(), current_rfh->GetProcess()); |
| ASSERT_EQ(speculative_rfh->browsing_context_state().get(), |
| current_rfh->browsing_context_state().get()); |
| |
| const blink::mojom::FrameReplicationState& replication_state = |
| child->current_replication_state(); |
| ASSERT_EQ(blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone, |
| replication_state.insecure_request_policy); |
| ASSERT_TRUE(replication_state.insecure_navigations_set.empty()); |
| ASSERT_FALSE(replication_state.has_received_user_gesture_before_nav); |
| blink::mojom::FrameAdStatus initial_ad_frame_status = |
| replication_state.ad_frame_status; |
| |
| // Simulate a compromised renderer sending state updates from the speculative |
| // RenderFrameHost before the navigation has committed. These should be |
| // ignored and must not modify the replication state of the still-current |
| // cross-origin document. |
| static_cast<blink::mojom::LocalFrameHost*>(speculative_rfh) |
| ->EnforceInsecureRequestPolicy( |
| blink::mojom::InsecureRequestPolicy::kBlockAllMixedContent); |
| static_cast<blink::mojom::LocalFrameHost*>(speculative_rfh) |
| ->EnforceInsecureNavigationsSet({1u}); |
| static_cast<blink::mojom::LocalFrameHost*>(speculative_rfh) |
| ->HadStickyUserActivationBeforeNavigationChanged(true); |
| speculative_rfh->UpdateToAdFrame(); |
| |
| EXPECT_EQ(blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone, |
| replication_state.insecure_request_policy); |
| EXPECT_TRUE(replication_state.insecure_navigations_set.empty()); |
| EXPECT_FALSE(replication_state.has_received_user_gesture_before_nav); |
| EXPECT_EQ(initial_ad_frame_status, replication_state.ad_frame_status); |
| } |
| |
| // Verify that a speculative RenderFrameHost cannot update the frame name or |
| // opener in the BrowsingContextState/FrameTreeNode that it shares with the |
| // current cross-origin document. Regression test for crbug.com/554348119. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| SpeculativeFrameCannotUpdateNameOrOpener) { |
| 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)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| FrameTreeNode* root = web_contents->GetPrimaryFrameTree().root(); |
| FrameTreeNode* child = root->child_at(0); |
| RenderFrameHostImpl* current_rfh = child->current_frame_host(); |
| |
| const std::string initial_name = child->current_replication_state().name; |
| const std::string initial_unique_name = |
| child->current_replication_state().unique_name; |
| |
| // Set an opener on the child frame so we can verify a speculative RFH |
| // cannot sever it. |
| child->SetOpener(root); |
| ASSERT_EQ(root, child->opener()); |
| |
| // Start a cross-process navigation in the subframe so that a speculative |
| // RenderFrameHost is created. The speculative RenderFrameHost shares the |
| // FrameTreeNode's BrowsingContextState with the current document. |
| GURL cross_site_url(embedded_test_server()->GetURL("c.com", "/title1.html")); |
| TestNavigationManager nav_manager(web_contents, cross_site_url); |
| EXPECT_TRUE(BeginNavigateToURLFromRenderer(child, cross_site_url)); |
| nav_manager.WaitForSpeculativeRenderFrameHostCreation(); |
| |
| RenderFrameHostImpl* speculative_rfh = |
| child->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_rfh); |
| ASSERT_EQ(RenderFrameHostLifecycleStateImpl::kSpeculative, |
| speculative_rfh->lifecycle_state()); |
| EXPECT_NE(speculative_rfh->GetProcess(), current_rfh->GetProcess()); |
| ASSERT_EQ(speculative_rfh->browsing_context_state().get(), |
| current_rfh->browsing_context_state().get()); |
| |
| // Simulate a compromised renderer sending DidChangeName and DidChangeOpener |
| // from the speculative RenderFrameHost before the navigation has committed. |
| // These should be ignored and must not modify the name or opener of the |
| // still-current cross-origin document. |
| static_cast<mojom::FrameHost*>(speculative_rfh) |
| ->DidChangeName("evil_name", "evil_unique_name"); |
| static_cast<blink::mojom::LocalFrameHost*>(speculative_rfh) |
| ->DidChangeOpener(std::nullopt); |
| |
| EXPECT_EQ(initial_name, child->current_replication_state().name); |
| EXPECT_EQ(initial_unique_name, |
| child->current_replication_state().unique_name); |
| EXPECT_EQ(root, child->opener()); |
| } |
| |
| 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::optional<url::Origin>& 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_; |
| }; |
| |
| class CapabilityDelegationRemoteFrameHostInterceptor |
| : public blink::mojom::RemoteFrameHostInterceptorForTesting { |
| public: |
| explicit CapabilityDelegationRemoteFrameHostInterceptor( |
| RenderFrameProxyHost* render_frame_proxy_host) |
| : swapped_impl_( |
| render_frame_proxy_host->frame_host_receiver_for_testing(), |
| this) {} |
| |
| ~CapabilityDelegationRemoteFrameHostInterceptor() 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::optional<url::Origin>& target_origin, |
| blink::TransferableMessage message) override { |
| // Inject the delegated capability into the message before forwarding. |
| message.delegated_capability = |
| blink::mojom::DelegatedCapability::kFullscreenRequest; |
| GetForwardingInterface()->RouteMessageEvent( |
| std::move(source_frame_token), source_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: |
| 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 cannot send IPCs that require a |
| // CrossProcessFrameConnector to a proxy that doesn't have one (e.g. a proxy |
| // for a cross-site popup). |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| IPCOnProxyWithoutCrossProcessFrameConnector) { |
| // 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()); |
| |
| // Open a cross-site popup. |
| GURL popup_url(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| Shell* popup = OpenPopup(web_contents, popup_url, ""); |
| WebContentsImpl* popup_contents = |
| static_cast<WebContentsImpl*>(popup->web_contents()); |
| |
| // Wait for the popup to finish loading. |
| EXPECT_TRUE(WaitForLoadStop(popup_contents)); |
| |
| // The popup process should have a main frame proxy for the original window. |
| SiteInstanceGroup* popup_group = |
| popup_contents->GetPrimaryMainFrame()->GetSiteInstance()->group(); |
| RenderFrameProxyHost* main_frame_proxy_host = |
| web_contents->GetPrimaryMainFrame() |
| ->browsing_context_state() |
| ->GetRenderFrameProxyHost(popup_group); |
| ASSERT_TRUE(main_frame_proxy_host); |
| |
| // The proxy is a main frame proxy, so it shouldn't have a |
| // CrossProcessFrameConnector. |
| EXPECT_FALSE(main_frame_proxy_host->cross_process_frame_connector()); |
| |
| // Simulate a compromised popup renderer sending an IPC that requires a CPFC |
| // (SetIsInert) to the main frame proxy. |
| RenderProcessHostBadIpcMessageWaiter kill_waiter( |
| main_frame_proxy_host->GetProcess()); |
| main_frame_proxy_host->SetIsInert(true); |
| |
| EXPECT_EQ(bad_message::RFPH_SET_IS_INERT_WITHOUT_CPFC, kill_waiter.Wait()); |
| } |
| |
| // Regression test for browser-side validation of the allow-popups sandbox |
| // attribute. See https://crbug.com/487471101. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| WindowOpenDisallowedFromSandboxedFrame) { |
| // Explicitly isolating b.com helps establish consistent expectations for the |
| // sandboxed frame across platforms. |
| 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. The frame lacks the allow-popups |
| // attribute, so it should not be allowed to create popups. |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.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)); |
| } |
| |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| EXPECT_TRUE( |
| subframe->GetSiteInstance()->GetSecurityPrincipal().IsSandboxed()); |
| EXPECT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kPopups)); |
| |
| // Simulate that the b.com renderer is compromised and sends an IPC to open a |
| // popup, bypassing renderer-side checks in blink::CreateNewWindow(). The |
| // browser process should detect this and terminate the renderer. |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New(); |
| params->target_url = GURL("about:blank"); |
| params->disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| subframe->CreateNewWindow(std::move(params), base::DoNothing()); |
| EXPECT_EQ(bad_message::RFH_CREATE_NEW_WINDOW_FROM_SANDBOXED_FRAME, |
| kill_waiter.Wait()); |
| EXPECT_FALSE(subframe->IsRenderFrameLive()); |
| EXPECT_EQ(1u, Shell::windows().size()); |
| } |
| |
| // Regression test for browser-side validation of the allow-modals sandbox |
| // attribute. A sandboxed frame without allow-modals should not be able to show |
| // modal dialogs (alert, confirm, prompt). This is a variant of |
| // WindowOpenDisallowedFromSandboxedFrame for the kModals sandbox flag. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| ModalDialogDisallowedFromSandboxedFrame) { |
| 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. The frame lacks the allow-modals |
| // attribute, so it should not be allowed to show modal dialogs. |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.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)); |
| } |
| |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| EXPECT_TRUE( |
| subframe->GetSiteInstance()->GetSecurityPrincipal().IsSandboxed()); |
| EXPECT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kModals)); |
| |
| // Simulate that the b.com renderer is compromised and sends an IPC to show |
| // a modal dialog, bypassing renderer-side checks in LocalDOMWindow::alert(). |
| // The browser process should detect this and terminate the renderer. |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| subframe->RunModalAlertDialog(u"test", false, base::DoNothing()); |
| EXPECT_EQ(bad_message::RFH_MODAL_DIALOG_FROM_SANDBOXED_FRAME, |
| kill_waiter.Wait()); |
| EXPECT_FALSE(subframe->IsRenderFrameLive()); |
| } |
| |
| // Regression test for browser-side validation of POST submissions in |
| // CreateNewWindow. See https://crbug.com/487768779. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| CreateNewWindowWithInaccessibleFile) { |
| 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(); |
| |
| // Simulate that the renderer is compromised and sends an IPC to open a popup, |
| // using a POST submission that includes a file the renderer does not have |
| // access to. The browser process should detect this and kill the renderer. |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess()); |
| mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New(); |
| params->target_url = main_url; |
| params->disposition = WindowOpenDisposition::NEW_FOREGROUND_TAB; |
| scoped_refptr<network::ResourceRequestBody> request_body = |
| new network::ResourceRequestBody(); |
| base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits"); |
| request_body->AppendFileRange( |
| bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| params->form_submission_post_data = std::move(request_body); |
| main_frame->CreateNewWindow(std::move(params), base::DoNothing()); |
| EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait()); |
| EXPECT_FALSE(main_frame->IsRenderFrameLive()); |
| EXPECT_EQ(1u, Shell::windows().size()); |
| } |
| |
| // 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) { |
| // 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()->GetSecurityPrincipal().IsSandboxed()); |
| |
| // 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()); |
| } |
| |
| // Test verifying that a compromised renderer cannot delegate a capability |
| // (e.g. fullscreen) without transient user activation. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| PostMessageDelegatedCapabilityWithoutUserActivation) { |
| // 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 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.src = '%s'; " |
| "document.body.appendChild(frame);", |
| child_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(main_frame, js_str)); |
| ASSERT_TRUE(WaitForLoadStop(web_contents)); |
| } |
| |
| // main frame and subframe should be in separate processes. |
| FrameTreeNode* subframe_node = root->child_at(0); |
| RenderFrameHostImpl* subframe = subframe_node->current_frame_host(); |
| EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess()); |
| |
| // 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()); |
| |
| // Intercept the RouteMessageEvent IPC message that will come from |
| // the subframe process. |
| CapabilityDelegationRemoteFrameHostInterceptor mojo_interceptor( |
| main_frame_proxy_host); |
| |
| // Post a message from the subframe to the cross-site parent and intercept the |
| // associated IPC message, changing it to include a delegated capability. |
| // Since there is no active or recently consumed user activation on the |
| // subframe, this should result in a renderer kill. |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')", |
| EXECUTE_SCRIPT_NO_USER_GESTURE)); |
| EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_DELEGATED_CAPABILITY, |
| kill_waiter.Wait()); |
| } |
| |
| // Test verifying that a legitimate renderer with user activation CAN delegate a |
| // capability (e.g. fullscreen) to another frame. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| PostMessageDelegatedCapabilityWithUserActivation) { |
| // 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 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.src = '%s'; " |
| "document.body.appendChild(frame);", |
| child_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(main_frame, js_str)); |
| ASSERT_TRUE(WaitForLoadStop(web_contents)); |
| } |
| |
| // main frame and subframe should be in separate processes. |
| FrameTreeNode* subframe_node = root->child_at(0); |
| RenderFrameHostImpl* subframe = subframe_node->current_frame_host(); |
| EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess()); |
| |
| // 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()); |
| |
| // Intercept the RouteMessageEvent IPC message that will come from |
| // the subframe process. |
| CapabilityDelegationRemoteFrameHostInterceptor mojo_interceptor( |
| main_frame_proxy_host); |
| |
| // Post a message from the subframe to the cross-site parent and intercept the |
| // associated IPC message, changing it to include a delegated capability. |
| // This time, we execute the script WITH user gesture (default ExecJs option). |
| // The browser should allow this and NOT terminate the subframe process. |
| EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')")); |
| |
| // Verify that the subframe process is still alive. |
| EXPECT_TRUE(subframe->IsRenderFrameLive()); |
| } |
| |
| 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) { |
| 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()->GetSecurityPrincipal().IsSandboxed()); |
| |
| // 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()); |
| } |
| |
| // Similar to the test above, but ensure that opaque initiator origins are |
| // validated even when the frame sending BeginNavigation is currently showing an |
| // error page. Some subframe error pages are not currently process-isolated, so |
| // a renderer for the embedding site can send IPCs on their behalf and must not |
| // be allowed to claim an arbitrary precursor. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| InvalidBeginNavigationOpaqueInitiatorFromErrorPage) { |
| 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")).DeriveNewOpaqueOrigin(); |
| BeginNavigationInitiatorReplacer injector(web_contents, injected_origin); |
| |
| // 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)); |
| RenderFrameHostImpl* main_frame = |
| web_contents->GetPrimaryFrameTree().root()->current_frame_host(); |
| |
| // Set CSP to block iframes, so the subframe navigation below results in an |
| // error page that stays in the current (a.com) process. |
| EXPECT_TRUE(ExecJs(main_frame, |
| "var meta = document.createElement('meta');" |
| "meta.httpEquiv = 'Content-Security-Policy';" |
| "meta.content = \"frame-src 'none'\";" |
| "document.head.appendChild(meta);")); |
| |
| // Create a subframe that fails to load and commits an error page. |
| GURL blocked_url(embedded_test_server()->GetURL("a.com", "/title2.html")); |
| TestNavigationObserver error_observer(web_contents); |
| EXPECT_TRUE( |
| ExecJs(main_frame, JsReplace("var f = document.createElement('iframe');" |
| "f.src = $1;" |
| "document.body.appendChild(f);", |
| blocked_url))); |
| error_observer.Wait(); |
| EXPECT_FALSE(error_observer.last_navigation_succeeded()); |
| |
| RenderFrameHostImpl* error_frame = |
| main_frame->child_at(0)->current_frame_host(); |
| ASSERT_TRUE(error_frame->IsErrorDocument()); |
| |
| // Start monitoring for renderer kills. |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(error_frame->GetProcess()); |
| |
| // Have the error page subframe navigate and lie that the initiator origin is |
| // an opaque origin with the precursor of evil.com. Note that an absolute URL |
| // is required here, since relative URLs in error documents resolve against |
| // chrome-error://chromewebdata/ and would be blocked in the renderer. |
| injector.Activate(); |
| // Don't expect a response for the script, as the process may be killed |
| // before the script sends its completion message. |
| ExecuteScriptAsync(error_frame, |
| JsReplace("window.location = $1;", blocked_url)); |
| |
| // 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()); |
| } |
| |
| // Ensure that DidChangeOpener IPC messages do not succeed if they attempt to |
| // assign an opener from a different Browsing Context Group (BCG). |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OpenerChangeDisallowedAcrossBrowsingInstances) { |
| // Explicitly isolate a.test to ensure that this test is applicable on |
| // platforms without site-per-process. |
| IsolateOrigin("a.test"); |
| |
| // Force process reuse so tab 1 and tab 2 are in the same renderer process. |
| // This guarantees that the opener token (A1) is valid in the same process as |
| // the proxy in A2's SiteInstance that will be used to send the spoofed |
| // DidChangeOpener IPC later. |
| RenderProcessHost::SetMaxRendererProcessCount(1); |
| |
| // Navigate to A1. |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.test", "/title1.html"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* tab1_rfh = web_contents->GetPrimaryMainFrame(); |
| FrameTreeNode* tab1_root = tab1_rfh->frame_tree_node(); |
| |
| Shell* tab2 = CreateBrowser(); |
| // Create an unrelated window and navigate it to A2. Workaround: navigate |
| // `tab2` to a random site before navigating to attacker's site to ensure that |
| // `tab2` shares the process with tab1, rather than staying in its initial |
| // process. |
| ASSERT_TRUE(NavigateToURL( |
| tab2, embedded_test_server()->GetURL("d.test", "/title3.html"))); |
| ASSERT_TRUE(NavigateToURL( |
| tab2, embedded_test_server()->GetURL("a.test", "/title2.html"))); |
| RenderFrameHostImpl* tab2_rfh = static_cast<RenderFrameHostImpl*>( |
| tab2->web_contents()->GetPrimaryMainFrame()); |
| FrameTreeNode* tab2_root = tab2_rfh->frame_tree_node(); |
| EXPECT_FALSE(tab1_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| tab2_rfh->GetSiteInstance())); |
| EXPECT_EQ(tab1_rfh->GetProcess(), tab2_rfh->GetProcess()); |
| |
| // Open a cross-site page B in the same BCG as A2. |
| GURL cross_site_url(embedded_test_server()->GetURL("b.test", "/title3.html")); |
| Shell* tab3 = OpenPopup(tab2, cross_site_url, "foo"); |
| RenderFrameHostImpl* tab3_rfh = static_cast<RenderFrameHostImpl*>( |
| tab3->web_contents()->GetPrimaryMainFrame()); |
| FrameTreeNode* tab3_root = tab3_rfh->frame_tree_node(); |
| EXPECT_EQ(tab3_root->opener(), tab2_root); |
| EXPECT_TRUE(tab2_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| tab3_rfh->GetSiteInstance())); |
| |
| // Grab the token of the A1 document that will be set as the new opener. |
| blink::LocalFrameToken opener_token = tab1_rfh->GetFrameToken(); |
| |
| // Look up the proxy that represents B in A2's SiteInstance. This is the |
| // target of the malicious DidChangeOpener IPC. This should exist as long as |
| // a.test and b.test are process-isolated from each other. |
| RenderFrameProxyHost* proxy = |
| tab3_rfh->browsing_context_state()->GetRenderFrameProxyHost( |
| tab2_rfh->GetSiteInstance()->group()); |
| ASSERT_TRUE(proxy); |
| |
| // Simulate the vulnerability by sending the DidChangeOpener IPC using the |
| // token of the frame in the wrong BCG. This goes to the |
| // RenderFrameHostManager of tab3, trying to set its opener to the FTN that's |
| // in the other BCG. |
| proxy->DidChangeOpener(opener_token); |
| |
| // The opener should *not* be updated to tab1_root. It should stay the same, |
| // still pointing to tab2. |
| EXPECT_NE(tab3_root->opener(), tab1_root); |
| EXPECT_EQ(tab3_root->opener(), tab2_root); |
| } |
| |
| // Verify that a compromised renderer can't use a BFCached page and a rogue |
| // DidChangeOpener IPC to establish an opener relationship with a |
| // cross-BrowsingInstance page. |
| // |
| // Setup: |
| // Tab 1: A1 in BCG1 -> navigates to WebUI in BCG3, A1 gets bfcached |
| // Tab 2: A2 in BCG2 |
| // Tab 3: B in BCG2 |
| // Attacker controls both A and B, and executes DidChangeOpener on a proxy for |
| // tab 3 in A2's SiteInstance, asking the browser process to update tab3's |
| // opener to A1. A1 is bfcached, and the test ensures that the opener isn't set |
| // to the WebUI page which is the current RFH in A1's FTN. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OpenerChangeDisallowedAcrossBrowsingInstances_Bfcache) { |
| // This test assumes that back-forward cache is enabled. |
| if (!IsBackForwardCacheEnabled()) { |
| GTEST_SKIP(); |
| } |
| |
| // Explicitly isolate a.test to ensure that this test is applicable on |
| // platforms without site-per-process. |
| IsolateOrigin("a.test"); |
| |
| // Force process reuse so tab 1 and tab 2 are in the same renderer process. |
| // This guarantees that the opener token (A1) is valid in the same process as |
| // the proxy in A2's SiteInstance that will be used to send the spoofed |
| // DidChangeOpener IPC later. |
| RenderProcessHost::SetMaxRendererProcessCount(1); |
| |
| // Navigate to A1. |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.test", "/title1.html"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* tab1_rfh = web_contents->GetPrimaryMainFrame(); |
| FrameTreeNode* tab1_root = tab1_rfh->frame_tree_node(); |
| |
| Shell* tab2 = CreateBrowser(); |
| // Create an unrelated window and navigate it to A2. Workaround: navigate |
| // `tab2` to a random site before navigating to attacker's site to ensure that |
| // `tab2` shares the process with tab1, rather than staying in its initial |
| // process. |
| ASSERT_TRUE(NavigateToURL( |
| tab2, embedded_test_server()->GetURL("d.test", "/title3.html"))); |
| ASSERT_TRUE(NavigateToURL( |
| tab2, embedded_test_server()->GetURL("a.test", "/title2.html"))); |
| RenderFrameHostImpl* tab2_rfh = static_cast<RenderFrameHostImpl*>( |
| tab2->web_contents()->GetPrimaryMainFrame()); |
| FrameTreeNode* tab2_root = tab2_rfh->frame_tree_node(); |
| EXPECT_FALSE(tab1_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| tab2_rfh->GetSiteInstance())); |
| EXPECT_EQ(tab1_rfh->GetProcess(), tab2_rfh->GetProcess()); |
| |
| // Open a cross-site page B (still controlled by the attacker) in the |
| // same BCG as A2. |
| GURL attacker_cross_site_url( |
| embedded_test_server()->GetURL("b.test", "/title3.html")); |
| Shell* tab3 = OpenPopup(tab2, attacker_cross_site_url, "foo"); |
| RenderFrameHostImpl* tab3_rfh = static_cast<RenderFrameHostImpl*>( |
| tab3->web_contents()->GetPrimaryMainFrame()); |
| FrameTreeNode* tab3_root = tab3_rfh->frame_tree_node(); |
| EXPECT_EQ(tab3_root->opener(), tab2_root); |
| EXPECT_TRUE(tab2_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| tab3_rfh->GetSiteInstance())); |
| |
| // Grab the token of the A1 document that will be set as the new opener. |
| blink::LocalFrameToken opener_token = tab1_rfh->GetFrameToken(); |
| |
| // Navigate A1 to a WebUI URL. This should force a BrowsingInstance swap. The |
| // original RenderFrameHost is placed into BFCache. Note that the popups |
| // created so far don't stop BFCache from working because they're in a |
| // separate BCG. |
| GURL chrome_url(std::string(kChromeUIScheme) + "://" + |
| std::string(kChromeUIGpuHost)); |
| EXPECT_TRUE(NavigateToURL(shell(), chrome_url)); |
| RenderFrameHostImpl* tab1_webui_rfh = tab1_root->current_frame_host(); |
| EXPECT_NE(tab1_rfh, tab1_webui_rfh); |
| EXPECT_TRUE(tab1_rfh->IsInBackForwardCache()); |
| EXPECT_FALSE(tab1_webui_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| tab2_rfh->GetSiteInstance())); |
| EXPECT_FALSE(tab1_webui_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| tab3_rfh->GetSiteInstance())); |
| |
| // Look up the proxy that represents B in A2's SiteInstance (both |
| // controlled by the attacker). This is the target of the malicious |
| // DidChangeOpener IPC. This should exist as long as a.test and b.test |
| // are process-isolated from each other. |
| RenderFrameProxyHost* proxy = |
| tab3_rfh->browsing_context_state()->GetRenderFrameProxyHost( |
| tab2_rfh->GetSiteInstance()->group()); |
| ASSERT_TRUE(proxy); |
| |
| // Simulate the vulnerability by sending the DidChangeOpener IPC using the |
| // token of the BFCached frame. This goes to the RenderFrameHostManager of |
| // tab3, trying to set its opener to the FTN that's currently showing the |
| // WebUI page. |
| proxy->DidChangeOpener(opener_token); |
| |
| // The opener should *not* be updated to tab1_root. It should stay the same, |
| // still pointing to tab2. |
| EXPECT_NE(tab3_root->opener(), tab1_root); |
| EXPECT_EQ(tab3_root->opener(), tab2_root); |
| |
| // Normally, opener updates trigger proxy creation to ensure that the frame |
| // that had its opener updated has access to the new opener chain. Ensure we |
| // didn't create a RenderFrameProxyHost for the WebUI page in tab3's |
| // SiteInstanceGroup, connecting the WebUI page to the attacker-controlled |
| // tab3. |
| RenderFrameProxyHost* webui_to_non_webui_proxy = |
| tab1_webui_rfh->browsing_context_state()->GetRenderFrameProxyHost( |
| tab3_rfh->GetSiteInstance()->group()); |
| EXPECT_FALSE(webui_to_non_webui_proxy); |
| } |
| |
| // Similar to OpenerChangeDisallowedAcrossBrowsingInstances_Bfcache, but |
| // exercises the case where the supplied opener token resolves to a speculative |
| // RenderFrameHost (which happens to be in the popup tab itself). This should |
| // fail even within the same BCG because the new opener is still in the |
| // speculative state and not the current frame of its FrameTreeNode. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OpenerChangeDisallowedViaSpeculativeFrame) { |
| // Explicitly isolate a.test to ensure that this test is applicable on |
| // platforms without site-per-process. |
| IsolateOrigin("a.test"); |
| |
| // Navigate to A1. |
| EXPECT_TRUE(NavigateToURL( |
| shell(), embedded_test_server()->GetURL("a.test", "/title1.html"))); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* tab1_rfh = web_contents->GetPrimaryMainFrame(); |
| FrameTreeNode* tab1_root = tab1_rfh->frame_tree_node(); |
| |
| // Open A2 in the same BCG as A1. |
| GURL popup_url(embedded_test_server()->GetURL("a.test", "/title2.html")); |
| Shell* tab2 = OpenPopup(shell(), popup_url, "foo"); |
| RenderFrameHostImpl* tab2_rfh = static_cast<RenderFrameHostImpl*>( |
| tab2->web_contents()->GetPrimaryMainFrame()); |
| FrameTreeNode* tab2_root = tab2_rfh->frame_tree_node(); |
| EXPECT_EQ(tab2_root->opener(), tab1_root); |
| EXPECT_TRUE(tab1_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| tab2_rfh->GetSiteInstance())); |
| |
| // Navigate tab 2 to B in the same BCG. |
| GURL b_url(embedded_test_server()->GetURL("b.test", "/title1.html")); |
| { |
| TestNavigationObserver nav_observer(tab2->web_contents(), 1); |
| EXPECT_TRUE(ExecJs(tab2, JsReplace("location.href = $1;", b_url))); |
| nav_observer.Wait(); |
| } |
| tab2_rfh = static_cast<RenderFrameHostImpl*>( |
| tab2->web_contents()->GetPrimaryMainFrame()); |
| EXPECT_EQ(tab2_root->opener(), tab1_root); |
| EXPECT_TRUE(tab1_rfh->GetSiteInstance()->IsRelatedSiteInstance( |
| tab2_rfh->GetSiteInstance())); |
| |
| // Start a cross-process navigation in tab 2 back to A, and pause it once the |
| // speculative RenderFrameHost has been created. |
| GURL dest_url(embedded_test_server()->GetURL("a.test", "/title3.html")); |
| TestNavigationManager nav_manager(tab2->web_contents(), dest_url); |
| EXPECT_TRUE(ExecJs(tab2, JsReplace("location.href = $1;", dest_url))); |
| nav_manager.WaitForSpeculativeRenderFrameHostCreation(); |
| RenderFrameHostImpl* speculative_rfh = |
| tab2_root->render_manager()->speculative_frame_host(); |
| ASSERT_TRUE(speculative_rfh); |
| ASSERT_EQ(speculative_rfh->GetProcess(), tab1_rfh->GetProcess()); |
| ASSERT_EQ(tab2_root->current_frame_host(), tab2_rfh); |
| |
| // Find SiteInstance A's proxy for tab 2, as the target for the IPC. |
| RenderFrameProxyHost* proxy = |
| tab2_rfh->browsing_context_state()->GetRenderFrameProxyHost( |
| tab1_rfh->GetSiteInstance()->group()); |
| ASSERT_TRUE(proxy); |
| |
| // Grab the speculative RenderFrameHost's frame token and simulate a rogue |
| // DidChangeOpener IPC that supplies it as the new opener for tab 2, which |
| // would (legally) create a cycle if it succeeds, but should fail because the |
| // token is for a speculative RFH. |
| blink::LocalFrameToken opener_token = speculative_rfh->GetFrameToken(); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(tab1_rfh->GetProcess()); |
| proxy->DidChangeOpener(opener_token); |
| |
| // The opener should *not* be updated to tab2_root, since the frame token is |
| // for a speculative RFH. |
| EXPECT_EQ(tab2_root->opener(), tab1_root); |
| |
| // The renderer that supplied a speculative frame's token as opener should |
| // be terminated. |
| EXPECT_EQ(bad_message::RFH_INACTIVE_CHECK_FROM_SPECULATIVE_RFH, |
| 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()); |
| } |
| |
| namespace { |
| |
| // An interceptor class that allows replacing the PageState of the DidCommit IPC |
| // from the renderer process to the browser process. |
| class DidCommitPageStateReplacer : public DidCommitNavigationInterceptor { |
| public: |
| DidCommitPageStateReplacer(WebContents* web_contents, |
| const blink::PageState& page_state) |
| : DidCommitNavigationInterceptor(web_contents), |
| replacement_page_state_(page_state) {} |
| |
| DidCommitPageStateReplacer(const DidCommitPageStateReplacer&) = delete; |
| DidCommitPageStateReplacer& operator=(const DidCommitPageStateReplacer&) = |
| delete; |
| |
| ~DidCommitPageStateReplacer() override = default; |
| |
| protected: |
| bool WillProcessDidCommitNavigation( |
| RenderFrameHost* render_frame_host, |
| NavigationRequest* navigation_request, |
| mojom::DidCommitProvisionalLoadParamsPtr* params, |
| mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) |
| override { |
| (**params).page_state = replacement_page_state_; |
| return true; |
| } |
| |
| private: |
| blink::PageState replacement_page_state_; |
| }; |
| |
| } // namespace |
| |
| // Test that committing a navigation with a PageState that does not list all of |
| // its file paths in GetReferencedFiles will cause a renderer kill. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PageStateWithUnlistedFile) { |
| // Navigate to foo.com initially. |
| GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), foo_url)); |
| |
| // Create a PageState that contains a file path which isn't in the list of |
| // referenced files which are validated. |
| GURL foo_url2(embedded_test_server()->GetURL("foo.com", "/title2.html")); |
| blink::ExplodedPageState exploded_page_state; |
| ASSERT_TRUE(blink::DecodePageState( |
| blink::PageState::CreateFromURL(foo_url2).ToEncodedData(), |
| &exploded_page_state)); |
| scoped_refptr<network::ResourceRequestBody> request_body = |
| new network::ResourceRequestBody(); |
| base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits"); |
| request_body->AppendFileRange( |
| bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| exploded_page_state.top.http_body.request_body = request_body; |
| exploded_page_state.top.http_body.http_content_type = u"text/plain"; |
| std::string encoded_page_state; |
| blink::EncodePageState(exploded_page_state, &encoded_page_state); |
| blink::PageState page_state = |
| blink::PageState::CreateFromEncodedData(encoded_page_state); |
| |
| // Create an interceptor which will put the modified PageState into the next |
| // navigation's DidCommit message. |
| DidCommitPageStateReplacer page_state_replacer(shell()->web_contents(), |
| page_state); |
| |
| // Navigate in the same renderer process to send the bad PageState. |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), foo_url2)); |
| |
| // Verify that the malicious renderer was killed, for the right reason. |
| EXPECT_EQ(bad_message::RFH_CAN_ACCESS_FILES_OF_PAGE_STATE_AT_COMMIT, |
| kill_waiter.Wait()); |
| } |
| |
| // Similar to the test above, but also uses a malformed DocumentState within the |
| // corrupted PageState, to make it harder to find file paths that are present. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| PageStateWithUnlistedFileAndBadDocumentState) { |
| // Navigate to foo.com initially. |
| GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), foo_url)); |
| |
| // Create a PageState that contains a file path which isn't in the list of |
| // referenced files which are validated. |
| GURL foo_url2(embedded_test_server()->GetURL("foo.com", "/title2.html")); |
| blink::ExplodedPageState exploded_page_state; |
| ASSERT_TRUE(blink::DecodePageState( |
| blink::PageState::CreateFromURL(foo_url2).ToEncodedData(), |
| &exploded_page_state)); |
| scoped_refptr<network::ResourceRequestBody> request_body = |
| new network::ResourceRequestBody(); |
| base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits"); |
| request_body->AppendFileRange( |
| bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| exploded_page_state.top.http_body.request_body = request_body; |
| exploded_page_state.top.http_body.http_content_type = u"text/plain"; |
| |
| // Also modify the DocumentState to force RecursivelyAppendReferencedFiles to |
| // return false. |
| exploded_page_state.top.document_state = {u"one", u"two"}; |
| |
| std::string encoded_page_state; |
| blink::EncodePageState(exploded_page_state, &encoded_page_state); |
| blink::PageState page_state = |
| blink::PageState::CreateFromEncodedData(encoded_page_state); |
| |
| // Create an interceptor which will put the modified PageState into the next |
| // navigation's DidCommit message. |
| DidCommitPageStateReplacer page_state_replacer(shell()->web_contents(), |
| page_state); |
| |
| // Navigate in the same renderer process to send the bad PageState. |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), foo_url2)); |
| |
| // Verify that the malicious renderer was killed, for the right reason. |
| EXPECT_EQ(bad_message::RFH_CAN_ACCESS_FILES_OF_PAGE_STATE_AT_COMMIT, |
| kill_waiter.Wait()); |
| } |
| |
| // Inner delegate attachment for MimeHandlerView has an extra phase in the |
| // beginning, invoked via RenderFrameHost::PrepareForInnerContentsAttach(), |
| // which currently invokes beforeunload handlers prior to proceeding with |
| // normal attachment in AttachInnerWebContents()/AttachGuestPage() in a later |
| // task. Ensure that a misbehaving renderer doesn't trigger RenderFrameDeleted() |
| // via an unload ACK in that first PrepareForInnerContentsAttach() phase, where |
| // we aren't actually expecting that unload ACK. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| SpoofUnloadACKDuringInnerDelegateAttach) { |
| // Start on a page with a blank iframe, simulating the normal starting point |
| // of attaching an inner delegate (e.g., for MimeHandlerView) to a placeholder |
| // subframe. |
| GURL main_url( |
| embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* subframe = main_frame->child_at(0)->current_frame_host(); |
| |
| // Add a beforeunload handler to the blank subframe. This shouldn't ever |
| // happen during the normal inner delegate attachment flow, but a compromised |
| // renderer can still do it. This is not technically necessary for the |
| // renderer to send a spoofed unlock ACK while we're in this phase, but it |
| // makes the time window where this ACK can lead to problems larger and more |
| // practical. |
| // TODO(crbug.com/40249634): Make it impossible for placeholder |
| // frames to trigger beforeunload during inner delegate attachment. |
| subframe->DisableBeforeUnloadHangMonitorForTesting(); |
| EXPECT_TRUE(ExecJs(subframe, "window.onbeforeunload = function() {};")); |
| |
| // Prepare for inner web contents attach (this simulates MimeHandlerView |
| // creating the PDF inner delegate). This currently triggers the beforeunload |
| // phase. |
| base::test::TestFuture<RenderFrameHost*> future; |
| subframe->PrepareForInnerWebContentsAttach(future.GetCallback()); |
| |
| // Verify that the subframe is waiting for a beforeunload ACK and also |
| // attaching an inner delegate. |
| EXPECT_TRUE(subframe->is_waiting_for_beforeunload_completion()); |
| EXPECT_TRUE(subframe->frame_tree_node() |
| ->render_manager() |
| ->is_attaching_inner_delegate()); |
| |
| RenderFrameHostWrapper observer(subframe); |
| |
| // Spoof DidUnloadRenderFrame IPC while waiting for beforeunload ACK. |
| subframe->OnUnloadACK(); |
| |
| // Ensure the subframe is still live, and its RenderFrame is not deleted. |
| ASSERT_FALSE(observer.IsRenderFrameDeleted()); |
| |
| // Check that we're still in the inner delegate attachment phase. |
| EXPECT_TRUE(subframe->frame_tree_node() |
| ->render_manager() |
| ->is_attaching_inner_delegate()); |
| |
| // The RFH shouldn't change when inner delegate attachment is ready to |
| // proceed. |
| EXPECT_EQ(subframe, future.Get()); |
| |
| // Make sure proceeding with the inner WebContents attachment succeeds. |
| EXPECT_TRUE(CreateAndAttachInnerContents(subframe)); |
| EXPECT_TRUE(subframe->frame_tree_node() |
| ->render_manager() |
| ->is_inner_delegate_attached()); |
| } |
| |
| // Test that when a compromised renderer sends an unload ACK for a placeholder |
| // frame after its inner delegate is attached, we ensure that any children |
| // created by the compromised renderer are properly deleted, preventing |
| // ghost frames. See https://crbug.com/517241992. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| NoChildrenAfterUnloadACKWithInnerDelegateAttached) { |
| // Start on a page with a blank iframe, simulating the normal starting point |
| // of attaching an inner delegate (e.g., for MimeHandlerView) to a placeholder |
| // subframe. |
| GURL main_url( |
| embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* subframe = main_frame->child_at(0)->current_frame_host(); |
| |
| // Add a child frame to the subframe so that we can test if it gets properly |
| // detached. A compromised renderer could create this child. |
| EXPECT_TRUE(ExecJs(subframe, |
| "let f = document.createElement('iframe'); " |
| "document.body.appendChild(f);")); |
| ASSERT_EQ(1U, subframe->child_count()); |
| RenderFrameHostImpl* grandchild = subframe->child_at(0)->current_frame_host(); |
| RenderFrameHostWrapper grandchild_observer(grandchild); |
| |
| // Attach an inner delegate. |
| // This synchronously calls SwapOuterDelegateFrame() which sends the Unload |
| // IPC, and it sets is_inner_delegate_attached(). |
| EXPECT_TRUE(CreateAndAttachInnerContents(subframe)); |
| |
| EXPECT_TRUE(subframe->frame_tree_node() |
| ->render_manager() |
| ->is_inner_delegate_attached()); |
| |
| // Pretend that the compromised renderer sent the unload ACK without first |
| // detaching the grandchild frame. |
| subframe->OnUnloadACK(); |
| |
| // Ensure the subframe is no longer live and that its child frame was deleted. |
| EXPECT_TRUE(grandchild_observer.IsRenderFrameDeleted()); |
| EXPECT_EQ(0U, subframe->child_count()); |
| } |
| |
| // Test that the placeholder frame for an inner delegate has its children |
| // cleared synchronously at attach time, without depending on the renderer's |
| // DidUnloadRenderFrame ACK (which a misbehaving renderer may withhold), and |
| // that the placeholder cannot acquire new children after the inner delegate is |
| // attached. See https://crbug.com/518078552. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| NoChildrenOnInnerDelegatePlaceholderAfterAttach) { |
| // Start on a page with a blank iframe, simulating the normal starting point |
| // of attaching an inner delegate (e.g., for MimeHandlerView) to a placeholder |
| // subframe. |
| GURL main_url( |
| embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = web_contents->GetPrimaryMainFrame(); |
| RenderFrameHostImpl* subframe = main_frame->child_at(0)->current_frame_host(); |
| |
| // Add a child frame to the subframe so that we can test if it gets properly |
| // detached. A compromised renderer could create this child. |
| EXPECT_TRUE(ExecJs(subframe, |
| "let f = document.createElement('iframe'); " |
| "document.body.appendChild(f);")); |
| ASSERT_EQ(1U, subframe->child_count()); |
| RenderFrameHostImpl* grandchild = subframe->child_at(0)->current_frame_host(); |
| RenderFrameHostWrapper grandchild_observer(grandchild); |
| |
| // Swallow the unload ACK so that any cleanup observed below is solely due to |
| // browser-side attach logic and not the renderer's DidUnloadRenderFrame ACK. |
| subframe->SetUnloadACKCallbackForTesting( |
| base::BindRepeating([]() { return true; })); |
| |
| // Attach an inner delegate. This synchronously calls SwapOuterDelegateFrame() |
| // which sends the Unload IPC, and it sets is_inner_delegate_attached(). |
| EXPECT_TRUE(CreateAndAttachInnerContents(subframe)); |
| EXPECT_TRUE(subframe->frame_tree_node() |
| ->render_manager() |
| ->is_inner_delegate_attached()); |
| EXPECT_TRUE(subframe->inner_tree_main_frame_tree_node_id()); |
| |
| // The placeholder frame must have no children immediately after attach, |
| // independently of whether the renderer ACKs the unload. |
| EXPECT_TRUE(grandchild_observer.IsRenderFrameDeleted()); |
| EXPECT_EQ(0U, subframe->child_count()); |
| |
| // Simulate the renderer attempting to create a new child on the placeholder |
| // frame after the inner delegate has been attached. The placeholder's render |
| // frame is still considered created in the browser (since the unload ACK was |
| // swallowed above), so OnCreateChildFrame()'s lifecycle checks alone would |
| // not reject this; the dedicated inner-delegate check should drop it. |
| ASSERT_TRUE(subframe->IsRenderFrameLive()); |
| mojo::AssociatedRemote<mojom::Frame> frame_remote; |
| std::ignore = frame_remote.BindNewEndpointAndPassDedicatedReceiver(); |
| mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker> bib_remote; |
| mojo::AssociatedReceiver<blink::mojom::AssociatedInterfaceProvider> |
| associated_interface_provider_receiver(nullptr); |
| std::ignore = associated_interface_provider_receiver |
| .BindNewEndpointAndPassDedicatedRemote(); |
| subframe->OnCreateChildFrame( |
| subframe->GetProcess()->GetNextRoutingID(), frame_remote.Unbind(), |
| bib_remote.InitWithNewPipeAndPassReceiver(), |
| blink::mojom::PolicyContainerBindParams::New( |
| mojo::PendingAssociatedRemote<blink::mojom::PolicyContainerHost>() |
| .InitWithNewEndpointAndPassReceiver()), |
| associated_interface_provider_receiver.Unbind(), |
| blink::mojom::TreeScopeType::kDocument, "", "uniqueName1", |
| /*is_created_by_script=*/false, blink::LocalFrameToken(), |
| base::UnguessableToken::Create(), blink::DocumentToken(), |
| blink::InitiatorStateToken(), blink::FramePolicy(), |
| blink::mojom::FrameOwnerProperties(), |
| blink::FrameOwnerElementType::kIframe, ukm::kInvalidSourceId); |
| |
| // The placeholder frame must still have no children. |
| EXPECT_EQ(0U, subframe->child_count()); |
| } |
| |
| 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 DidCommitTransitionReplacer : public DidCommitNavigationInterceptor { |
| public: |
| DidCommitTransitionReplacer(WebContents* web_contents, |
| ui::PageTransition transition_to_inject) |
| : DidCommitNavigationInterceptor(web_contents), |
| transition_to_inject_(transition_to_inject) {} |
| |
| DidCommitTransitionReplacer(const DidCommitTransitionReplacer&) = delete; |
| DidCommitTransitionReplacer& operator=(const DidCommitTransitionReplacer&) = |
| delete; |
| |
| ~DidCommitTransitionReplacer() override = default; |
| |
| void Activate() { is_activated_ = true; } |
| |
| protected: |
| bool WillProcessDidCommitNavigation( |
| RenderFrameHost* render_frame_host, |
| NavigationRequest* navigation_request, |
| mojom::DidCommitProvisionalLoadParamsPtr* params, |
| mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params) |
| override { |
| if (is_activated_) { |
| (**params).transition = transition_to_inject_; |
| is_activated_ = false; |
| } |
| return true; |
| } |
| |
| private: |
| ui::PageTransition transition_to_inject_; |
| bool is_activated_ = false; |
| }; |
| |
| class SecurityExploitPageTransitionTest |
| : public SecurityExploitBrowserTest, |
| public ::testing::WithParamInterface<ui::PageTransition> {}; |
| |
| // TODO(https://crbug.com/562797793): Re-enable once |
| // RFH_COMMIT_NAVIGATION_NON_WEBBY_TRANSITION is re-enabled. |
| IN_PROC_BROWSER_TEST_P(SecurityExploitPageTransitionTest, |
| DISABLED_DidCommitNonWebbyTransition) { |
| ui::PageTransition transition = GetParam(); |
| WebContents* web_contents = shell()->web_contents(); |
| DidCommitTransitionReplacer injector(web_contents, transition); |
| |
| GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(web_contents, main_url)); |
| |
| RenderProcessHost* main_process = |
| web_contents->GetPrimaryMainFrame()->GetProcess(); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); |
| |
| injector.Activate(); |
| ExecuteScriptAsync(web_contents, |
| "history.pushState({}, '', '/title2.html');"); |
| |
| EXPECT_EQ(bad_message::RFH_COMMIT_NAVIGATION_NON_WEBBY_TRANSITION, |
| kill_waiter.Wait()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| SecurityExploitPageTransitionTest, |
| ::testing::Values(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)); |
| |
| // TODO(https://crbug.com/562797793): Re-enable once |
| // RFH_COMMIT_NAVIGATION_BROWSER_INITIATED_TRANSITION_MISMATCH is re-enabled. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| DISABLED_DidCommitBrowserInitiatedTransitionMismatch) { |
| WebContents* web_contents = shell()->web_contents(); |
| DidCommitTransitionReplacer injector(web_contents, |
| ui::PAGE_TRANSITION_AUTO_BOOKMARK); |
| |
| GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(web_contents, main_url)); |
| |
| RenderProcessHost* main_process = |
| web_contents->GetPrimaryMainFrame()->GetProcess(); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); |
| |
| injector.Activate(); |
| ExecuteScriptAsync(web_contents, "window.location = '/title2.html';"); |
| |
| EXPECT_EQ( |
| bad_message::RFH_COMMIT_NAVIGATION_BROWSER_INITIATED_TRANSITION_MISMATCH, |
| kill_waiter.Wait()); |
| } |
| |
| class SecurityExploitDisallowedQualifierTest |
| : public SecurityExploitBrowserTest, |
| public ::testing::WithParamInterface<ui::PageTransition> {}; |
| |
| // TODO(https://crbug.com/562797793): Re-enable once |
| // RFH_COMMIT_NAVIGATION_DISALLOWED_QUALIFIER is re-enabled. |
| IN_PROC_BROWSER_TEST_P(SecurityExploitDisallowedQualifierTest, |
| DISABLED_DidCommitDisallowedQualifier) { |
| ui::PageTransition transition = GetParam(); |
| WebContents* web_contents = shell()->web_contents(); |
| DidCommitTransitionReplacer injector(web_contents, transition); |
| |
| GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(web_contents, main_url)); |
| |
| RenderProcessHost* main_process = |
| web_contents->GetPrimaryMainFrame()->GetProcess(); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process); |
| |
| injector.Activate(); |
| ExecuteScriptAsync(web_contents, "window.location = '/title2.html';"); |
| |
| EXPECT_EQ(bad_message::RFH_COMMIT_NAVIGATION_DISALLOWED_QUALIFIER, |
| kill_waiter.Wait()); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| SecurityExploitDisallowedQualifierTest, |
| ::testing::Values( |
| static_cast<ui::PageTransition>(ui::PAGE_TRANSITION_LINK | |
| ui::PAGE_TRANSITION_FROM_ADDRESS_BAR), |
| static_cast<ui::PageTransition>(ui::PAGE_TRANSITION_LINK | |
| ui::PAGE_TRANSITION_HOME_PAGE), |
| static_cast<ui::PageTransition>(ui::PAGE_TRANSITION_LINK | |
| ui::PAGE_TRANSITION_FROM_API), |
| static_cast<ui::PageTransition>(ui::PAGE_TRANSITION_LINK | |
| ui::PAGE_TRANSITION_FORWARD_BACK), |
| static_cast<ui::PageTransition>(ui::PAGE_TRANSITION_LINK | |
| ui::PAGE_TRANSITION_SERVER_REDIRECT))); |
| |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| DidCommitInvalidTransitionForSubframe) { |
| WebContents* web_contents = shell()->web_contents(); |
| DidCommitTransitionReplacer injector(web_contents, ui::PAGE_TRANSITION_LINK); |
| |
| GURL main_url(embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(a)")); |
| EXPECT_TRUE(NavigateToURL(web_contents, main_url)); |
| |
| FrameTreeNode* root = |
| static_cast<WebContentsImpl*>(web_contents)->GetPrimaryFrameTree().root(); |
| FrameTreeNode* child = root->child_at(0); |
| RenderProcessHost* child_process = child->current_frame_host()->GetProcess(); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(child_process); |
| |
| injector.Activate(); |
| ExecuteScriptAsync(child->current_frame_host(), |
| "window.location = '/title2.html';"); |
| |
| EXPECT_EQ(bad_message::RFH_COMMIT_NAVIGATION_TRANSITION_FRAME_TYPE_MISMATCH, |
| 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() /* actual_navigation_start */, |
| 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 */, |
| rfh->current_initiator_state_token(), rfh->GetDocumentToken(), |
| 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 */, |
| base::TimeTicks() /* renderer_before_unload_start */, |
| base::TimeTicks() /* renderer_before_unload_end */, |
| base::TimeTicks() /* before_unload_dialog_opened */, |
| base::TimeTicks() /* before_unload_dialog_closed */, |
| false /* started_with_transient_activation */, |
| false /* started_by_ad */, false /* is_container_initiated */, |
| false /* has_rel_opener */, |
| std::nullopt /* script_tool_invocation_id */, |
| /*script_injector_host=*/""); |
| |
| // 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(), |
| mojo::NullReceiver(), mojo::NullReceiver()); |
| EXPECT_EQ(bad_message::RFH_BASE_URL_FOR_DATA_URL_SPECIFIED, |
| process_kill_waiter.Wait()); |
| |
| EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->CanReadFile( |
| rfh->GetProcess()->GetID(), file_path)); |
| |
| // Reload the page to create another renderer process. |
| TestNavigationObserver tab_observer(shell()->web_contents(), 1); |
| shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false); |
| tab_observer.Wait(); |
| |
| // Make an XHR request to check if the page has access. |
| std::string script = base::StringPrintf( |
| "var xhr = new XMLHttpRequest()\n" |
| "xhr.open('GET', '%s', false);\n" |
| "try { xhr.send(); } catch (e) {}\n" |
| "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() /* actual_navigation_start */, |
| 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 */, |
| rfh->current_initiator_state_token(), rfh->GetDocumentToken(), |
| 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 */, |
| base::TimeTicks() /* renderer_before_unload_start */, |
| base::TimeTicks() /* renderer_before_unload_end */, |
| base::TimeTicks() /* before_unload_dialog_opened */, |
| base::TimeTicks() /* before_unload_dialog_closed */, |
| false /* started_with_transient_activation */, |
| false /* started_by_ad */, false /* is_container_initiated */, |
| false /* has_rel_opener */, |
| std::nullopt /* script_tool_invocation_id */, |
| /*script_injector_host=*/""); |
| |
| // 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(), |
| mojo::NullReceiver(), mojo::NullReceiver()); |
| EXPECT_EQ(bad_message::RFH_INITIATOR_BASE_URL_IS_EMPTY, |
| process_kill_waiter.Wait()); |
| } |
| |
| // Test to verify that an exploited renderer process trying to specify a nullopt |
| // initiator_state_token in BeginNavigation is correctly terminated. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| BeginNavigationNoInitiatorStateToken) { |
| 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"); |
| |
| blink::mojom::CommonNavigationParamsPtr common_params = |
| blink::mojom::CommonNavigationParams::New( |
| url, url::Origin::Create(start_url), |
| /*initiator_base_url=*/std::nullopt, blink::mojom::Referrer::New(), |
| ui::PAGE_TRANSITION_LINK, |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT, |
| blink::NavigationDownloadPolicy(), |
| /*should_replace_current_entry=*/false, |
| GURL() /* base_url_for_data_url */, |
| base::TimeTicks::Now() /* actual_navigation_start */, |
| base::TimeTicks::Now() /* navigation_start */, "GET", |
| nullptr /* post_data */, network::mojom::SourceLocation::New(), |
| /*started_from_context_menu=*/false, /*has_user_gesture=*/false, |
| /*text_fragment_token=*/false, network::mojom::CSPDisposition::CHECK, |
| std::vector<int>() /* initiator_origin_trial_features */, |
| std::string() /* href_translate */, |
| /*is_history_navigation_in_new_child_frame=*/false, |
| base::TimeTicks() /* input_start */, |
| network::mojom::RequestDestination::kDocument); |
| blink::mojom::BeginNavigationParamsPtr begin_params = |
| blink::mojom::BeginNavigationParams::New( |
| std::nullopt /* initiator_frame_token */, |
| std::nullopt /* initiator_state_token */, rfh->GetDocumentToken(), |
| std::string() /* headers */, net::LOAD_NORMAL, |
| /*skip_service_worker=*/false, |
| blink::mojom::RequestContextType::LOCATION, |
| blink::mojom::MixedContentContextType::kBlockable, |
| /*is_form_submission=*/false, |
| /*was_initiated_by_link_click=*/false, |
| 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 */, |
| base::TimeTicks() /* renderer_before_unload_start */, |
| base::TimeTicks() /* renderer_before_unload_end */, |
| base::TimeTicks() /* before_unload_dialog_opened */, |
| base::TimeTicks() /* before_unload_dialog_closed */, |
| /*started_with_transient_activation=*/false, |
| /*started_by_ad=*/false, /*is_container_initiated=*/false, |
| /*has_rel_opener=*/false, |
| std::nullopt /* script_tool_invocation_id */, |
| /*script_injector_host=*/""); |
| |
| // 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(), |
| mojo::NullReceiver(), mojo::NullReceiver()); |
| EXPECT_EQ(bad_message::RFH_BEGIN_NAVIGATION_NO_INITIATOR_TOKENS, |
| process_kill_waiter.Wait()); |
| } |
| |
| // Test to verify that an exploited renderer process trying to specify a nullopt |
| // initiator_document_token in BeginNavigation is correctly terminated. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| BeginNavigationNoInitiatorDocumentToken) { |
| 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"); |
| |
| blink::mojom::CommonNavigationParamsPtr common_params = |
| blink::mojom::CommonNavigationParams::New( |
| url, url::Origin::Create(start_url), |
| /*initiator_base_url=*/std::nullopt, blink::mojom::Referrer::New(), |
| ui::PAGE_TRANSITION_LINK, |
| blink::mojom::NavigationType::DIFFERENT_DOCUMENT, |
| blink::NavigationDownloadPolicy(), |
| /*should_replace_current_entry=*/false, |
| GURL() /* base_url_for_data_url */, |
| base::TimeTicks::Now() /* actual_navigation_start */, |
| base::TimeTicks::Now() /* navigation_start */, "GET", |
| nullptr /* post_data */, network::mojom::SourceLocation::New(), |
| /*started_from_context_menu=*/false, /*has_user_gesture=*/false, |
| /*text_fragment_token=*/false, network::mojom::CSPDisposition::CHECK, |
| std::vector<int>() /* initiator_origin_trial_features */, |
| std::string() /* href_translate */, |
| /*is_history_navigation_in_new_child_frame=*/false, |
| base::TimeTicks() /* input_start */, |
| network::mojom::RequestDestination::kDocument); |
| blink::mojom::BeginNavigationParamsPtr begin_params = |
| blink::mojom::BeginNavigationParams::New( |
| std::nullopt /* initiator_frame_token */, |
| rfh->current_initiator_state_token(), std::nullopt, |
| std::string() /* headers */, net::LOAD_NORMAL, |
| /*skip_service_worker=*/false, |
| blink::mojom::RequestContextType::LOCATION, |
| blink::mojom::MixedContentContextType::kBlockable, |
| /*is_form_submission=*/false, |
| /*was_initiated_by_link_click=*/false, |
| 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 */, |
| base::TimeTicks() /* renderer_before_unload_start */, |
| base::TimeTicks() /* renderer_before_unload_end */, |
| base::TimeTicks() /* before_unload_dialog_opened */, |
| base::TimeTicks() /* before_unload_dialog_closed */, |
| /*started_with_transient_activation=*/false, |
| /*started_by_ad=*/false, /*is_container_initiated=*/false, |
| /*has_rel_opener=*/false, |
| std::nullopt /* script_tool_invocation_id */, |
| /*script_injector_host=*/""); |
| |
| // 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(), |
| mojo::NullReceiver(), mojo::NullReceiver()); |
| EXPECT_EQ(bad_message::RFH_BEGIN_NAVIGATION_NO_INITIATOR_TOKENS, |
| process_kill_waiter.Wait()); |
| } |
| |
| // Test to verify that an exploited renderer process trying to request a |
| // keep-alive for an invalid initiator_state_token in IssueKeepAliveHandle is |
| // correctly terminated. |
| // TODO(crbug.com/564465304): Re-enable checking for the correct |
| // InitiatorStateToken in IssueKeepAlive handle once we understand why this is |
| // hit in the wild. |
| IN_PROC_BROWSER_TEST_F( |
| SecurityExploitBrowserTest, |
| DISABLED_IssueKeepAliveHandleInvalidInitiatorStateToken) { |
| GURL start_url(embedded_test_server()->GetURL("/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| // Receiving the invalid IPC message should lead to renderer process |
| // termination. |
| RenderProcessHostBadIpcMessageWaiter process_kill_waiter(rfh->GetProcess()); |
| |
| mojo::PendingRemote<blink::mojom::NavigationStateKeepAliveHandle> |
| keep_alive_remote; |
| rfh->local_frame_host_receiver_for_testing().impl()->IssueKeepAliveHandle( |
| keep_alive_remote.InitWithNewPipeAndPassReceiver(), |
| blink::InitiatorStateToken()); |
| |
| EXPECT_EQ(bad_message::RFH_ISSUE_KEEP_ALIVE_HANDLE_INVALID_INITIATOR_TOKEN, |
| 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"))); |
| } |
| |
| // 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, compromised_rfh); |
| 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()); |
| } |
| |
| namespace { |
| |
| // Interceptor that replaces the origin in the DidCommitProvisionalLoadParams |
| // with the specified value for the first DidCommitProvisionalLoad message it |
| // observes in the given |web_contents| while in scope. |
| class DidCommitParamsOriginReplacer : public DidCommitNavigationInterceptor { |
| public: |
| DidCommitParamsOriginReplacer(WebContents* web_contents, |
| url::Origin origin_override) |
| : DidCommitNavigationInterceptor(web_contents), |
| origin_override_(std::move(origin_override)) {} |
| |
| DidCommitParamsOriginReplacer(const DidCommitParamsOriginReplacer&) = delete; |
| DidCommitParamsOriginReplacer& operator=( |
| const DidCommitParamsOriginReplacer&) = delete; |
| |
| ~DidCommitParamsOriginReplacer() override = default; |
| |
| protected: |
| bool WillProcessDidCommitNavigation( |
| RenderFrameHost* render_frame_host, |
| NavigationRequest* navigation_request, |
| mojom::DidCommitProvisionalLoadParamsPtr* params, |
| mojom::DidCommitProvisionalLoadInterfaceParamsPtr*) override { |
| (*params)->origin = origin_override_; |
| |
| return true; |
| } |
| |
| private: |
| url::Origin origin_override_; |
| }; |
| |
| // Interceptor that replaces the origin in DidCommitProvisionalLoadParams with |
| // the specified value for synchronous about:blank commits, which arrive without |
| // a NavigationRequest. |
| class SyncAboutBlankCommitOriginReplacer |
| : public DidCommitNavigationInterceptor { |
| public: |
| SyncAboutBlankCommitOriginReplacer(WebContents* web_contents, |
| url::Origin origin_override) |
| : DidCommitNavigationInterceptor(web_contents), |
| origin_override_(std::move(origin_override)) {} |
| |
| SyncAboutBlankCommitOriginReplacer( |
| const SyncAboutBlankCommitOriginReplacer&) = delete; |
| SyncAboutBlankCommitOriginReplacer& operator=( |
| const SyncAboutBlankCommitOriginReplacer&) = delete; |
| |
| ~SyncAboutBlankCommitOriginReplacer() override = default; |
| |
| protected: |
| bool WillProcessDidCommitNavigation( |
| RenderFrameHost* render_frame_host, |
| NavigationRequest* navigation_request, |
| mojom::DidCommitProvisionalLoadParamsPtr* params, |
| mojom::DidCommitProvisionalLoadInterfaceParamsPtr*) override { |
| if (!navigation_request && (*params)->url.IsAboutBlank()) { |
| (*params)->origin = origin_override_; |
| } |
| return true; |
| } |
| |
| private: |
| url::Origin origin_override_; |
| }; |
| |
| } // namespace |
| |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginToCommit) { |
| // Navigate normally. |
| GURL url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| // Navigate again, but make it so that the DidCommitParams origin will |
| // mismatch, causing a renderer kill. |
| GURL url_2(embedded_test_server()->GetURL("a.test", "/title2.html")); |
| NavigationHandleObserver navigation_observer(shell()->web_contents(), url_2); |
| DidCommitParamsOriginReplacer replacer(shell()->web_contents(), |
| url::Origin::Create(GURL("b.test"))); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter( |
| shell()->web_contents()->GetPrimaryMainFrame()->GetProcess()); |
| EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), url_2)); |
| EXPECT_EQ(bad_message::RFH_ORIGIN_TO_COMMIT_MISMATCH, kill_waiter.Wait()); |
| } |
| |
| // Verify that a sandboxed iframe's synchronous about:blank commit rejects a |
| // non-opaque origin when an opaque origin is expected. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| SandboxedIframeSyncCommitWithNonOpaqueOrigin) { |
| GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| url::Origin parent_origin = |
| root->current_frame_host()->GetLastCommittedOrigin(); |
| EXPECT_FALSE(parent_origin.opaque()); |
| |
| // Set up an interceptor that modifies the about:blank commit's origin to |
| // test robustness against invalid origin reporting from the renderer. |
| SyncAboutBlankCommitOriginReplacer replacer(shell()->web_contents(), |
| parent_origin); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter( |
| root->current_frame_host()->GetProcess()); |
| |
| // Create a sandboxed iframe with src="about:blank". The renderer performs a |
| // synchronous about:blank commit whose origin is modified above. |
| ExecuteScriptAsync(root, |
| "var iframe = document.createElement('iframe');" |
| "iframe.sandbox = '';" |
| "iframe.src = 'about:blank';" |
| "document.body.appendChild(iframe);"); |
| |
| EXPECT_EQ(bad_message::RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH, |
| kill_waiter.Wait()); |
| } |
| |
| // Verify that a sandboxed iframe's synchronous about:blank commit rejects an |
| // opaque origin whose precursor does not match the expected precursor. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| SandboxedIframeSyncCommitWithMismatchedOpaqueOrigin) { |
| GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| |
| // Set up an interceptor that modifies the about:blank commit's origin to an |
| // opaque origin with no precursor, testing error handling when precursors do |
| // not match. |
| SyncAboutBlankCommitOriginReplacer replacer(shell()->web_contents(), |
| url::Origin()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter( |
| root->current_frame_host()->GetProcess()); |
| |
| // Create a sandboxed iframe with src="about:blank". The renderer performs a |
| // synchronous about:blank commit whose origin is modified above. |
| ExecuteScriptAsync(root, |
| "var iframe = document.createElement('iframe');" |
| "iframe.sandbox = '';" |
| "iframe.src = 'about:blank';" |
| "document.body.appendChild(iframe);"); |
| |
| EXPECT_EQ(bad_message::RFHI_SYNCHRONOUS_COMMIT_ORIGIN_MISMATCH, |
| kill_waiter.Wait()); |
| } |
| |
| // Ensure that the renderer is terminated if it sends an OpenURL IPC with an |
| // invalid SWITCH_TO_TAB disposition. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, OpenURLInvalidDisposition) { |
| 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()); |
| RenderFrameHostImpl* main_frame = |
| web_contents->GetPrimaryFrameTree().root()->current_frame_host(); |
| |
| // Simulate that the renderer is compromised and sends an IPC to OpenURL |
| // with an invalid disposition (SWITCH_TO_TAB). |
| auto params = |
| CreateOpenURLParams(GURL("http://b.com/title2.html"), main_frame); |
| params->disposition = WindowOpenDisposition::SWITCH_TO_TAB; |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess()); |
| static_cast<mojom::FrameHost*>(main_frame)->OpenURL(std::move(params)); |
| EXPECT_EQ(bad_message::RFH_OPEN_URL_INVALID_DISPOSITION, kill_waiter.Wait()); |
| EXPECT_FALSE(main_frame->IsRenderFrameLive()); |
| } |
| |
| // Ensure that the renderer is terminated if it sends a CreateNewWindow IPC with |
| // an invalid SWITCH_TO_TAB disposition. Similar to the test above, except that |
| // it uses CreateNewWindow instead of OpenURL. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| CreateNewWindowInvalidDisposition) { |
| 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()); |
| RenderFrameHostImpl* main_frame = |
| web_contents->GetPrimaryFrameTree().root()->current_frame_host(); |
| |
| // Simulate that the renderer is compromised and sends an IPC to OpenURL |
| // with an invalid disposition (SWITCH_TO_TAB). |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess()); |
| mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New(); |
| params->target_url = GURL("https://b.com"); |
| params->disposition = WindowOpenDisposition::SWITCH_TO_TAB; |
| main_frame->CreateNewWindow(std::move(params), base::DoNothing()); |
| EXPECT_EQ(bad_message::RFH_CREATE_NEW_WINDOW_INVALID_DISPOSITION, |
| kill_waiter.Wait()); |
| EXPECT_FALSE(main_frame->IsRenderFrameLive()); |
| } |
| |
| namespace { |
| class ScopedDumpWithoutCrashingCallback { |
| public: |
| explicit ScopedDumpWithoutCrashingCallback(base::RepeatingClosure callback) { |
| DCHECK(!callback_); |
| callback_ = std::move(callback); |
| base::debug::SetDumpWithoutCrashingFunction(&DumpWithoutCrashingHandler); |
| } |
| |
| ~ScopedDumpWithoutCrashingCallback() { |
| base::debug::SetDumpWithoutCrashingFunction(nullptr); |
| callback_.Reset(); |
| } |
| |
| private: |
| static void DumpWithoutCrashingHandler() { |
| if (callback_) { |
| callback_.Run(); |
| } |
| } |
| |
| static base::RepeatingClosure callback_; |
| }; |
| |
| base::RepeatingClosure ScopedDumpWithoutCrashingCallback::callback_; |
| |
| class NavigationHeaderInterceptor : public FrameHostInterceptor { |
| public: |
| explicit NavigationHeaderInterceptor(WebContents* web_contents) |
| : FrameHostInterceptor(web_contents) {} |
| |
| NavigationHeaderInterceptor(const NavigationHeaderInterceptor&) = delete; |
| NavigationHeaderInterceptor& operator=(const NavigationHeaderInterceptor&) = |
| delete; |
| |
| void set_headers_to_inject(const std::string& headers) { |
| headers_to_inject_ = headers; |
| } |
| |
| void Activate() { is_activated_ = true; } |
| |
| 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_ && headers_to_inject_.has_value()) { |
| (*begin_params)->headers = headers_to_inject_.value(); |
| is_activated_ = false; |
| } |
| |
| return true; |
| } |
| |
| private: |
| std::optional<std::string> headers_to_inject_; |
| bool is_activated_ = false; |
| }; |
| |
| class SecurityExploitHeaderKillDisabledTest |
| : public SecurityExploitBrowserTest { |
| public: |
| SecurityExploitHeaderKillDisabledTest() : SecurityExploitBrowserTest() { |
| feature_list_.InitWithFeatures({}, |
| {features::kKillOnInvalidNavigationHeaders}); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| } // namespace |
| |
| // Tests that the renderer is killed if it provides arbitrary headers in a |
| // navigation request when kKillOnInvalidNavigationHeaders is enabled. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| ForbiddenHeaderInBeginNavigation) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| NavigationHeaderInterceptor interceptor(shell()->web_contents()); |
| interceptor.set_headers_to_inject("Cookie: secret=123"); |
| interceptor.Activate(); |
| |
| RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess()); |
| |
| ExecuteScriptAsync(rfhi, "location = '/title2.html';"); |
| EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait()); |
| EXPECT_FALSE(rfhi->IsRenderFrameLive()); |
| } |
| |
| // Tests that the renderer is killed if it provides arbitrary headers in a |
| // navigation request when the headers are separated by LF rather than CRLF. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| ForbiddenHeaderInBeginNavigation_LFSeparated) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| NavigationHeaderInterceptor interceptor(shell()->web_contents()); |
| interceptor.set_headers_to_inject("DNT: 1\nCookie: secret=123"); |
| interceptor.Activate(); |
| |
| RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess()); |
| |
| ExecuteScriptAsync(rfhi, "location = '/title2.html';"); |
| EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait()); |
| EXPECT_FALSE(rfhi->IsRenderFrameLive()); |
| } |
| |
| // Tests that the navigation succeeds if a renderer process provides arbitrary |
| // headers in a navigation request when kKillOnInvalidNavigationHeaders is |
| // disabled. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitHeaderKillDisabledTest, |
| ForbiddenHeaderInBeginNavigation_KillDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| NavigationHeaderInterceptor interceptor(shell()->web_contents()); |
| interceptor.set_headers_to_inject("Cookie: secret=123"); |
| interceptor.Activate(); |
| |
| RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame(); |
| |
| TestNavigationObserver navigation_observer(shell()->web_contents()); |
| ExecuteScriptAsync(rfhi, "location = '/title2.html';"); |
| navigation_observer.Wait(); |
| |
| EXPECT_EQ(embedded_test_server()->GetURL("a.test", "/title2.html"), |
| shell()->web_contents()->GetLastCommittedURL()); |
| } |
| |
| // Tests that the renderer is killed if it provides arbitrary headers in an |
| // OpenURL request when kKillOnInvalidNavigationHeaders is enabled. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, ForbiddenHeaderInOpenURL) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| auto params = CreateOpenURLParams( |
| embedded_test_server()->GetURL("a.test", "/title2.html"), rfhi); |
| params->extra_headers = "Cookie: secret=123"; |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess()); |
| static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params)); |
| EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait()); |
| EXPECT_FALSE(rfhi->IsRenderFrameLive()); |
| } |
| |
| // Tests that the renderer is killed if it provides arbitrary headers in an |
| // OpenURL request when the headers are separated by LF rather than CRLF. |
| // OpenURLParams::extra_headers are normalized from LF to CRLF before they are |
| // applied to the outgoing request, so the allowlist check must apply the same |
| // normalization. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| ForbiddenHeaderInOpenURL_LFSeparated) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| auto params = CreateOpenURLParams( |
| embedded_test_server()->GetURL("a.test", "/title2.html"), rfhi); |
| params->extra_headers = "DNT: 1\nCookie: secret=123"; |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess()); |
| static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params)); |
| EXPECT_EQ(bad_message::RFH_INVALID_NAVIGATION_HEADERS, kill_waiter.Wait()); |
| EXPECT_FALSE(rfhi->IsRenderFrameLive()); |
| } |
| |
| // Tests that the navigation succeeds if a renderer process provides arbitrary |
| // headers in an OpenURL request when kKillOnInvalidNavigationHeaders is |
| // disabled. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitHeaderKillDisabledTest, |
| ForbiddenHeaderInOpenURL_KillDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| auto params = CreateOpenURLParams( |
| embedded_test_server()->GetURL("a.test", "/title2.html"), rfhi); |
| params->extra_headers = "Cookie: secret=123"; |
| |
| TestNavigationObserver navigation_observer(shell()->web_contents()); |
| static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params)); |
| navigation_observer.Wait(); |
| |
| EXPECT_EQ(embedded_test_server()->GetURL("a.test", "/title2.html"), |
| shell()->web_contents()->GetLastCommittedURL()); |
| } |
| |
| namespace { |
| // Used to communicate info about whether an Origin header was present |
| // from a URLLoaderInterceptor on the IO thread back to the test running on the |
| // UI thread. |
| struct ForbiddenOriginHeaderSharedState |
| : public base::RefCountedThreadSafe<ForbiddenOriginHeaderSharedState> { |
| ForbiddenOriginHeaderSharedState() = default; |
| std::atomic<bool> intercepted{false}; |
| std::atomic<bool> has_origin_header{false}; |
| |
| private: |
| friend class base::RefCountedThreadSafe<ForbiddenOriginHeaderSharedState>; |
| ~ForbiddenOriginHeaderSharedState() = default; |
| }; |
| } // namespace |
| |
| // Tests that if a renderer provides an Origin header on an OpenURL request, |
| // the renderer is killed. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| ForbiddenOriginHeaderInOpenURL) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| auto params = CreateOpenURLParams( |
| embedded_test_server()->GetURL("a.test", "/echoheader?origin"), rfhi); |
| params->extra_headers = "Origin: https://b.test"; |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess()); |
| |
| // Check that if a request was sent, the Origin header was stripped. |
| auto state = base::MakeRefCounted<ForbiddenOriginHeaderSharedState>(); |
| URLLoaderInterceptor interceptor(base::BindRepeating( |
| [](scoped_refptr<ForbiddenOriginHeaderSharedState> state, |
| URLLoaderInterceptor::RequestParams* params) { |
| if (params->url_request.url.path() == "/echoheader") { |
| state->intercepted = true; |
| state->has_origin_header = params->url_request.headers.HasHeader( |
| net::HttpRequestHeaders::kOrigin); |
| } |
| return false; |
| }, |
| state)); |
| |
| static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params)); |
| EXPECT_EQ(bad_message::NR_BAD_ORIGIN_HEADER, kill_waiter.Wait()); |
| |
| if (state->intercepted) { |
| EXPECT_FALSE(state->has_origin_header); |
| } |
| } |
| |
| class SecurityExploitKillDisabledBrowserTest |
| : public SecurityExploitBrowserTest { |
| public: |
| SecurityExploitKillDisabledBrowserTest() { |
| feature_list_.InitAndDisableFeature( |
| features::kKillOnUnexpectedOriginHeader); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| // Tests that if the feature flag is disabled, the renderer is not killed. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitKillDisabledBrowserTest, |
| ForbiddenOriginHeaderInOpenURL_KillDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfhi = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHost* rph = rfhi->GetProcess(); |
| |
| auto params = CreateOpenURLParams( |
| embedded_test_server()->GetURL("a.test", "/echoheader?origin"), rfhi); |
| params->extra_headers = "Origin: https://b.test"; |
| |
| TestNavigationObserver nav_observer(shell()->web_contents()); |
| static_cast<mojom::FrameHost*>(rfhi)->OpenURL(std::move(params)); |
| nav_observer.Wait(); |
| |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), rph); |
| EXPECT_TRUE(rph->IsInitializedAndNotDead()); |
| EXPECT_EQ("None", EvalJs(shell(), "document.body.innerText").ExtractString()); |
| } |
| |
| // Tests that if a renderer provides an Origin header on a GET request, |
| // the renderer is killed. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OriginHeaderGETMismatchesInitiator) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| NavigationHeaderInterceptor interceptor(shell()->web_contents()); |
| interceptor.set_headers_to_inject("Origin: https://b.test"); |
| interceptor.Activate(); |
| |
| RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess()); |
| ExecuteScriptAsync(rfhi, "location = '/echoheader?origin';"); |
| EXPECT_EQ(bad_message::NR_BAD_ORIGIN_HEADER, kill_waiter.Wait()); |
| } |
| |
| // Tests that if the feature flag is disabled, the renderer is not killed. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitKillDisabledBrowserTest, |
| OriginHeaderPOSTMismatchesInitiator_KillDisabled) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| NavigationHeaderInterceptor interceptor(shell()->web_contents()); |
| interceptor.set_headers_to_inject("Origin: https://b.test"); |
| interceptor.Activate(); |
| |
| RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderProcessHost* rph = rfhi->GetProcess(); |
| TestNavigationObserver nav_observer(shell()->web_contents()); |
| ExecuteScriptAsync( |
| rfhi, |
| "var f = document.createElement('form'); f.action='/echoheader?origin'; " |
| "f.method='POST'; document.body.appendChild(f); f.submit();"); |
| nav_observer.Wait(); |
| |
| EXPECT_EQ(shell()->web_contents()->GetPrimaryMainFrame()->GetProcess(), rph); |
| EXPECT_TRUE(rph->IsInitializedAndNotDead()); |
| EXPECT_EQ(url::Origin::Create(start_url).Serialize(), |
| EvalJs(shell(), "document.body.innerText").ExtractString()); |
| } |
| |
| // Tests that if a renderer provides an Origin header on a cross-origin GET |
| // request, the renderer is killed. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OriginHeaderCrossOriginGETMismatchesInitiator) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| NavigationHeaderInterceptor interceptor(shell()->web_contents()); |
| interceptor.set_headers_to_inject("Origin: https://b.test"); |
| interceptor.Activate(); |
| |
| RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess()); |
| ExecuteScriptAsync(rfhi, "location = '" + |
| embedded_test_server() |
| ->GetURL("c.test", "/echoheader?origin") |
| .spec() + |
| "'"); |
| EXPECT_EQ(bad_message::NR_BAD_ORIGIN_HEADER, kill_waiter.Wait()); |
| } |
| |
| // Tests that a renderer can provide an Origin header on a POST request but |
| // the renderer is killed if the origin was incorrect. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OriginHeaderPOSTMismatchesInitiator) { |
| GURL start_url(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| NavigationHeaderInterceptor interceptor(shell()->web_contents()); |
| interceptor.set_headers_to_inject("Origin: https://b.test"); |
| interceptor.Activate(); |
| |
| RenderFrameHost* rfhi = shell()->web_contents()->GetPrimaryMainFrame(); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfhi->GetProcess()); |
| ExecuteScriptAsync( |
| rfhi, |
| "var f = document.createElement('form'); f.action='/echoheader?origin'; " |
| "f.method='POST'; document.body.appendChild(f); f.submit();"); |
| EXPECT_EQ(bad_message::NR_BAD_ORIGIN_HEADER, kill_waiter.Wait()); |
| } |
| |
| // Tests that a history navigation to a page that was originally reached via a |
| // POST-to-GET redirect does not crash/dump. |
| // |
| // Note: unlike other tests in this file, this test does not simulate a |
| // compromised renderer. Rather, the test is checking that a legitimate code |
| // path doesn't regress when we add stricter checks for a compromised renderer. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OriginHeaderInHistoryNavigation) { |
| GURL url1(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| GURL url2(embedded_test_server()->GetURL("a.test", "/title2.html")); |
| GURL redirect_url(embedded_test_server()->GetURL( |
| "a.test", "/server-redirect?" + url1.spec())); |
| |
| // Perform a browser-initiated POST request with a non-null Origin |
| // header that redirects to url1. |
| { |
| TestNavigationObserver nav_observer(shell()->web_contents()); |
| NavigationController::LoadURLParams load_params(redirect_url); |
| load_params.load_type = NavigationController::LOAD_TYPE_HTTP_POST; |
| load_params.post_data = network::ResourceRequestBody::CreateFromBytes( |
| std::vector<uint8_t>{'f', 'o', 'o'}); |
| load_params.extra_headers = "Origin: https://origin.test"; |
| shell()->web_contents()->GetController().LoadURLWithParams(load_params); |
| nav_observer.Wait(); |
| EXPECT_EQ(url1, shell()->web_contents()->GetLastCommittedURL()); |
| } |
| |
| // Navigate elsewhere so that we can navigate back. |
| EXPECT_TRUE(NavigateToURL(shell(), url2)); |
| |
| TestNavigationObserver back_nav_observer(shell()->web_contents()); |
| shell()->web_contents()->GetController().GoBack(); |
| back_nav_observer.Wait(); |
| |
| // There should be no kill on the Back navigation because browser-initiated |
| // navigations are excluded. |
| EXPECT_TRUE( |
| shell()->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive()); |
| } |
| |
| // Tests that a renderer-initiated history navigation with an incorrect Origin |
| // header does not kill the renderer. |
| // |
| // Note: unlike other tests in this file, this test does not simulate a |
| // compromised renderer. Rather, the test is checking that a legitimate code |
| // path doesn't regress when we add stricter checks for a compromised renderer. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OriginHeaderInRendererInitiatedHistoryNavigation) { |
| GURL url1(embedded_test_server()->GetURL("a.test", "/echoheader?Origin")); |
| GURL url2(embedded_test_server()->GetURL("a.test", "/title1.html")); |
| |
| // Perform an initial navigation with an Origin header. |
| // We use browser-initiated navigation to set up the entry with an Origin |
| // header. We use a header that is NOT expected for a GET navigation to ensure |
| // it would trigger a kill if it were renderer-initiated. |
| NavigationController::LoadURLParams load_params(url1); |
| load_params.extra_headers = "Origin: https://foo.test"; |
| TestNavigationObserver nav_observer1(shell()->web_contents()); |
| shell()->web_contents()->GetController().LoadURLWithParams(load_params); |
| nav_observer1.Wait(); |
| |
| // Verify that url1 shows the Origin header. |
| EXPECT_THAT(EvalJs(shell()->web_contents(), "document.body.innerText") |
| .ExtractString(), |
| HasSubstr("https://foo.test")); |
| |
| // Navigate away so we can go back. |
| EXPECT_TRUE(NavigateToURL(shell(), url2)); |
| |
| // Perform a renderer-initiated back navigation. |
| TestNavigationObserver back_nav_observer(shell()->web_contents()); |
| ExecuteScriptAsync(shell()->web_contents(), "history.back();"); |
| back_nav_observer.Wait(); |
| |
| // There should be no kill on the Back navigation because history navigations |
| // are excluded. |
| EXPECT_TRUE( |
| shell()->web_contents()->GetPrimaryMainFrame()->IsRenderFrameLive()); |
| |
| // Verify that the back navigation also sent the Origin header. |
| EXPECT_THAT(EvalJs(shell()->web_contents(), "document.body.innerText") |
| .ExtractString(), |
| HasSubstr("https://foo.test")); |
| } |
| |
| // Verify that EnterFullscreen from a cross-origin child frame without the |
| // allowfullscreen attribute is rejected with a bad message. The renderer-side |
| // Permissions Policy check should prevent this IPC, so receiving it indicates a |
| // compromised renderer. See https://crbug.com/488514078. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| EnterFullscreenDisallowedByPermissionsPolicy) { |
| 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 a cross-origin child frame WITHOUT allowfullscreen. |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| { |
| std::string js_str = base::StringPrintf( |
| "var frame = document.createElement('iframe'); " |
| "frame.src = '%s'; " |
| "document.body.appendChild(frame);", |
| child_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(main_frame, js_str)); |
| ASSERT_TRUE(WaitForLoadStop(web_contents)); |
| } |
| |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| ASSERT_FALSE(subframe->IsFeatureEnabled( |
| network::mojom::PermissionsPolicyFeature::kFullscreen)); |
| |
| // Simulate a compromised renderer sending EnterFullscreen despite the |
| // Permissions Policy denial. The browser should kill the renderer. |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| subframe->EnterFullscreen(blink::mojom::FullscreenOptions::New(), |
| base::DoNothing()); |
| EXPECT_EQ(bad_message::RFH_ENTER_FULLSCREEN_PERMISSION_DENIED, |
| kill_waiter.Wait()); |
| EXPECT_FALSE(subframe->IsRenderFrameLive()); |
| } |
| |
| // Verify that EnterFullscreen from a cross-origin child frame WITH the |
| // allowfullscreen attribute does NOT trigger a bad message. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| EnterFullscreenAllowedByPermissionsPolicy) { |
| 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 a cross-origin child frame WITH allowfullscreen. |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| { |
| std::string js_str = base::StringPrintf( |
| "var frame = document.createElement('iframe'); " |
| "frame.src = '%s'; " |
| "frame.allowFullscreen = true; " |
| "document.body.appendChild(frame);", |
| child_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(main_frame, js_str)); |
| ASSERT_TRUE(WaitForLoadStop(web_contents)); |
| } |
| |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| ASSERT_TRUE(subframe->IsFeatureEnabled( |
| network::mojom::PermissionsPolicyFeature::kFullscreen)); |
| |
| // EnterFullscreen should proceed without killing the renderer. |
| subframe->EnterFullscreen(blink::mojom::FullscreenOptions::New(), |
| base::DoNothing()); |
| EXPECT_TRUE(subframe->IsRenderFrameLive()); |
| } |
| |
| // Verify that EnterFullscreen drops the is_xr_overlay flag when no XR setup |
| // was recorded by the browser. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| EnterFullscreenIgnoresXrOverlayFlagWithoutXrSetup) { |
| 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(); |
| |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| { |
| std::string js_str = base::StringPrintf( |
| "var frame = document.createElement('iframe'); " |
| "frame.src = '%s'; " |
| "frame.allowFullscreen = true; " |
| "document.body.appendChild(frame);", |
| child_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(main_frame, js_str)); |
| ASSERT_TRUE(WaitForLoadStop(web_contents)); |
| } |
| |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| |
| base::HistogramTester histogram_tester; |
| |
| // Give subframe transient user activation. |
| subframe->UpdateUserActivationState( |
| blink::mojom::UserActivationUpdateType::kNotifyActivation, |
| blink::mojom::UserActivationNotificationType::kInteraction); |
| |
| auto options = blink::mojom::FullscreenOptions::New(); |
| options->is_xr_overlay = true; |
| |
| subframe->EnterFullscreen(std::move(options), base::DoNothing()); |
| |
| // Wait for parent frame to process fullscreen change. |
| EXPECT_TRUE(ExecJs(main_frame, |
| "new Promise(resolve => {" |
| " if (document.fullscreenElement) resolve();" |
| " else document.addEventListener('fullscreenchange', () " |
| "=> resolve(), {once: true});" |
| "});")); |
| |
| // Parent frame's iframe should match :fullscreen but NOT :xr-overlay. |
| EXPECT_EQ(true, |
| EvalJs(main_frame, |
| "document.querySelector('iframe').matches(':fullscreen')")); |
| EXPECT_EQ(false, |
| EvalJs(main_frame, |
| "document.querySelector('iframe').matches(':xr-overlay')")); |
| |
| histogram_tester.ExpectUniqueSample( |
| "XR.DOMOverlay.IsXrOverlayFullscreenValid", false, 1); |
| } |
| |
| // Verify that EnterFullscreen preserves the is_xr_overlay flag when XR setup |
| // was recorded by the browser. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| EnterFullscreenPropagatesXrOverlayFlagWithXrSetup) { |
| 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(); |
| |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| { |
| std::string js_str = base::StringPrintf( |
| "var frame = document.createElement('iframe'); " |
| "frame.src = '%s'; " |
| "frame.allowFullscreen = true; " |
| "document.body.appendChild(frame);", |
| child_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(main_frame, js_str)); |
| ASSERT_TRUE(WaitForLoadStop(web_contents)); |
| } |
| |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| |
| base::HistogramTester histogram_tester; |
| |
| // Record that an XR overlay setup occurred. |
| subframe->SetIsXrOverlaySetup(); |
| |
| auto options = blink::mojom::FullscreenOptions::New(); |
| options->is_xr_overlay = true; |
| |
| subframe->EnterFullscreen(std::move(options), base::DoNothing()); |
| |
| // Wait for parent frame to process fullscreen change. |
| EXPECT_TRUE(ExecJs(main_frame, |
| "new Promise(resolve => {" |
| " if (document.fullscreenElement) resolve();" |
| " else document.addEventListener('fullscreenchange', () " |
| "=> resolve(), {once: true});" |
| "});")); |
| |
| // Parent frame's iframe SHOULD match :xr-overlay. |
| EXPECT_EQ(true, |
| EvalJs(main_frame, |
| "document.querySelector('iframe').matches(':xr-overlay')")); |
| |
| histogram_tester.ExpectUniqueSample( |
| "XR.DOMOverlay.IsXrOverlayFullscreenValid", true, 1); |
| } |
| |
| // Regression test for browser-side validation of the allow-pointer-lock |
| // sandbox attribute. A sandboxed frame without allow-pointer-lock should not |
| // be able to acquire pointer lock via Mojo IPC. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| PointerLockDisallowedFromSandboxedFrame) { |
| 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(); |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.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)); |
| } |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| EXPECT_TRUE( |
| subframe->IsSandboxed(network::mojom::WebSandboxFlags::kPointerLock)); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| RenderWidgetHostImpl* rwh = subframe->GetRenderWidgetHost(); |
| rwh->RequestMouseLock(false, false, base::DoNothing()); |
| EXPECT_EQ(bad_message::RWH_POINTER_LOCK_FROM_SANDBOXED_FRAME, |
| kill_waiter.Wait()); |
| EXPECT_FALSE(subframe->IsRenderFrameLive()); |
| } |
| |
| // Regression test for browser-side validation of the |
| // allow-storage-access-by-user-activation sandbox attribute. A sandboxed frame |
| // without that token should not be able to request the storage-access |
| // permission by binding blink::mojom::PermissionService directly, bypassing |
| // the Blink-side check in DocumentStorageAccess::RequestStorageAccessImpl(). |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| StorageAccessDisallowedFromSandboxedFrame) { |
| 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(); |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.html")); |
| { |
| std::string js_str = base::StringPrintf( |
| "var frame = document.createElement('iframe'); " |
| "frame.sandbox = 'allow-scripts allow-same-origin'; " |
| "frame.src = '%s'; " |
| "document.body.appendChild(frame);", |
| child_url.spec().c_str()); |
| EXPECT_TRUE(ExecJs(main_frame, js_str)); |
| ASSERT_TRUE(WaitForLoadStop(web_contents)); |
| } |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| ASSERT_TRUE(subframe->IsSandboxed( |
| network::mojom::WebSandboxFlags::kStorageAccessByUserActivation)); |
| |
| // Bind PermissionService straight at the sandboxed frame, the way a |
| // compromised renderer would, skipping Blink's own gate entirely. |
| mojo::Remote<blink::mojom::PermissionService> permission_service; |
| PermissionServiceContext::GetOrCreateForCurrentDocument(subframe) |
| ->CreateService(permission_service.BindNewPipeAndPassReceiver()); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| auto descriptor = blink::mojom::PermissionDescriptor::New(); |
| descriptor->name = blink::mojom::PermissionName::STORAGE_ACCESS; |
| permission_service->RequestPermission(std::move(descriptor), |
| base::DoNothing()); |
| EXPECT_EQ(bad_message::PSI_STORAGE_ACCESS_FROM_SANDBOXED_FRAME, |
| kill_waiter.Wait()); |
| EXPECT_FALSE(subframe->IsRenderFrameLive()); |
| } |
| |
| // Tests that a compromised renderer cannot exploit a CSP-blocked subframe error |
| // page to place a srcdoc frame into a sandboxed SiteInstance for a site that it |
| // doesn't have access to. This verifies the fix for |
| // https://crbug.com/502348223. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| ErrorPagePrecursorDoesNotLeakToSandboxedSrcdoc) { |
| GURL attacker_url( |
| embedded_test_server()->GetURL("attacker.test", "/title1.html")); |
| GURL victim_url( |
| embedded_test_server()->GetURL("victim.test", "/title1.html")); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), attacker_url)); |
| RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| // Set CSP to block iframes, so we get an error page. |
| EXPECT_TRUE(ExecJs(main_frame, |
| "var meta = document.createElement('meta');" |
| "meta.httpEquiv = 'Content-Security-Policy';" |
| "meta.content = \"frame-src 'none'\";" |
| "document.head.appendChild(meta);")); |
| |
| // Create an iframe to victim.test. It will be blocked and commit an error |
| // page. |
| TestNavigationObserver nav_observer(shell()->web_contents()); |
| EXPECT_TRUE( |
| ExecJs(main_frame, JsReplace("var f = document.createElement('iframe');" |
| "f.src = $1;" |
| "document.body.appendChild(f);", |
| victim_url))); |
| nav_observer.Wait(); |
| |
| EXPECT_FALSE(nav_observer.last_navigation_succeeded()); |
| EXPECT_EQ(net::ERR_BLOCKED_BY_CSP, nav_observer.last_net_error_code()); |
| |
| RenderFrameHostImpl* error_frame = |
| main_frame->child_at(0)->current_frame_host(); |
| EXPECT_TRUE(error_frame->IsErrorDocument()); |
| if (SiteIsolationPolicy::IsErrorPageIsolationEnabled( |
| /*in_main_frame=*/false)) { |
| ASSERT_NE(error_frame->GetProcess(), main_frame->GetProcess()); |
| } else { |
| ASSERT_EQ(error_frame->GetProcess(), main_frame->GetProcess()); |
| } |
| |
| // Simulate a compromised renderer by injecting a sandboxed srcdoc inside the |
| // error page. Once we have error page isolation for subframes, attacker.test |
| // won't be able to do this step, and the precursor clearing defense that the |
| // test verifies below would not apply (since it's not needed). So it only |
| // makes sense to continue with this test when there's no subframe error page |
| // isolation. |
| if (SiteIsolationPolicy::IsErrorPageIsolationEnabled( |
| /*in_main_frame=*/false)) { |
| return; |
| } |
| TestNavigationObserver srcdoc_observer(shell()->web_contents()); |
| EXPECT_TRUE(ExecJs(error_frame, |
| "var f = document.createElement('iframe');" |
| "f.sandbox = 'allow-scripts';" |
| "f.srcdoc = 'foo';" |
| "document.body.appendChild(f);")); |
| srcdoc_observer.Wait(); |
| |
| RenderFrameHostImpl* srcdoc_frame = |
| error_frame->child_at(0)->current_frame_host(); |
| |
| // With the fix, the error page's opaque origin has no precursor. Check that |
| // the sandboxed srcdoc's SiteInstance was not derived from victim.test. |
| EXPECT_TRUE(srcdoc_frame->GetLastCommittedOrigin().opaque()); |
| EXPECT_TRUE(srcdoc_frame->GetLastCommittedOrigin() |
| .GetTupleOrPrecursorTupleIfOpaque() |
| .GetURL() |
| .is_empty()); |
| SiteInfo site_info = srcdoc_frame->GetSiteInstance()->GetSiteInfo(); |
| EXPECT_FALSE(site_info.site_url().DomainIs("victim.test")); |
| |
| // OOPSIFs require site isolation, so the srcdoc frame will be in a new |
| // sandboxed process if site isolation is enabled; otherwise, it will go into |
| // the error page's current unsandboxed process. |
| if (AreAllSitesIsolatedForTesting()) { |
| EXPECT_TRUE(site_info.IsSandboxed()); |
| EXPECT_NE(srcdoc_frame->GetProcess(), error_frame->GetProcess()); |
| } else { |
| EXPECT_FALSE(site_info.IsSandboxed()); |
| EXPECT_EQ(srcdoc_frame->GetProcess(), error_frame->GetProcess()); |
| } |
| } |
| |
| // Verify that binding GamepadMonitor is rejected when the Permissions Policy |
| // denies gamepad access. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| GamepadMonitorBlockedByPermissionsPolicy) { |
| GURL url( |
| embedded_test_server()->GetURL("/gamepad-permissions-policy-none.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| auto* frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess()); |
| |
| mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver = |
| frame->browser_interface_broker_receiver_for_testing(); |
| blink::mojom::BrowserInterfaceBroker* broker = |
| broker_receiver.internal_state()->impl(); |
| |
| mojo::Remote<device::mojom::GamepadMonitor> remote; |
| broker->GetInterface(remote.BindNewPipeAndPassReceiver()); |
| |
| EXPECT_EQ( |
| bad_message::BIBI_BIND_GAMEPAD_MONITOR_BLOCKED_BY_PERMISSIONS_POLICY, |
| kill_waiter.Wait()); |
| } |
| |
| // Verify that binding GamepadHapticsManager is rejected when the Permissions |
| // Policy denies gamepad access. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| GamepadHapticsManagerBlockedByPermissionsPolicy) { |
| GURL url( |
| embedded_test_server()->GetURL("/gamepad-permissions-policy-none.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), url)); |
| |
| auto* frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess()); |
| |
| mojo::Receiver<blink::mojom::BrowserInterfaceBroker>& broker_receiver = |
| frame->browser_interface_broker_receiver_for_testing(); |
| blink::mojom::BrowserInterfaceBroker* broker = |
| broker_receiver.internal_state()->impl(); |
| |
| mojo::Remote<device::mojom::GamepadHapticsManager> remote; |
| broker->GetInterface(remote.BindNewPipeAndPassReceiver()); |
| |
| EXPECT_EQ(bad_message:: |
| BIBI_BIND_GAMEPAD_HAPTICS_MANAGER_BLOCKED_BY_PERMISSIONS_POLICY, |
| kill_waiter.Wait()); |
| } |
| |
| namespace { |
| |
| // Helper class to wait for ReadyToCommitNavigation for the next navigation that |
| // results in an error page, and then to wait for a renderer kill for the |
| // process that will commit that error page. |
| class ErrorPageKillWaiter : public WebContentsObserver { |
| public: |
| explicit ErrorPageKillWaiter(WebContents* web_contents) |
| : WebContentsObserver(web_contents) {} |
| |
| // WebContentsObserver: |
| void ReadyToCommitNavigation(NavigationHandle* navigation_handle) override { |
| if (NavigationRequest::From(navigation_handle)->DidEncounterError()) { |
| kill_waiter_ = std::make_unique<RenderProcessHostBadIpcMessageWaiter>( |
| navigation_handle->GetRenderFrameHost()->GetProcess()); |
| run_loop_.Quit(); |
| } |
| } |
| |
| void DidFinishNavigation(NavigationHandle* navigation_handle) override { |
| if (!kill_waiter_) { |
| run_loop_.Quit(); |
| } |
| } |
| |
| std::optional<bad_message::BadMessageReason> Wait() { |
| run_loop_.Run(); |
| if (!kill_waiter_) { |
| return std::nullopt; |
| } |
| return kill_waiter_->Wait(); |
| } |
| |
| private: |
| std::unique_ptr<RenderProcessHostBadIpcMessageWaiter> kill_waiter_; |
| base::RunLoop run_loop_; |
| }; |
| |
| } // namespace |
| |
| // Tests that a compromised renderer cannot lie about the URL when committing a |
| // CSP-blocked subframe error page. Such error pages are allowed to commit a URL |
| // that doesn't match the process lock, but the URL must still match the URL |
| // that the browser asked the renderer to commit. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| BlockedSubframeErrorPageDidCommitInvalidURL) { |
| GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL blocked_url(embedded_test_server()->GetURL("b.com", "/title1.html")); |
| GURL spoofed_url(embedded_test_server()->GetURL("c.com", "/title2.html")); |
| |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| RenderFrameHostImpl* main_frame = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| // Add a CSP that blocks all subframe navigations, so the navigation below |
| // commits an error page. |
| EXPECT_TRUE(ExecJs(main_frame, |
| "var meta = document.createElement('meta');" |
| "meta.httpEquiv = 'Content-Security-Policy';" |
| "meta.content = \"frame-src 'none'\";" |
| "document.head.appendChild(meta);")); |
| |
| // Simulate a compromised renderer that lies about the committed URL. |
| DidCommitUrlReplacer url_replacer(shell()->web_contents(), spoofed_url); |
| |
| // Set up an observer to capture the error page process right before it |
| // commits. Note that we can't assume that it will be the same as the main |
| // frame's process due to subframe error page isolation. |
| ErrorPageKillWaiter kill_waiter(shell()->web_contents()); |
| |
| // Create an iframe that will be blocked by CSP and commit an error page. The |
| // browser process should detect the URL mismatch and terminate the renderer. |
| // We use ExecuteScriptAsync because the renderer might be terminated before |
| // ExecJs returns. |
| ExecuteScriptAsync(main_frame, |
| JsReplace("var f = document.createElement('iframe');" |
| "f.src = $1;" |
| "document.body.appendChild(f);", |
| blocked_url)); |
| EXPECT_EQ(bad_message::RFH_ERROR_PAGE_URL_MISMATCH, kill_waiter.Wait()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OrientationLockFromSandboxedFrameIsRecorded) { |
| base::HistogramTester histogram_tester; |
| 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(); |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.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)); |
| } |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| ASSERT_TRUE( |
| subframe->IsSandboxed(network::mojom::WebSandboxFlags::kOrientationLock)); |
| |
| ScreenOrientationProvider* provider = |
| web_contents->GetScreenOrientationProviderForTesting(); |
| provider->SetCurrentTargetFrameForTesting(subframe); |
| provider->LockOrientation( |
| device::mojom::ScreenOrientationLockType::LANDSCAPE_PRIMARY, |
| base::DoNothing()); |
| provider->SetCurrentTargetFrameForTesting(nullptr); |
| |
| EXPECT_TRUE(subframe->IsRenderFrameLive()); |
| histogram_tester.ExpectUniqueSample( |
| "Security.ScreenOrientation.LockRequestIsSandboxed", true, 1); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| FormSubmissionDisallowedFromSandboxedFrame) { |
| 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(); |
| GURL child_url(embedded_test_server()->GetURL("b.com", "/title2.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)); |
| } |
| RenderFrameHostImpl* subframe = root->child_at(0)->current_frame_host(); |
| ASSERT_TRUE(subframe->IsSandboxed(network::mojom::WebSandboxFlags::kForms)); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(subframe->GetProcess()); |
| GURL next_url(embedded_test_server()->GetURL("b.com", "/empty.html")); |
| // This navigation should be rejected because `is_form_submission` in |
| // `begin_params` is set while the initiating frame is sandboxed with forms. |
| blink::mojom::CommonNavigationParamsPtr common_params = |
| blink::mojom::CommonNavigationParams::New( |
| next_url, subframe->GetLastCommittedOrigin(), |
| std::nullopt /* initiator_base_url */, 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() /* actual_navigation_start */, |
| 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( |
| subframe->GetFrameToken() /* initiator_frame_token */, |
| subframe->current_initiator_state_token(), |
| subframe->GetDocumentToken(), std::string() /* headers */, |
| net::LOAD_NORMAL, false /* skip_service_worker */, |
| blink::mojom::RequestContextType::LOCATION, |
| blink::mojom::MixedContentContextType::kBlockable, |
| true /* is_form_submission: checked above */, |
| 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 */, |
| base::TimeTicks() /* renderer_before_unload_start */, |
| base::TimeTicks() /* renderer_before_unload_end */, |
| base::TimeTicks() /* before_unload_dialog_opened */, |
| base::TimeTicks() /* before_unload_dialog_closed */, |
| false /* started_with_transient_activation */, |
| false /* started_by_ad */, false /* is_container_initiated */, |
| false /* has_rel_opener */, |
| std::nullopt /* script_tool_invocation_id */, |
| /*script_injector_host=*/""); |
| mojo::PendingAssociatedRemote<mojom::NavigationClient> navigation_client; |
| auto navigation_client_receiver = |
| navigation_client.InitWithNewEndpointAndPassReceiver(); |
| subframe->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(), |
| mojo::NullReceiver(), mojo::NullReceiver()); |
| EXPECT_EQ(bad_message::RFH_FORM_SUBMISSION_FROM_SANDBOXED_FRAME, |
| kill_waiter.Wait()); |
| EXPECT_FALSE(subframe->IsRenderFrameLive()); |
| } |
| |
| // Ensure that the renderer is terminated if it sends a DownloadURL IPC with a |
| // referrer URL that the renderer's process is not allowed to host. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DownloadURLInvalidReferrer) { |
| // 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", "/title1.html")); |
| EXPECT_TRUE(NavigateToURL(shell(), main_url)); |
| |
| WebContentsImpl* web_contents = |
| static_cast<WebContentsImpl*>(shell()->web_contents()); |
| RenderFrameHostImpl* main_frame = |
| web_contents->GetPrimaryFrameTree().root()->current_frame_host(); |
| |
| // Simulate that the renderer sends a DownloadURL IPC claiming that the |
| // download was referred by a page on a different site. |
| auto params = blink::mojom::DownloadURLParams::New(); |
| params->url = embedded_test_server()->GetURL("a.com", "/title2.html"); |
| params->initiator_origin = main_frame->GetLastCommittedOrigin(); |
| params->referrer = blink::mojom::Referrer::New(); |
| params->referrer->url = |
| embedded_test_server()->GetURL("b.com", "/title1.html"); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess()); |
| main_frame->DownloadURL(std::move(params)); |
| EXPECT_EQ(bad_message::RFH_DOWNLOAD_URL_INVALID_REFERRER, kill_waiter.Wait()); |
| EXPECT_FALSE(main_frame->IsRenderFrameLive()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends an OpenURL |
| // IPC with an invalid initiator_state_token. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OpenURL_InvalidInitiatorStateToken) { |
| GURL start_url(embedded_test_server()->GetURL("/title1.html")); |
| GURL target_url(embedded_test_server()->GetURL("/title2.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| auto params = CreateOpenURLParams(target_url, rfh); |
| params->initiator_state_token = blink::InitiatorStateToken(); |
| |
| static_cast<mojom::FrameHost*>(rfh)->OpenURL(std::move(params)); |
| |
| EXPECT_EQ(start_url, rfh->GetLastCommittedURL()); |
| EXPECT_EQ(bad_message::RFHI_OPEN_URL_INVALID_INITIATOR_TOKENS, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends an OpenURL |
| // IPC with an invalid initiator_document_token. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OpenURL_InvalidInitiatorDocumentToken) { |
| GURL start_url(embedded_test_server()->GetURL("/title1.html")); |
| GURL target_url(embedded_test_server()->GetURL("/title2.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| auto params = CreateOpenURLParams(target_url, rfh); |
| params->initiator_document_token = blink::DocumentToken(); |
| |
| static_cast<mojom::FrameHost*>(rfh)->OpenURL(std::move(params)); |
| |
| EXPECT_EQ(start_url, rfh->GetLastCommittedURL()); |
| EXPECT_EQ(bad_message::RFHI_OPEN_URL_INVALID_INITIATOR_TOKENS, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends an OpenURL |
| // IPC with initiator tokens coming from a RenderFrameHost in another page |
| // hosted in a different process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OpenURL_InvalidInitiatorProcess) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL target_url(embedded_test_server()->GetURL("a.com", "/title2.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| Shell* other_shell = CreateBrowser(); |
| GURL cross_origin_url( |
| embedded_test_server()->GetURL("b.com", "/title1.html")); |
| ASSERT_TRUE(NavigateToURL(other_shell, cross_origin_url)); |
| RenderFrameHostImpl* cross_origin_rfh = static_cast<RenderFrameHostImpl*>( |
| other_shell->web_contents()->GetPrimaryMainFrame()); |
| ASSERT_NE(rfh->GetProcess(), cross_origin_rfh->GetProcess()); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| auto params = CreateOpenURLParams(target_url, cross_origin_rfh); |
| |
| static_cast<mojom::FrameHost*>(rfh)->OpenURL(std::move(params)); |
| |
| EXPECT_EQ(start_url, rfh->GetLastCommittedURL()); |
| EXPECT_EQ(bad_message::RFHI_OPEN_URL_INVALID_INITIATOR_PROCESS, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends an OpenURL |
| // IPC to a RenderFrameProxyHost with an invalid initiator_state_token. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OpenURLFromRemoteFrame_InvalidInitiatorStateToken) { |
| if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) { |
| // Isolate "a.com" so that we are guaranteed to create a specific |
| // SiteInstance for it and not use the default one. |
| IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(), |
| {"a.com"}); |
| } |
| GURL start_url = embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)"); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| FrameTreeNode* child = root->child_at(0); |
| SiteInstanceImpl* a_com_instance = |
| root->current_frame_host()->GetSiteInstance(); |
| |
| RenderFrameProxyHost* proxy = |
| child->current_frame_host() |
| ->browsing_context_state() |
| ->GetRenderFrameProxyHost(a_com_instance->group()); |
| ASSERT_TRUE(proxy); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(proxy->GetProcess()); |
| |
| auto params = CreateOpenURLParams( |
| embedded_test_server()->GetURL("a.com", "/title1.html"), |
| root->current_frame_host()); |
| params->initiator_state_token = blink::InitiatorStateToken(); |
| |
| static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL( |
| std::move(params)); |
| |
| EXPECT_EQ(bad_message::RFPH_OPEN_URL_INVALID_INITIATOR_TOKENS, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends an OpenURL |
| // IPC to a RenderFrameProxyHost with an invalid initiator_document_token. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OpenURLFromRemoteFrame_InvalidInitiatorDocumentToken) { |
| if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) { |
| // Isolate "a.com" so that we are guaranteed to create a specific |
| // SiteInstance for it and not use the default one. |
| IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(), |
| {"a.com"}); |
| } |
| GURL start_url = embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)"); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| FrameTreeNode* child = root->child_at(0); |
| SiteInstanceImpl* a_com_instance = |
| root->current_frame_host()->GetSiteInstance(); |
| |
| RenderFrameProxyHost* proxy = |
| child->current_frame_host() |
| ->browsing_context_state() |
| ->GetRenderFrameProxyHost(a_com_instance->group()); |
| ASSERT_TRUE(proxy); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(proxy->GetProcess()); |
| |
| auto params = CreateOpenURLParams( |
| embedded_test_server()->GetURL("a.com", "/title1.html"), |
| root->current_frame_host()); |
| params->initiator_document_token = blink::DocumentToken(); |
| |
| static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL( |
| std::move(params)); |
| |
| EXPECT_EQ(bad_message::RFPH_OPEN_URL_INVALID_INITIATOR_TOKENS, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends an OpenURL |
| // IPC to a RenderFrameProxyHost with initiator tokens coming from a |
| // RenderFrameHost in another page hosted in a different process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| OpenURLFromRemoteFrame_InvalidInitiatorProcess) { |
| if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) { |
| // Isolate "a.com" and "b.com" so that we are guaranteed to create a |
| // specific SiteInstances for them and not use the default one. |
| IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(), |
| {"a.com", "b.com"}); |
| } |
| GURL start_url = embedded_test_server()->GetURL( |
| "a.com", "/cross_site_iframe_factory.html?a(b)"); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents()) |
| ->GetPrimaryFrameTree() |
| .root(); |
| FrameTreeNode* child = root->child_at(0); |
| SiteInstanceImpl* a_com_instance = |
| root->current_frame_host()->GetSiteInstance(); |
| |
| RenderFrameProxyHost* proxy = |
| child->current_frame_host() |
| ->browsing_context_state() |
| ->GetRenderFrameProxyHost(a_com_instance->group()); |
| ASSERT_TRUE(proxy); |
| |
| Shell* other_shell = CreateBrowser(); |
| GURL cross_origin_url( |
| embedded_test_server()->GetURL("b.com", "/title1.html")); |
| ASSERT_TRUE(NavigateToURL(other_shell, cross_origin_url)); |
| RenderFrameHostImpl* cross_origin_rfh = static_cast<RenderFrameHostImpl*>( |
| other_shell->web_contents()->GetPrimaryMainFrame()); |
| ASSERT_NE(proxy->GetProcess(), cross_origin_rfh->GetProcess()); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(proxy->GetProcess()); |
| |
| auto params = CreateOpenURLParams( |
| embedded_test_server()->GetURL("a.com", "/title1.html"), |
| cross_origin_rfh); |
| |
| static_cast<blink::mojom::RemoteFrameHost*>(proxy)->OpenURL( |
| std::move(params)); |
| |
| EXPECT_EQ(bad_message::RFPH_OPEN_URL_INVALID_INITIATOR_PROCESS, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends a |
| // BeginNavigation IPC with an invalid initiator_state_token. |
| // TODO(crbug.com/564465304): Re-enable checking for the correct |
| // InitiatorStateToken in BeginNavigation once we understand why this is |
| // hit in the wild. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| DISABLED_BeginNavigation_InvalidInitiatorStateToken) { |
| GURL start_url(embedded_test_server()->GetURL("/title1.html")); |
| GURL dest_url(embedded_test_server()->GetURL("/title2.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| blink::mojom::CommonNavigationParamsPtr common_params = |
| blink::mojom::CommonNavigationParams::New( |
| dest_url, url::Origin::Create(dest_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 */, |
| GURL() /* base_url_for_data_url */, |
| base::TimeTicks::Now() /* actual_navigation_start */, |
| 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 */, |
| blink::InitiatorStateToken() /* initiator_state_token */, |
| rfh->GetDocumentToken(), 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 */, |
| base::TimeTicks() /* renderer_before_unload_start */, |
| base::TimeTicks() /* renderer_before_unload_end */, |
| base::TimeTicks() /* before_unload_dialog_opened */, |
| base::TimeTicks() /* before_unload_dialog_closed */, |
| false /* started_with_transient_activation */, |
| false /* started_by_ad */, false /* is_container_initiated */, |
| false /* has_rel_opener */, |
| std::nullopt /* script_tool_invocation_id */, |
| /*script_injector_host=*/""); |
| |
| 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(), |
| mojo::NullReceiver(), mojo::NullReceiver()); |
| |
| EXPECT_EQ(start_url, rfh->GetLastCommittedURL()); |
| EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_INVALID_INITIATOR_TOKENS, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends a |
| // BeginNavigation IPC with an invalid initiator_document_token. |
| // TODO(crbug.com/564465304): Re-enable checking for the correct |
| // initiator document token in BeginNavigation once we understand why this is |
| // hit in the wild. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| DISABLED_BeginNavigation_InvalidInitiatorDocumentToken) { |
| GURL start_url(embedded_test_server()->GetURL("/title1.html")); |
| GURL dest_url(embedded_test_server()->GetURL("/title2.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| blink::mojom::CommonNavigationParamsPtr common_params = |
| blink::mojom::CommonNavigationParams::New( |
| dest_url, url::Origin::Create(dest_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 */, |
| GURL() /* base_url_for_data_url */, |
| base::TimeTicks::Now() /* actual_navigation_start */, |
| 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 */, |
| rfh->current_initiator_state_token(), |
| blink::DocumentToken() /* initiator_document_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 */, |
| base::TimeTicks() /* renderer_before_unload_start */, |
| base::TimeTicks() /* renderer_before_unload_end */, |
| base::TimeTicks() /* before_unload_dialog_opened */, |
| base::TimeTicks() /* before_unload_dialog_closed */, |
| false /* started_with_transient_activation */, |
| false /* started_by_ad */, false /* is_container_initiated */, |
| false /* has_rel_opener */, |
| std::nullopt /* script_tool_invocation_id */, |
| /*script_injector_host=*/""); |
| |
| 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(), |
| mojo::NullReceiver(), mojo::NullReceiver()); |
| |
| EXPECT_EQ(start_url, rfh->GetLastCommittedURL()); |
| EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_INVALID_INITIATOR_TOKENS, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends a |
| // BeginNavigation IPC with initiator tokens coming from a RenderFrameHost |
| // in another page hosted in a different process. |
| IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, |
| BeginNavigation_InvalidInitiatorProcess) { |
| GURL start_url(embedded_test_server()->GetURL("a.com", "/title1.html")); |
| GURL dest_url(embedded_test_server()->GetURL("a.com", "/title2.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| |
| Shell* other_shell = CreateBrowser(); |
| GURL cross_origin_url( |
| embedded_test_server()->GetURL("b.com", "/title1.html")); |
| ASSERT_TRUE(NavigateToURL(other_shell, cross_origin_url)); |
| RenderFrameHostImpl* cross_origin_rfh = static_cast<RenderFrameHostImpl*>( |
| other_shell->web_contents()->GetPrimaryMainFrame()); |
| ASSERT_NE(rfh->GetProcess(), cross_origin_rfh->GetProcess()); |
| |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| blink::mojom::CommonNavigationParamsPtr common_params = |
| blink::mojom::CommonNavigationParams::New( |
| dest_url, url::Origin::Create(dest_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 */, |
| GURL() /* base_url_for_data_url */, |
| base::TimeTicks::Now() /* actual_navigation_start */, |
| 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 */, |
| cross_origin_rfh->current_initiator_state_token(), |
| cross_origin_rfh->GetDocumentToken(), 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 */, |
| base::TimeTicks() /* renderer_before_unload_start */, |
| base::TimeTicks() /* renderer_before_unload_end */, |
| base::TimeTicks() /* before_unload_dialog_opened */, |
| base::TimeTicks() /* before_unload_dialog_closed */, |
| false /* started_with_transient_activation */, |
| false /* started_by_ad */, false /* is_container_initiated */, |
| false /* has_rel_opener */, |
| std::nullopt /* script_tool_invocation_id */, |
| /*script_injector_host=*/""); |
| |
| 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(), |
| mojo::NullReceiver(), mojo::NullReceiver()); |
| |
| EXPECT_EQ(start_url, rfh->GetLastCommittedURL()); |
| EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_INVALID_INITIATOR_PROCESS, |
| kill_waiter.Wait()); |
| } |
| |
| // Test that a compromised renderer process is killed if it sends a |
| // PolicyContainerHost message with an initiator_state_token that is already |
| // registered. |
| IN_PROC_BROWSER_TEST_F( |
| SecurityExploitBrowserTest, |
| UpdateInitiatorNavigationState_DuplicateInitiatorStateToken) { |
| GURL start_url(embedded_test_server()->GetURL("/title1.html")); |
| ASSERT_TRUE(NavigateToURL(shell(), start_url)); |
| |
| RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>( |
| shell()->web_contents()->GetPrimaryMainFrame()); |
| RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess()); |
| |
| auto current_token = static_cast<InitiatorNavigationStateImpl*>( |
| rfh->current_navigation_state().get()) |
| ->initiator_state_token(); |
| |
| static_cast<blink::mojom::PolicyContainerHost*>(rfh->policy_container_host()) |
| ->SetReferrerPolicy(network::mojom::ReferrerPolicy::kNever, |
| current_token); |
| |
| EXPECT_EQ(bad_message::RFHI_INVALID_INITIATOR_STATE_TOKEN_UPDATE, |
| kill_waiter.Wait()); |
| } |
| |
| } // namespace content |