blob: 091c9b849c8bf5bef456370b7543a080807ee72b [file]
// 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(&params_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