blob: 31fb44db7e84b265f934a5dc019bb48da92e80ad [file] [log] [blame]
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stdint.h>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/bad_message.h"
#include "content/browser/dom_storage/dom_storage_context_wrapper.h"
#include "content/browser/dom_storage/session_storage_namespace_impl.h"
#include "content/browser/frame_host/navigator.h"
#include "content/browser/frame_host/render_frame_host_impl.h"
#include "content/browser/frame_host/render_frame_proxy_host.h"
#include "content/browser/renderer_host/render_process_host_impl.h"
#include "content/browser/renderer_host/render_view_host_factory.h"
#include "content/browser/renderer_host/render_view_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/frame.mojom.h"
#include "content/common/frame_messages.h"
#include "content/common/render_message_filter.mojom.h"
#include "content/common/view_messages.h"
#include "content/public/browser/blob_handle.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/file_select_listener.h"
#include "content/public/browser/interstitial_page.h"
#include "content/public/browser/interstitial_page_delegate.h"
#include "content/public/browser/resource_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/navigation_policy.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/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "content/test/did_commit_navigation_interceptor.h"
#include "content/test/frame_host_interceptor.h"
#include "content/test/mock_widget_impl.h"
#include "content/test/test_content_browser_client.h"
#include "ipc/ipc_security_test_util.h"
#include "mojo/core/embedder/embedder.h"
#include "mojo/public/cpp/bindings/strong_associated_binding.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/base/network_isolation_key.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/url_request/url_request_slow_download_job.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/test/test_url_loader_client.h"
#include "storage/browser/blob/blob_registry_impl.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/blob/blob_utils.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/appcache/appcache.mojom.h"
#include "third_party/blink/public/mojom/blob/blob_url_store.mojom-test-utils.h"
#include "third_party/blink/public/web/web_triggering_event_info.h"
using IPC::IpcSecurityTestUtil;
namespace content {
namespace {
// This request id is used by tests that call CreateLoaderAndStart. The id is
// sufficiently large that it doesn't collide with ids used by previous
// navigation requests.
const int kRequestIdNotPreviouslyUsed = 10000;
// This is a helper function for the tests which attempt to create a
// duplicate RenderViewHost or RenderWidgetHost. It tries to create two objects
// with the same process and routing ids, which causes a collision.
// It creates a couple of windows in process 1, which causes a few routing ids
// to be allocated. Then a cross-process navigation is initiated, which causes a
// new process 2 to be created and have a pending RenderViewHost for it. The
// routing id of the RenderViewHost which is target for a duplicate is set
// into |target_routing_id| and the pending RenderFrameHost which is used for
// the attempt is the return value.
RenderFrameHostImpl* PrepareToDuplicateHosts(Shell* shell,
net::EmbeddedTestServer* server,
int* target_routing_id) {
GURL foo("http://foo.com/simple_page.html");
if (AreDefaultSiteInstancesEnabled()) {
// Isolate "bar.com" so we are guaranteed to get a different process
// for navigations to this origin.
IsolateOriginsForTesting(server, shell->web_contents(), {"bar.com"});
}
// Start off with initial navigation, so we get the first process allocated.
NavigateToURL(shell, foo);
EXPECT_EQ(base::ASCIIToUTF16("OK"), shell->web_contents()->GetTitle());
// Open another window, so we generate some more routing ids.
ShellAddedObserver shell2_observer;
EXPECT_TRUE(ExecuteScript(shell, "window.open(document.URL + '#2');"));
Shell* shell2 = shell2_observer.GetShell();
// The new window must be in the same process, but have a new routing id.
EXPECT_EQ(shell->web_contents()->GetMainFrame()->GetProcess()->GetID(),
shell2->web_contents()->GetMainFrame()->GetProcess()->GetID());
*target_routing_id =
shell2->web_contents()->GetRenderViewHost()->GetRoutingID();
EXPECT_NE(*target_routing_id,
shell->web_contents()->GetRenderViewHost()->GetRoutingID());
// Now, simulate a link click coming from the renderer.
GURL extension_url("http://bar.com/simple_page.html");
WebContentsImpl* wc = static_cast<WebContentsImpl*>(shell->web_contents());
wc->GetFrameTree()->root()->navigator()->RequestOpenURL(
wc->GetFrameTree()->root()->current_frame_host(), extension_url,
url::Origin::Create(foo), false, nullptr, std::string(), Referrer(),
WindowOpenDisposition::CURRENT_TAB, false, true,
blink::WebTriggeringEventInfo::kFromTrustedEvent, std::string(),
nullptr /* blob_url_loader_factory */);
// Since the navigation above requires a cross-process swap, there will be a
// speculative/pending RenderFrameHost. Ensure it exists and is in a different
// process than the initial page.
RenderFrameHostImpl* next_rfh =
wc->GetRenderManagerForTesting()->speculative_frame_host();
EXPECT_TRUE(next_rfh);
EXPECT_NE(shell->web_contents()->GetMainFrame()->GetProcess()->GetID(),
next_rfh->GetProcess()->GetID());
return next_rfh;
}
network::ResourceRequest CreateXHRRequest(const char* url) {
network::ResourceRequest request;
request.method = "GET";
request.url = GURL(url);
request.referrer_policy = Referrer::GetDefaultReferrerPolicy();
request.request_initiator = url::Origin();
request.load_flags = 0;
request.plugin_child_id = -1;
request.resource_type = static_cast<int>(ResourceType::kXhr);
request.appcache_host_id = base::nullopt;
request.should_reset_appcache = false;
request.is_main_frame = true;
request.transition_type = ui::PAGE_TRANSITION_LINK;
request.allow_download = true;
return request;
}
std::unique_ptr<content::BlobHandle> CreateMemoryBackedBlob(
BrowserContext* browser_context,
const std::string& contents,
const std::string& content_type) {
std::unique_ptr<content::BlobHandle> result;
base::RunLoop loop;
BrowserContext::CreateMemoryBackedBlob(
browser_context, contents.c_str(), contents.length(), content_type,
base::BindOnce(
[](std::unique_ptr<content::BlobHandle>* out_blob,
base::OnceClosure done,
std::unique_ptr<content::BlobHandle> blob) {
*out_blob = std::move(blob);
std::move(done).Run();
},
&result, loop.QuitClosure()));
loop.Run();
EXPECT_TRUE(result);
return result;
}
// Helper class to interpose on Blob URL registrations, replacing the URL
// contained in incoming registration requests with the specified URL.
class BlobURLStoreInterceptor
: public blink::mojom::BlobURLStoreInterceptorForTesting {
public:
static void Intercept(
GURL target_url,
mojo::StrongAssociatedBindingPtr<blink::mojom::BlobURLStore> binding) {
auto interceptor =
base::WrapUnique(new BlobURLStoreInterceptor(target_url));
auto* raw_interceptor = interceptor.get();
auto impl = binding->SwapImplForTesting(std::move(interceptor));
raw_interceptor->url_store_ = std::move(impl);
}
blink::mojom::BlobURLStore* GetForwardingInterface() override {
return url_store_.get();
}
void Register(blink::mojom::BlobPtr blob,
const GURL& url,
RegisterCallback callback) override {
GetForwardingInterface()->Register(std::move(blob), target_url_,
std::move(callback));
}
private:
explicit BlobURLStoreInterceptor(GURL target_url) : target_url_(target_url) {}
std::unique_ptr<blink::mojom::BlobURLStore> url_store_;
GURL target_url_;
};
// Constructs a WebContentsDelegate that mocks a file dialog.
// Unlike content::FileChooserDelegate, this class doesn't make a response in
// RunFileChooser(), and a user needs to call Choose().
class DelayedFileChooserDelegate : public WebContentsDelegate {
public:
void Choose(const base::FilePath& file) {
auto file_info = blink::mojom::FileChooserFileInfo::NewNativeFile(
blink::mojom::NativeFileInfo::New(file, base::string16()));
std::vector<blink::mojom::FileChooserFileInfoPtr> files;
files.push_back(std::move(file_info));
listener_->FileSelected(std::move(files), base::FilePath(),
blink::mojom::FileChooserParams::Mode::kOpen);
listener_.reset();
}
// WebContentsDelegate overrides
void RunFileChooser(RenderFrameHost* render_frame_host,
std::unique_ptr<FileSelectListener> listener,
const blink::mojom::FileChooserParams& params) override {
listener_ = std::move(listener);
}
void EnumerateDirectory(WebContents* web_contents,
std::unique_ptr<FileSelectListener> listener,
const base::FilePath& directory_path) override {
listener->FileSelectionCanceled();
}
private:
std::unique_ptr<FileSelectListener> listener_;
};
void FileChooserCallback(base::RunLoop* run_loop,
blink::mojom::FileChooserResultPtr result) {
run_loop->Quit();
}
} // namespace
// The goal of these tests will be to "simulate" exploited renderer processes,
// which can send arbitrary IPC messages and confuse browser process internal
// state, leading to security bugs. We are trying to verify that the browser
// doesn't perform any dangerous operations in such cases.
class SecurityExploitBrowserTest : public ContentBrowserTest {
public:
SecurityExploitBrowserTest() {}
void SetUpCommandLine(base::CommandLine* command_line) override {
// EmbeddedTestServer::InitializeAndListen() initializes its |base_url_|
// which is required below. This cannot invoke Start() however as that kicks
// off the "EmbeddedTestServer IO Thread" which then races with
// initialization in ContentBrowserTest::SetUp(), http://crbug.com/674545.
ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
// Add a host resolver rule to map all outgoing requests to the test server.
// This allows us to use "real" hostnames in URLs, which we can use to
// create arbitrary SiteInstances.
command_line->AppendSwitchASCII(
network::switches::kHostResolverRules,
"MAP * " +
net::HostPortPair::FromURL(embedded_test_server()->base_url())
.ToString() +
",EXCLUDE localhost");
}
void SetUpOnMainThread() override {
// Complete the manual Start() after ContentBrowserTest's own
// initialization, ref. comment on InitializeAndListen() above.
embedded_test_server()->StartAcceptingConnections();
base::PostTaskWithTraits(
FROM_HERE, {BrowserThread::IO},
base::BindOnce(&net::URLRequestSlowDownloadJob::AddUrlHandler));
}
static void CreateLoaderAndStart(
RenderFrameHost* frame,
int route_id,
int request_id,
const network::ResourceRequest& resource_request) {
network::mojom::URLLoaderPtr loader;
network::TestURLLoaderClient client;
CreateLoaderAndStart(frame, mojo::MakeRequest(&loader), route_id,
request_id, resource_request,
client.CreateInterfacePtr().PassInterface());
}
static void CreateLoaderAndStart(
RenderFrameHost* frame,
network::mojom::URLLoaderRequest request,
int route_id,
int request_id,
const network::ResourceRequest& resource_request,
network::mojom::URLLoaderClientPtrInfo client) {
network::mojom::URLLoaderFactoryPtr factory;
frame->GetProcess()->CreateURLLoaderFactory(
frame->GetLastCommittedOrigin(),
network::mojom::CrossOriginEmbedderPolicy::kNone,
nullptr /* preferences */, net::NetworkIsolationKey(),
nullptr /* header_client */, mojo::MakeRequest(&factory));
factory->CreateLoaderAndStart(
std::move(request), route_id, request_id,
network::mojom::kURLLoadOptionNone, resource_request,
network::mojom::URLLoaderClientPtr(std::move(client)),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
}
void TryCreateDuplicateRequestIds(Shell* shell, bool block_loaders) {
NavigateToURL(shell, GURL("http://foo.com/simple_page.html"));
RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
shell->web_contents()->GetMainFrame());
if (block_loaders) {
// Test the case where loaders are placed into blocked_loaders_map_.
rfh->BlockRequestsForFrame();
}
// URLRequestSlowDownloadJob waits for another request to kFinishDownloadUrl
// to finish all pending requests. It is never sent, so the following URL
// blocks indefinitely, which is good because the request stays alive and
// the test can try to reuse the request id without a race.
const char* blocking_url = net::URLRequestSlowDownloadJob::kUnknownSizeUrl;
network::ResourceRequest request(CreateXHRRequest(blocking_url));
// Use the same request id twice.
RenderProcessHostKillWaiter kill_waiter(rfh->GetProcess());
// We need to keep loader and client to keep the requests alive.
network::mojom::URLLoaderPtr loader1, loader2;
network::TestURLLoaderClient client1, client2;
CreateLoaderAndStart(rfh, mojo::MakeRequest(&loader1), rfh->GetRoutingID(),
kRequestIdNotPreviouslyUsed, request,
client1.CreateInterfacePtr().PassInterface());
CreateLoaderAndStart(rfh, mojo::MakeRequest(&loader2), rfh->GetRoutingID(),
kRequestIdNotPreviouslyUsed, request,
client2.CreateInterfacePtr().PassInterface());
EXPECT_EQ(bad_message::RDH_INVALID_REQUEST_ID, kill_waiter.Wait());
}
protected:
// Tests that a given file path sent in a FrameHostMsg_RunFileChooser will
// cause renderer to be killed.
void TestFileChooserWithPath(const base::FilePath& path);
void IsolateOrigin(const std::string& hostname) {
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
{hostname});
}
};
void SecurityExploitBrowserTest::TestFileChooserWithPath(
const base::FilePath& path) {
GURL foo("http://foo.com/simple_page.html");
NavigateToURL(shell(), foo);
EXPECT_EQ(base::ASCIIToUTF16("OK"), shell()->web_contents()->GetTitle());
RenderFrameHost* compromised_renderer =
shell()->web_contents()->GetMainFrame();
blink::mojom::FileChooserParamsPtr params =
blink::mojom::FileChooserParams::New();
params->default_file_name = path;
mojo::test::BadMessageObserver bad_message_observer;
blink::mojom::FileChooserPtr factory =
static_cast<RenderFrameHostImpl*>(compromised_renderer)
->BindFileChooserForTesting();
factory->OpenFileChooser(
std::move(params), blink::mojom::FileChooser::OpenFileChooserCallback());
factory.FlushForTesting();
EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
::testing::StartsWith("FileChooser: The default file name"));
}
// Ensure that we kill the renderer process if we try to give it WebUI
// properties and it doesn't have enabled WebUI bindings.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, SetWebUIProperty) {
GURL foo("http://foo.com/simple_page.html");
NavigateToURL(shell(), foo);
EXPECT_EQ(base::ASCIIToUTF16("OK"), shell()->web_contents()->GetTitle());
EXPECT_EQ(0, shell()->web_contents()->GetMainFrame()->GetEnabledBindings());
RenderViewHost* compromised_renderer =
shell()->web_contents()->GetRenderViewHost();
RenderProcessHostKillWaiter kill_waiter(compromised_renderer->GetProcess());
compromised_renderer->SetWebUIProperty("toolkit", "views");
EXPECT_EQ(bad_message::RVH_WEB_UI_BINDINGS_MISMATCH, kill_waiter.Wait());
}
// This is a test for crbug.com/312016 attempting to create duplicate
// RenderViewHosts. SetupForDuplicateHosts sets up this test case and leaves
// it in a state with pending RenderViewHost. Before the commit of the new
// pending RenderViewHost, this test case creates a new window through the new
// process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
AttemptDuplicateRenderViewHost) {
int32_t duplicate_routing_id = MSG_ROUTING_NONE;
RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts(
shell(), embedded_test_server(), &duplicate_routing_id);
EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id);
mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
params->target_url = GURL("about:blank");
pending_rfh->CreateNewWindow(
std::move(params), base::BindOnce([](mojom::CreateNewWindowStatus,
mojom::CreateNewWindowReplyPtr) {}));
// If the above operation doesn't cause a crash, the test has succeeded!
}
// This is a test for crbug.com/312016. It tries to create two RenderWidgetHosts
// with the same process and routing ids, which causes a collision. It is almost
// identical to the AttemptDuplicateRenderViewHost test case.
// Crashes on all platforms. http://crbug.com/939338
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
DISABLED_AttemptDuplicateRenderWidgetHost) {
int duplicate_routing_id = MSG_ROUTING_NONE;
RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts(
shell(), embedded_test_server(), &duplicate_routing_id);
EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id);
mojom::WidgetPtr widget;
std::unique_ptr<MockWidgetImpl> widget_impl =
std::make_unique<MockWidgetImpl>(mojo::MakeRequest(&widget));
// Since this test executes on the UI thread and hopping threads might cause
// different timing in the test, let's simulate a CreateNewWidget call coming
// from the IO thread. Use the existing window routing id to cause a
// deliberate collision.
pending_rfh->render_view_host()->CreateNewWidget(duplicate_routing_id,
std::move(widget));
// If the above operation doesn't crash, the test has succeeded!
}
// This is a test for crbug.com/444198. It tries to send a
// FrameHostMsg_RunFileChooser containing an invalid path. The browser should
// correctly terminate the renderer in these cases.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, AttemptRunFileChoosers) {
TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("../../*.txt")));
TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("/etc/*.conf")));
#if defined(OS_WIN)
TestFileChooserWithPath(
base::FilePath(FILE_PATH_LITERAL("\\\\evilserver\\evilshare\\*.txt")));
TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("c:\\*.txt")));
TestFileChooserWithPath(base::FilePath(FILE_PATH_LITERAL("..\\..\\*.txt")));
#endif
}
// A test for crbug.com/941008.
// Calling OpenFileChooser() and EnumerateChosenDirectory() for a single
// FileChooser instance had a problem.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, UnexpectedMethodsSequence) {
NavigateToURL(shell(), GURL("http://foo.com/simple_page.html"));
RenderFrameHost* compromised_renderer =
shell()->web_contents()->GetMainFrame();
auto delegate = std::make_unique<DelayedFileChooserDelegate>();
shell()->web_contents()->SetDelegate(delegate.get());
blink::mojom::FileChooserPtr chooser =
static_cast<RenderFrameHostImpl*>(compromised_renderer)
->BindFileChooserForTesting();
base::RunLoop run_loop1;
base::RunLoop run_loop2;
chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(),
base::BindOnce(FileChooserCallback, &run_loop2));
// The following EnumerateChosenDirectory() runs the specified callback
// immediately regardless of the content of the first argument FilePath.
chooser->EnumerateChosenDirectory(
base::FilePath(FILE_PATH_LITERAL(":*?\"<>|")),
base::BindOnce(FileChooserCallback, &run_loop1));
run_loop1.Run();
delegate->Choose(base::FilePath(FILE_PATH_LITERAL("foo.txt")));
run_loop2.Run();
// The test passes if it doesn't crash.
}
class SecurityExploitTestInterstitialPage : public InterstitialPageDelegate {
public:
explicit SecurityExploitTestInterstitialPage(WebContents* contents) {
InterstitialPage* interstitial = InterstitialPage::Create(
contents, true, contents->GetLastCommittedURL(), this);
interstitial->Show();
}
// InterstitialPageDelegate implementation.
void CommandReceived(const std::string& command) override {
last_command_ = command;
}
std::string GetHTMLContents() override {
return "<html><head><script>"
"window.domAutomationController.send(\"okay\");"
"</script></head>"
"<body>this page is an interstitial</body></html>";
}
std::string last_command() { return last_command_; }
private:
std::string last_command_;
DISALLOW_COPY_AND_ASSIGN(SecurityExploitTestInterstitialPage);
};
// Fails due to InterstitialPage's reliance on PostNonNestableTask
// http://crbug.com/432737
#if defined(OS_ANDROID)
#define MAYBE_InterstitialCommandFromUnderlyingContent \
DISABLED_InterstitialCommandFromUnderlyingContent
#else
#define MAYBE_InterstitialCommandFromUnderlyingContent \
InterstitialCommandFromUnderlyingContent
#endif
// The interstitial should not be controllable by the underlying content.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
MAYBE_InterstitialCommandFromUnderlyingContent) {
// Start off with initial navigation, to allocate the process.
GURL foo("http://foo.com/simple_page.html");
NavigateToURL(shell(), foo);
EXPECT_EQ(base::ASCIIToUTF16("OK"), shell()->web_contents()->GetTitle());
DOMMessageQueue message_queue;
// Install and show an interstitial page.
SecurityExploitTestInterstitialPage* interstitial =
new SecurityExploitTestInterstitialPage(shell()->web_contents());
ASSERT_EQ("", interstitial->last_command());
WaitForInterstitialAttach(shell()->web_contents());
InterstitialPage* interstitial_page =
shell()->web_contents()->GetInterstitialPage();
ASSERT_TRUE(interstitial_page != nullptr);
ASSERT_TRUE(shell()->web_contents()->ShowingInterstitialPage());
ASSERT_TRUE(interstitial_page->GetDelegateForTesting() == interstitial);
// The interstitial page ought to be able to send a message.
std::string message;
ASSERT_TRUE(message_queue.WaitForMessage(&message));
ASSERT_EQ("\"okay\"", message);
ASSERT_EQ("\"okay\"", interstitial->last_command());
// Send an automation message from the underlying content and wait for it to
// be dispatched on this thread. This message should not be received by the
// interstitial.
RenderFrameHost* compromised_renderer =
shell()->web_contents()->GetMainFrame();
FrameHostMsg_DomOperationResponse evil(compromised_renderer->GetRoutingID(),
"evil");
IpcSecurityTestUtil::PwnMessageReceived(
compromised_renderer->GetProcess()->GetChannel(), evil);
ASSERT_TRUE(message_queue.WaitForMessage(&message));
ASSERT_EQ("evil", message)
<< "Automation message should be received by WebContents.";
ASSERT_EQ("\"okay\"", interstitial->last_command())
<< "Interstitial should not be affected.";
// Send a second message from the interstitial page, and make sure that the
// "evil" message doesn't arrive in the intervening period.
ExecuteScriptAsync(interstitial_page->GetMainFrame(),
"window.domAutomationController.send(\"okay2\");");
ASSERT_TRUE(message_queue.WaitForMessage(&message));
ASSERT_EQ("\"okay2\"", message);
ASSERT_EQ("\"okay2\"", interstitial->last_command());
}
// Test that receiving a commit with incorrect origin properly terminates the
// renderer process.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, MismatchedOriginOnCommit) {
GURL start_url(embedded_test_server()->GetURL("/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
// Navigate to a new URL, with an interceptor that replaces the origin with
// one that does not match params.url.
GURL url(embedded_test_server()->GetURL("/title2.html"));
PwnCommitIPC(shell()->web_contents(), url, url,
url::Origin::Create(GURL("http://bar.com/")));
// Use LoadURL, as the test shouldn't wait for navigation commit.
NavigationController& controller = shell()->web_contents()->GetController();
controller.LoadURL(url, Referrer(), ui::PAGE_TRANSITION_LINK, std::string());
EXPECT_NE(nullptr, controller.GetPendingEntry());
EXPECT_EQ(url, controller.GetPendingEntry()->GetURL());
RenderProcessHostKillWaiter kill_waiter(
root->current_frame_host()->GetProcess());
// When the IPC message is received and validation fails, the process is
// terminated. However, the notification for that should be processed in a
// separate task of the message loop, so ensure that the process is still
// considered alive.
EXPECT_TRUE(
root->current_frame_host()->GetProcess()->IsInitializedAndNotDead());
EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait());
}
namespace {
// Interceptor that replaces |interface_params| with the specified
// value for the first DidCommitProvisionalLoad message it observes in the given
// |web_contents| while in scope.
class ScopedInterfaceParamsReplacer : public DidCommitNavigationInterceptor {
public:
ScopedInterfaceParamsReplacer(
WebContents* web_contents,
mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override)
: DidCommitNavigationInterceptor(web_contents),
params_override_(std::move(params_override)) {}
~ScopedInterfaceParamsReplacer() override = default;
protected:
bool WillProcessDidCommitNavigation(
RenderFrameHost* render_frame_host,
NavigationRequest* navigation_request,
::FrameHostMsg_DidCommitProvisionalLoad_Params* params,
mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
override {
interface_params->Swap(&params_override_);
return true;
}
private:
mojom::DidCommitProvisionalLoadInterfaceParamsPtr params_override_;
DISALLOW_COPY_AND_ASSIGN(ScopedInterfaceParamsReplacer);
};
} // namespace
// Test that, as a general rule, not receiving new
// DidCommitProvisionalLoadInterfaceParamsPtr for a cross-document navigation
// properly terminates the renderer process. There is one exception to this
// rule, see: RenderFrameHostImplBrowserTest.
// InterfaceProviderRequestIsOptionalForFirstCommit.
// TODO(crbug.com/718652): when all clients are converted to use
// DocumentInterfaceBroker, InterfaceProviderRequest-related code will be
// removed.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
MissingInterfaceProviderOnNonSameDocumentCommit) {
const GURL start_url(embedded_test_server()->GetURL("/title1.html"));
const GURL non_same_document_url(
embedded_test_server()->GetURL("/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
RenderFrameHostImpl* frame = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetMainFrame());
RenderProcessHostKillWaiter kill_waiter(frame->GetProcess());
ScopedInterfaceParamsReplacer replacer(shell()->web_contents(), nullptr);
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 behing and leak
// NavigationRequests - see https://crbug.com/869193.
EXPECT_EQ(0u, frame->GetNavigationEntryIdsPendingCommit().size());
}
// Test that a compromised renderer cannot ask to upload an arbitrary file in
// OpenURL. This is a regression test for https://crbug.com/726067.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
OpenUrl_ResourceRequestBody) {
GURL start_url(embedded_test_server()->GetURL("/title1.html"));
GURL target_url(embedded_test_server()->GetURL("/echoall"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
RenderProcessHostKillWaiter kill_waiter(
root->current_frame_host()->GetProcess());
// Prepare a file to upload.
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir temp_dir;
base::FilePath file_path;
std::string file_content("test-file-content");
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
ASSERT_LT(
0, base::WriteFile(file_path, file_content.data(), file_content.size()));
// Simulate an IPC message asking to POST a file that the renderer shouldn't
// have access to.
FrameHostMsg_OpenURL_Params params;
params.url = target_url;
params.uses_post = true;
params.resource_request_body = new network::ResourceRequestBody;
params.resource_request_body->AppendFileRange(
file_path, 0, file_content.size(), base::Time());
params.disposition = WindowOpenDisposition::CURRENT_TAB;
params.should_replace_current_entry = true;
params.user_gesture = true;
FrameHostMsg_OpenURL msg(root->current_frame_host()->routing_id(), params);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
root->current_frame_host()->GetProcess()->GetChannel(), msg);
// Verify that the malicious navigation did not commit the navigation to
// |target_url|.
WaitForLoadStop(shell()->web_contents());
EXPECT_EQ(start_url, root->current_frame_host()->GetLastCommittedURL());
// Verify that the malicious renderer got killed.
EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait());
}
// Test that forging a frame's unique name and commit won't allow changing the
// PageState of a cross-process FrameNavigationEntry.
// See https://crbug.com/766262.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PageStateToWrongEntry) {
IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
// Commit a page with nested iframes and a separate cross-process iframe.
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a(a),b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
NavigationEntryImpl* back_entry = static_cast<NavigationEntryImpl*>(
shell()->web_contents()->GetController().GetLastCommittedEntry());
int nav_entry_id = back_entry->GetUniqueID();
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
FrameTreeNode* child0_0 = root->child_at(0)->child_at(0);
std::string child0_0_unique_name = child0_0->unique_name();
FrameTreeNode* child1 = root->child_at(1);
GURL child1_url = child1->current_url();
int child1_pid = child1->current_frame_host()->GetProcess()->GetID();
PageState child1_page_state = back_entry->GetFrameEntry(child1)->page_state();
// Add a history item in the nested frame. It's important to do it there and
// not the main frame for the repro to work, since we don't walk the subtree
// when navigating back/forward between same document items.
TestNavigationObserver fragment_observer(shell()->web_contents());
EXPECT_TRUE(ExecuteScript(child0_0, "location.href = '#foo';"));
fragment_observer.Wait();
// Simulate a name change IPC from the nested iframe, matching the cross-site
// iframe's unique name.
child0_0->SetFrameName("foo", child1->unique_name());
// Simulate a back navigation from the now renamed nested iframe, which would
// put a PageState on the cross-site iframe's FrameNavigationEntry. Forge a
// data URL within the PageState that differs from child1_url.
std::unique_ptr<FrameHostMsg_DidCommitProvisionalLoad_Params> params =
std::make_unique<FrameHostMsg_DidCommitProvisionalLoad_Params>();
params->nav_entry_id = nav_entry_id;
params->did_create_new_entry = false;
params->url = GURL("about:blank");
params->transition = ui::PAGE_TRANSITION_AUTO_SUBFRAME;
params->should_update_history = false;
params->gesture = NavigationGestureAuto;
params->method = "GET";
params->page_state = PageState::CreateFromURL(GURL("data:text/html,foo"));
params->origin = url::Origin::Create(GURL("about:blank"));
service_manager::mojom::InterfaceProviderPtr isolated_interface_provider;
blink::mojom::DocumentInterfaceBrokerPtr
isolated_document_interface_broker_content;
blink::mojom::DocumentInterfaceBrokerPtr
isolated_document_interface_broker_blink;
static_cast<mojom::FrameHost*>(child0_0->current_frame_host())
->DidCommitProvisionalLoad(
std::move(params),
mojom::DidCommitProvisionalLoadInterfaceParams::New(
mojo::MakeRequest(&isolated_interface_provider),
mojo::MakeRequest(&isolated_document_interface_broker_content),
mojo::MakeRequest(&isolated_document_interface_broker_blink),
mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker>()
.InitWithNewPipeAndPassReceiver()));
// Make sure we haven't changed the FrameNavigationEntry. An attack would
// modify the PageState but leave the SiteInstance as it was.
EXPECT_EQ(child1->current_frame_host()->GetSiteInstance(),
back_entry->GetFrameEntry(child1)->site_instance());
EXPECT_EQ(child1_page_state, back_entry->GetFrameEntry(child1)->page_state());
// Put the frame's unique name back.
child0_0->SetFrameName("bar", child0_0_unique_name);
// Go forward after the fake back navigation.
TestNavigationObserver forward_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoForward();
forward_observer.Wait();
// Go back to the possibly corrupted entry and ensure we didn't load the data
// URL in the previous process. A test failure here would appear as a failure
// of the URL check and not the process ID check.
TestNavigationObserver back_observer(shell()->web_contents());
shell()->web_contents()->GetController().GoBack();
back_observer.Wait();
EXPECT_EQ(child1_pid, child1->current_frame_host()->GetProcess()->GetID());
ASSERT_EQ(child1_url, child1->current_url());
}
class SecurityExploitBrowserTestMojoBlobURLs
: public SecurityExploitBrowserTest {
public:
SecurityExploitBrowserTestMojoBlobURLs() = default;
void TearDown() override {
storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(nullptr);
}
};
// Check that when site isolation is enabled, an origin can't create a blob URL
// for a different origin. Similar to the test above, but checks the
// mojo-based Blob URL implementation. See https://crbug.com/886976.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTestMojoBlobURLs,
CreateMojoBlobURLInDifferentOrigin) {
IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHost* rfh = shell()->web_contents()->GetMainFrame();
// Intercept future blob URL registrations and overwrite the blob URL origin
// with b.com.
std::string target_origin = "http://b.com";
std::string blob_path = "5881f76e-10d2-410d-8c61-ef210502acfd";
auto intercept_hook =
base::BindRepeating(&BlobURLStoreInterceptor::Intercept,
GURL("blob:" + target_origin + "/" + blob_path));
storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(&intercept_hook);
// Register a blob URL from the a.com main frame, which will go through the
// interceptor above and be rewritten to register the blob URL with the b.com
// origin. This should result in a kill because a.com should not be allowed
// to create blob URLs outside of its own origin.
base::HistogramTester histograms;
RenderProcessHostWatcher crash_observer(
rfh->GetProcess(), RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
// The renderer should always get killed, but sometimes ExecuteScript returns
// true anyway, so just ignore the result.
ignore_result(
content::ExecuteScript(rfh, "URL.createObjectURL(new Blob(['foo']))"));
// If the process is killed, this test passes.
crash_observer.Wait();
histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 123,
1);
}
// Check that with site isolation enabled, an origin can't create a filesystem
// URL for a different origin. See https://crbug.com/888001.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
CreateFilesystemURLInDifferentOrigin) {
IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHost* rfh = shell()->web_contents()->GetMainFrame();
// Block the renderer on operation that never completes, to shield it from
// receiving unexpected browser->renderer IPCs that might CHECK.
rfh->ExecuteJavaScriptWithUserGestureForTests(
base::ASCIIToUTF16("var r = new XMLHttpRequest();"
"r.open('GET', '/slow?99999', false);"
"r.send(null);"
"while (1);"));
// Set up a blob ID and populate it with attacker-controlled value. This
// is just using the blob APIs directly since creating arbitrary blobs is not
// what is prohibited; this data is not in any origin.
std::string payload = "<html><body>pwned.</body></html>";
std::string payload_type = "text/html";
std::unique_ptr<content::BlobHandle> blob = CreateMemoryBackedBlob(
rfh->GetSiteInstance()->GetBrowserContext(), payload, payload_type);
std::string blob_id = blob->GetUUID();
// Target a different origin.
std::string target_origin = "http://b.com";
GURL target_url =
GURL("filesystem:" + target_origin + "/temporary/exploit.html");
// Note: a well-behaved renderer would always call Open first before calling
// Create and Write, but it's actually not necessary for the original attack
// to succeed, so we omit it. As a result there are some log warnings from the
// quota observer.
PwnMessageHelper::FileSystemCreate(rfh->GetProcess(), 23, target_url, false,
false, false);
// Write the blob into the file. If successful, this places an
// attacker-controlled value in a resource on the target origin.
PwnMessageHelper::FileSystemWrite(rfh->GetProcess(), 24, target_url, blob_id,
0);
// Now navigate to |target_url| in a subframe. It should not succeed, and the
// subframe should not contain |payload|.
TestNavigationObserver observer(shell()->web_contents());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
NavigateFrameToURL(root->child_at(0), target_url);
EXPECT_FALSE(observer.last_navigation_succeeded());
EXPECT_EQ(net::ERR_FILE_NOT_FOUND, observer.last_net_error_code());
RenderFrameHost* attacked_rfh = root->child_at(0)->current_frame_host();
std::string body =
EvalJs(attacked_rfh, "document.body.innerText").ExtractString();
EXPECT_TRUE(base::StartsWith(body, "Could not load the requested resource",
base::CompareCase::INSENSITIVE_ASCII))
<< " body=" << body;
}
// Verify that when a compromised renderer tries to navigate a remote frame to
// a disallowed URL (e.g., file URL), that navigation is blocked.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
BlockIllegalOpenURLFromRemoteFrame) {
// Explicitly isolating a.com helps ensure that this test is applicable on
// platforms without site-per-process.
IsolateOrigin("a.com");
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
FrameTreeNode* child = root->child_at(0);
// Simulate an IPC message where the top frame asks the remote subframe to
// navigate to a file: URL.
GURL file_url("file:///");
FrameHostMsg_OpenURL_Params params;
params.url = file_url;
params.uses_post = false;
params.disposition = WindowOpenDisposition::CURRENT_TAB;
params.should_replace_current_entry = false;
params.user_gesture = true;
SiteInstance* a_com_instance = root->current_frame_host()->GetSiteInstance();
RenderFrameProxyHost* proxy =
child->render_manager()->GetRenderFrameProxyHost(a_com_instance);
EXPECT_TRUE(proxy);
{
FrameHostMsg_OpenURL msg(proxy->GetRoutingID(), params);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
proxy->GetProcess()->GetChannel(), msg);
}
// Verify that the malicious navigation was blocked. Currently, this happens
// by rewriting the target URL to about:blank#blocked.
//
// TODO(alexmos): Consider killing the renderer process in this case, since
// this security check is already enforced in the renderer process.
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(GURL(kBlockedURL),
child->current_frame_host()->GetLastCommittedURL());
// Navigate to the starting page again to recreate the proxy, then try the
// same malicious navigation with a chrome:// URL.
EXPECT_TRUE(NavigateToURL(shell(), main_url));
child = root->child_at(0);
proxy = child->render_manager()->GetRenderFrameProxyHost(a_com_instance);
EXPECT_TRUE(proxy);
GURL chrome_url(std::string(kChromeUIScheme) + "://" +
std::string(kChromeUIGpuHost));
params.url = chrome_url;
{
FrameHostMsg_OpenURL msg(proxy->GetRoutingID(), params);
IPC::IpcSecurityTestUtil::PwnMessageReceived(
proxy->GetProcess()->GetChannel(), msg);
}
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(GURL(kBlockedURL),
child->current_frame_host()->GetLastCommittedURL());
}
class PostMessageIpcInterceptor : public BrowserMessageFilter {
public:
// Starts listening for IPC messages to |process|, intercepting
// FrameHostMsg_RouteMessageEvent and storing once it comes.
explicit PostMessageIpcInterceptor(RenderProcessHost* process)
: BrowserMessageFilter(FrameMsgStart) {
process->AddFilter(this);
}
// Waits for FrameMsg_PostMessage_Params (if it didn't come yet) and returns
// message payload to the caller.
void WaitForMessage(int32_t* out_routing_id,
FrameMsg_PostMessage_Params* out_params) {
run_loop_.Run();
*out_routing_id = intercepted_routing_id_;
*out_params = intercepted_params_;
}
private:
~PostMessageIpcInterceptor() override = default;
void OnRouteMessageEvent(const FrameMsg_PostMessage_Params& params) {
intercepted_params_ = params;
// UaF would have happened without the call below - the call ensures that
// the data is still valid even once the original message is destroyed.
intercepted_params_.message->data.EnsureDataIsOwned();
}
bool OnMessageReceived(const IPC::Message& message) override {
// Only intercept one message.
if (already_intercepted_)
return false;
// See if we got FrameHostMsg_RouteMessageEvent and if so unpack and store
// its payload in OnRouteMessageEvent.
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(PostMessageIpcInterceptor, message)
IPC_MESSAGE_HANDLER(FrameHostMsg_RouteMessageEvent, OnRouteMessageEvent)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
// If we got FrameHostMsg_RouteMessageEvent, then also store the routing ID
// and signal to the main test thread that it can stop waiting.
if (handled) {
already_intercepted_ = true;
intercepted_routing_id_ = message.routing_id();
run_loop_.Quit();
}
return handled;
}
bool already_intercepted_ = false;
int32_t intercepted_routing_id_;
FrameMsg_PostMessage_Params intercepted_params_;
base::RunLoop run_loop_;
DISALLOW_COPY_AND_ASSIGN(PostMessageIpcInterceptor);
};
// Test verifying that a compromised renderer can't lie about
// FrameMsg_PostMessage_Params::source_origin. See also
// https://crbug.com/915721.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PostMessageSourceOrigin) {
// Explicitly isolating a.com helps ensure that this test is applicable on
// platforms without site-per-process.
IsolateOrigin("b.com");
// Navigate to a page with an OOPIF.
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
// Sanity check of test setup: main frame and subframe should be isolated.
WebContents* web_contents = shell()->web_contents();
RenderFrameHost* main_frame = web_contents->GetMainFrame();
RenderFrameHost* subframe = web_contents->GetAllFrames()[1];
EXPECT_NE(main_frame->GetProcess(), subframe->GetProcess());
// Prepare to intercept FrameHostMsg_RouteMessageEvent IPC message that will
// come from the subframe process.
RenderProcessHost* subframe_process = subframe->GetProcess();
auto ipc_interceptor =
base::MakeRefCounted<PostMessageIpcInterceptor>(subframe_process);
// Post a message from the subframe to the cross-site parent and intercept the
// associated IPC message.
EXPECT_TRUE(ExecJs(subframe, "parent.postMessage('blah', '*')"));
int intercepted_routing_id;
FrameMsg_PostMessage_Params intercepted_params;
ipc_interceptor->WaitForMessage(&intercepted_routing_id, &intercepted_params);
// Change the intercepted message to simulate a compromised subframe renderer
// lying that the |source_origin| of the postMessage is the origin of the
// parent (not of the subframe).
url::Origin invalid_origin =
web_contents->GetMainFrame()->GetLastCommittedOrigin();
FrameMsg_PostMessage_Params evil_params = intercepted_params;
evil_params.source_origin = base::UTF8ToUTF16(invalid_origin.Serialize());
FrameHostMsg_RouteMessageEvent evil_msg(intercepted_routing_id, evil_params);
// Inject the invalid IPC and verify that the renderer gets terminated.
RenderProcessHostKillWaiter kill_waiter(subframe_process);
IpcSecurityTestUtil::PwnMessageReceived(subframe_process->GetChannel(),
evil_msg);
EXPECT_EQ(bad_message::RFPH_POST_MESSAGE_INVALID_SOURCE_ORIGIN,
kill_waiter.Wait());
}
class OpenUrlIpcInterceptor : public BrowserMessageFilter {
public:
// Starts listening for IPC messages to |process|, intercepting
// FrameHostMsg_OpenURL IPC message and storing once it comes.
explicit OpenUrlIpcInterceptor(RenderProcessHost* process)
: BrowserMessageFilter(FrameMsgStart) {
process->AddFilter(this);
}
// Waits for FrameHostMsg_OpenURL (if it didn't come yet) and returns
// message payload to the caller.
void WaitForMessage(int32_t* out_routing_id,
FrameHostMsg_OpenURL_Params* out_params) {
run_loop_.Run();
*out_routing_id = intercepted_routing_id_;
*out_params = intercepted_params_;
}
private:
~OpenUrlIpcInterceptor() override = default;
void OnOpenURL(const FrameHostMsg_OpenURL_Params& params) {
intercepted_params_ = params;
}
bool OnMessageReceived(const IPC::Message& message) override {
// Only intercept one message.
if (already_intercepted_)
return false;
// See if we got FrameHostMsg_RouteMessageEvent and if so unpack and store
// its payload in OnRouteMessageEvent.
bool handled = true;
IPC_BEGIN_MESSAGE_MAP(OpenUrlIpcInterceptor, message)
IPC_MESSAGE_HANDLER(FrameHostMsg_OpenURL, OnOpenURL)
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
// If we got FrameHostMsg_RouteMessageEvent, then also store the routing ID
// and signal to the main test thread that it can stop waiting.
if (handled) {
already_intercepted_ = true;
intercepted_routing_id_ = message.routing_id();
run_loop_.Quit();
}
return handled;
}
bool already_intercepted_ = false;
int32_t intercepted_routing_id_;
FrameHostMsg_OpenURL_Params intercepted_params_;
base::RunLoop run_loop_;
DISALLOW_COPY_AND_ASSIGN(OpenUrlIpcInterceptor);
};
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
InvalidRemoteNavigationInitiator) {
// Explicitly isolating a.com helps ensure that this test is applicable on
// platforms without site-per-process.
IsolateOrigin("a.com");
// Navigate to a test page where the subframe is cross-site (and because of
// IsolateOrigin call above in a separate process) from the main frame.
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
RenderFrameHost* main_frame = shell()->web_contents()->GetMainFrame();
RenderProcessHost* main_process = main_frame->GetProcess();
EXPECT_EQ(2u, shell()->web_contents()->GetAllFrames().size());
RenderFrameHost* subframe = shell()->web_contents()->GetAllFrames()[1];
RenderProcessHost* subframe_process = subframe->GetProcess();
EXPECT_NE(main_process->GetID(), subframe_process->GetID());
// Prepare to intercept FrameHostMsg_OpenURL IPC message that will come from
// the main frame process.
auto ipc_interceptor =
base::MakeRefCounted<OpenUrlIpcInterceptor>(main_process);
// Have the main frame request navigation in the "remote" subframe. This will
// result in FrameHostMsg_OpenURL IPC being sent to the RenderFrameProxyHost.
EXPECT_TRUE(ExecJs(shell()->web_contents()->GetMainFrame(),
"window.frames[0].location = '/title1.html';"));
int intercepted_routing_id;
FrameHostMsg_OpenURL_Params intercepted_params;
ipc_interceptor->WaitForMessage(&intercepted_routing_id, &intercepted_params);
// Change the intercepted message to simulate a compromised subframe renderer
// lying that the |initiator_origin| is the origin of the |subframe|.
FrameHostMsg_OpenURL_Params evil_params = intercepted_params;
evil_params.initiator_origin = subframe->GetLastCommittedOrigin();
FrameHostMsg_OpenURL evil_msg(intercepted_routing_id, evil_params);
// Inject the invalid IPC and verify that the renderer gets terminated.
RenderProcessHostKillWaiter kill_waiter(main_process);
IpcSecurityTestUtil::PwnMessageReceived(main_process->GetChannel(), evil_msg);
EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
}
class BeginNavigationInitiatorReplacer : public FrameHostInterceptor {
public:
BeginNavigationInitiatorReplacer(
WebContents* web_contents,
base::Optional<url::Origin> initiator_to_inject)
: FrameHostInterceptor(web_contents),
initiator_to_inject_(initiator_to_inject) {}
bool WillDispatchBeginNavigation(
RenderFrameHost* render_frame_host,
mojom::CommonNavigationParamsPtr* common_params,
mojom::BeginNavigationParamsPtr* begin_params,
blink::mojom::BlobURLTokenPtr* blob_url_token,
mojom::NavigationClientAssociatedPtrInfo* navigation_client,
blink::mojom::NavigationInitiatorPtr* navigation_initiator) override {
if (is_activated_) {
(*common_params)->initiator_origin = initiator_to_inject_;
is_activated_ = false;
}
return true;
}
void Activate() { is_activated_ = true; }
private:
base::Optional<url::Origin> initiator_to_inject_;
bool is_activated_ = false;
DISALLOW_COPY_AND_ASSIGN(BeginNavigationInitiatorReplacer);
};
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
InvalidBeginNavigationInitiator) {
// Explicitly isolating a.com helps ensure that this test is applicable on
// platforms without site-per-process.
IsolateOrigin("a.com");
// 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, url::Origin::Create(GURL("http://b.com")));
// Navigate to a test page that will be locked to a.com.
GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(web_contents, main_url));
// Start monitoring for renderer kills.
RenderProcessHost* main_process = web_contents->GetMainFrame()->GetProcess();
RenderProcessHostKillWaiter kill_waiter(main_process);
// Have the main frame navigate and lie that the initiator origin is b.com.
injector.Activate();
// Don't expect a response for the script, as the process may be killed
// before the script sends its completion message.
ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");
// Verify that the renderer was terminated.
EXPECT_EQ(bad_message::INVALID_INITIATOR_ORIGIN, kill_waiter.Wait());
}
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
MissingBeginNavigationInitiator) {
// Prepare to intercept BeginNavigation mojo IPC. This has to be done before
// the test creates the RenderFrameHostImpl that is the target of the IPC.
WebContents* web_contents = shell()->web_contents();
BeginNavigationInitiatorReplacer injector(web_contents, base::nullopt);
// Navigate to a test page.
GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(web_contents, main_url));
// Start monitoring for renderer kills.
RenderProcessHost* main_process = web_contents->GetMainFrame()->GetProcess();
RenderProcessHostKillWaiter kill_waiter(main_process);
// Have the main frame submit a BeginNavigation IPC with a missing initiator.
injector.Activate();
// Don't expect a response for the script, as the process may be killed
// before the script sends its completion message.
ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");
// Verify that the renderer was terminated.
EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_MISSING_INITIATOR_ORIGIN,
kill_waiter.Wait());
}
namespace {
// An interceptor class that allows replacing the URL of the commit IPC from
// the renderer process to the browser process.
class DidCommitUrlReplacer : public DidCommitNavigationInterceptor {
public:
DidCommitUrlReplacer(WebContents* web_contents, const GURL& replacement_url)
: DidCommitNavigationInterceptor(web_contents),
replacement_url_(replacement_url) {}
~DidCommitUrlReplacer() override = default;
protected:
bool WillProcessDidCommitNavigation(
RenderFrameHost* render_frame_host,
NavigationRequest* navigation_request,
::FrameHostMsg_DidCommitProvisionalLoad_Params* params,
mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
override {
params->url = replacement_url_;
return true;
}
private:
GURL replacement_url_;
DISALLOW_COPY_AND_ASSIGN(DidCommitUrlReplacer);
};
} // namespace
// Test which verifies that when an exploited renderer process sends a commit
// message with URL that the process is not allowed to commit.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DidCommitInvalidURL) {
// Explicitly isolating foo.com helps ensure that this test is applicable on
// platforms without site-per-process.
IsolateOrigin("foo.com");
// Navigate to foo.com initially.
GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), foo_url));
// Create the interceptor object which will replace the URL of the subsequent
// navigation with bar.com based URL.
GURL bar_url(embedded_test_server()->GetURL("bar.com", "/title3.html"));
DidCommitUrlReplacer url_replacer(shell()->web_contents(), bar_url);
// Navigate to another URL within foo.com, which would usually be committed
// succsesfuly, but when the URL is modified it should result in the
// termination of the renderer process.
RenderProcessHostKillWaiter kill_waiter(
shell()->web_contents()->GetMainFrame()->GetProcess());
NavigateToURL(shell(),
embedded_test_server()->GetURL("foo.com", "/title2.html"));
EXPECT_EQ(bad_message::RFH_CAN_COMMIT_URL_BLOCKED, kill_waiter.Wait());
}
class BeginNavigationTransitionReplacer : public FrameHostInterceptor {
public:
BeginNavigationTransitionReplacer(WebContents* web_contents,
ui::PageTransition transition_to_inject)
: FrameHostInterceptor(web_contents),
transition_to_inject_(transition_to_inject) {}
bool WillDispatchBeginNavigation(
RenderFrameHost* render_frame_host,
mojom::CommonNavigationParamsPtr* common_params,
mojom::BeginNavigationParamsPtr* begin_params,
blink::mojom::BlobURLTokenPtr* blob_url_token,
mojom::NavigationClientAssociatedPtrInfo* navigation_client,
blink::mojom::NavigationInitiatorPtr* navigation_initiator) override {
if (is_activated_) {
(*common_params)->transition = transition_to_inject_;
is_activated_ = false;
}
return true;
}
void Activate() { is_activated_ = true; }
private:
ui::PageTransition transition_to_inject_;
bool is_activated_ = false;
DISALLOW_COPY_AND_ASSIGN(BeginNavigationTransitionReplacer);
};
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, NonWebbyTransition) {
const ui::PageTransition test_cases[] = {
ui::PAGE_TRANSITION_TYPED,
ui::PAGE_TRANSITION_AUTO_BOOKMARK,
ui::PAGE_TRANSITION_GENERATED,
ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
ui::PAGE_TRANSITION_RELOAD,
ui::PAGE_TRANSITION_KEYWORD,
ui::PAGE_TRANSITION_KEYWORD_GENERATED};
for (ui::PageTransition transition : test_cases) {
// Prepare to intercept BeginNavigation mojo IPC. This has to be done
// before the test creates the RenderFrameHostImpl that is the target of the
// IPC.
WebContents* web_contents = shell()->web_contents();
BeginNavigationTransitionReplacer injector(web_contents, transition);
// Navigate to a test page.
GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
EXPECT_TRUE(NavigateToURL(web_contents, main_url));
// Start monitoring for renderer kills.
RenderProcessHost* main_process =
web_contents->GetMainFrame()->GetProcess();
RenderProcessHostKillWaiter kill_waiter(main_process);
// Have the main frame submit a BeginNavigation IPC with a missing
// initiator.
injector.Activate();
// Don't expect a response for the script, as the process may be killed
// before the script sends its completion message.
ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");
// Verify that the renderer was terminated.
EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_NON_WEBBY_TRANSITION,
kill_waiter.Wait());
}
}
} // namespace content