blob: 9f13f95ed9b0d05eeb651fd47b4b0b47bf45c188 [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/command_line.h"
#include "base/containers/hash_tables.h"
#include "base/macros.h"
#include "base/strings/utf_string_conversions.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/loader/resource_message_filter.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/browser_context.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.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/resource_dispatcher_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/appcache_info.h"
#include "content/public/common/browser_side_navigation_policy.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/file_chooser_params.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_provisional_load_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 "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/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 "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,
int* target_routing_id) {
GURL foo("http://foo.com/simple_page.html");
// 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("https://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, false,
nullptr, std::string(), Referrer(), WindowOpenDisposition::CURRENT_TAB,
false, true, blink::WebTriggeringEventInfo::kFromTrustedEvent);
// 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 = RESOURCE_TYPE_XHR;
request.appcache_host_id = kAppCacheNoHostId;
request.download_to_file = false;
request.should_reset_appcache = false;
request.is_main_frame = true;
request.transition_type = ui::PAGE_TRANSITION_LINK;
request.allow_download = true;
return request;
}
network::ResourceRequest CreateXHRRequestWithOrigin(const char* origin) {
network::ResourceRequest request =
CreateXHRRequest("http://bar.com/simple_page.html");
request.site_for_cookies = GURL(origin);
request.headers.SetHeader(net::HttpRequestHeaders::kOrigin, origin);
return request;
}
} // 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();
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
base::BindOnce(&net::URLRequestSlowDownloadJob::AddUrlHandler));
}
static void CreateLoaderAndStartOnIOThread(
scoped_refptr<ResourceMessageFilter> filter,
int route_id,
int request_id,
const network::ResourceRequest& request) {
network::mojom::URLLoaderPtr loader;
network::TestURLLoaderClient client;
filter->CreateLoaderAndStart(
mojo::MakeRequest(&loader), route_id, request_id,
network::mojom::kURLLoadOptionNone, request,
client.CreateInterfacePtr(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
}
static void CreateLoaderAndStart(RenderProcessHost* process,
int route_id,
int request_id,
const network::ResourceRequest& request) {
RenderProcessHostImpl* impl = static_cast<RenderProcessHostImpl*>(process);
auto filter = impl->resource_message_filter_;
process->GetChannel()->ipc_task_runner()->PostTask(
FROM_HERE, base::BindOnce(CreateLoaderAndStartOnIOThread, filter,
route_id, request_id, request));
}
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());
CreateLoaderAndStart(rfh->GetProcess(), rfh->GetRoutingID(),
kRequestIdNotPreviouslyUsed, request);
CreateLoaderAndStart(rfh->GetProcess(), rfh->GetRoutingID(),
kRequestIdNotPreviouslyUsed, request);
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 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();
RenderProcessHostKillWaiter kill_waiter(compromised_renderer->GetProcess());
FileChooserParams params;
params.default_file_name = path;
FrameHostMsg_RunFileChooser evil(compromised_renderer->GetRoutingID(),
params);
IpcSecurityTestUtil::PwnMessageReceived(
compromised_renderer->GetProcess()->GetChannel(), evil);
EXPECT_EQ(bad_message::RFH_FILE_CHOOSER_PATH, kill_waiter.Wait());
}
// 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(), &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.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
AttemptDuplicateRenderWidgetHost) {
#if defined(OS_WIN)
// PlzNavigate
// This test is failing in a flaky manner on Android as it appears to be
// leaking URLRequests. See crbug.com/702584.
if (IsBrowserSideNavigationEnabled())
return;
#endif
int duplicate_routing_id = MSG_ROUTING_NONE;
RenderFrameHostImpl* pending_rfh =
PrepareToDuplicateHosts(shell(), &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), blink::kWebPopupTypePage);
// 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
}
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());
}
// Intercepts the HTTP origin header and on being invoked once it is found
// aborts the requeest.
void OnHttpHeaderReceived(const std::string& header,
const std::string& value,
int child_process_id,
content::ResourceContext* resource_context,
OnHeaderProcessedCallback callback) {
std::move(callback).Run(HeaderInterceptorResult::KILL);
}
// Renderer processes should not be able to spoof Origin HTTP headers.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, InvalidOriginHeaders) {
// Create a set of IPC messages with various Origin headers.
network::ResourceRequest chrome_origin_msg(
CreateXHRRequestWithOrigin("chrome://settings"));
network::ResourceRequest embedder_isolated_origin_msg(
CreateXHRRequestWithOrigin("https://isolated.bar.com"));
network::ResourceRequest invalid_origin_msg(
CreateXHRRequestWithOrigin("invalidurl"));
network::ResourceRequest invalid_scheme_origin_msg(
CreateXHRRequestWithOrigin("fake-scheme://foo"));
GURL web_url("http://foo.com/simple_page.html");
NavigateToURL(shell(), web_url);
RenderFrameHost* web_rfh = shell()->web_contents()->GetMainFrame();
// Web processes cannot make XHRs with chrome:// Origin headers.
{
RenderProcessHostKillWaiter kill_waiter(web_rfh->GetProcess());
CreateLoaderAndStart(web_rfh->GetProcess(), web_rfh->GetRoutingID(),
kRequestIdNotPreviouslyUsed, chrome_origin_msg);
EXPECT_EQ(bad_message::RDH_ILLEGAL_ORIGIN, kill_waiter.Wait());
}
// Web processes cannot make XHRs with URLs that the content embedder expects
// to have process isolation. Ideally this would test chrome-extension://
// URLs for Chrome Apps, but those can't be tested inside content/ and the
// ResourceRequest IPC can't be created in a test outside content/.
NavigateToURL(shell(), web_url);
{
content::ResourceDispatcherHost::Get()->RegisterInterceptor(
"Origin", "", base::Bind(&OnHttpHeaderReceived));
RenderProcessHostKillWaiter kill_waiter(web_rfh->GetProcess());
CreateLoaderAndStart(web_rfh->GetProcess(), web_rfh->GetRoutingID(),
kRequestIdNotPreviouslyUsed,
embedder_isolated_origin_msg);
EXPECT_EQ(bad_message::RDH_ILLEGAL_ORIGIN, kill_waiter.Wait());
}
// Web processes cannot make XHRs with invalid Origin headers.
NavigateToURL(shell(), web_url);
{
RenderProcessHostKillWaiter kill_waiter(web_rfh->GetProcess());
CreateLoaderAndStart(web_rfh->GetProcess(), web_rfh->GetRoutingID(),
kRequestIdNotPreviouslyUsed, invalid_origin_msg);
EXPECT_EQ(bad_message::RDH_ILLEGAL_ORIGIN, kill_waiter.Wait());
}
// Web processes cannot make XHRs with invalid scheme Origin headers.
NavigateToURL(shell(), web_url);
{
RenderProcessHostKillWaiter kill_waiter(web_rfh->GetProcess());
CreateLoaderAndStart(web_rfh->GetProcess(), web_rfh->GetRoutingID(),
kRequestIdNotPreviouslyUsed,
invalid_scheme_origin_msg);
EXPECT_EQ(bad_message::RDH_ILLEGAL_ORIGIN, kill_waiter.Wait());
}
}
// Renderer process should not be able to create multiple requests with the same
// id.
// Disabled due to flakiness on all operating systems: crbug.com/799516
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DISABLED_InvalidRequestId) {
// Existing loader in pending_loaders_.
TryCreateDuplicateRequestIds(shell(), false);
// Existing loader in blocked_loaders_map_.
TryCreateDuplicateRequestIds(shell(), true);
}
// 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();
// Setup an URL which will never commit, allowing this test to send its own,
// malformed, commit message.
GURL url(embedded_test_server()->GetURL("/hung"));
// 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());
// Create commit params with different origins in params.url and
// params.origin.
std::unique_ptr<FrameHostMsg_DidCommitProvisionalLoad_Params> params =
std::make_unique<FrameHostMsg_DidCommitProvisionalLoad_Params>();
params->nav_entry_id = 0;
params->did_create_new_entry = false;
params->url = url;
params->transition = ui::PAGE_TRANSITION_LINK;
params->should_update_history = false;
params->gesture = NavigationGestureAuto;
params->method = "GET";
params->page_state = PageState::CreateFromURL(url);
params->origin = url::Origin::Create(GURL("http://bar.com/"));
service_manager::mojom::InterfaceProviderPtr interface_provider;
static_cast<mojom::FrameHost*>(root->current_frame_host())
->DidCommitProvisionalLoad(std::move(params),
mojo::MakeRequest(&interface_provider));
// 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()->HasConnection());
EXPECT_EQ(bad_message::RFH_INVALID_ORIGIN_ON_COMMIT, kill_waiter.Wait());
}
namespace {
// Interceptor that replaces |interface_provider_request| with the specified
// value for the first DidCommitProvisionalLoad message it observes in the given
// |web_contents| while in scope.
class ScopedInterfaceProviderRequestReplacer
: public DidCommitProvisionalLoadInterceptor {
public:
ScopedInterfaceProviderRequestReplacer(
WebContents* web_contents,
service_manager::mojom::InterfaceProviderRequest
interface_provider_request_override)
: DidCommitProvisionalLoadInterceptor(web_contents),
interface_provider_request_override_(
std::move(interface_provider_request_override)) {}
~ScopedInterfaceProviderRequestReplacer() override = default;
protected:
void WillDispatchDidCommitProvisionalLoad(
RenderFrameHost* render_frame_host,
::FrameHostMsg_DidCommitProvisionalLoad_Params* params,
service_manager::mojom::InterfaceProviderRequest*
interface_provider_request) override {
ASSERT_TRUE(interface_provider_request_override_.has_value());
*interface_provider_request =
std::move(interface_provider_request_override_).value();
}
private:
base::Optional<service_manager::mojom::InterfaceProviderRequest>
interface_provider_request_override_;
DISALLOW_COPY_AND_ASSIGN(ScopedInterfaceProviderRequestReplacer);
};
} // namespace
// Test that, as a general rule, not receiving a new InterfaceProviderRequest
// for a cross-document navigation properly terminates the renderer process.
// There is one exception to this rule, see: RenderFrameHostImplBrowserTest.
// InterfaceProviderRequestIsOptionalForFirstCommit.
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));
RenderProcessHostKillWaiter kill_waiter(
shell()->web_contents()->GetMainFrame()->GetProcess());
ScopedInterfaceProviderRequestReplacer replacer(shell()->web_contents(),
nullptr);
NavigateToURLAndExpectNoCommit(shell(), non_same_document_url);
EXPECT_EQ(bad_message::RFH_INTERFACE_PROVIDER_MISSING, kill_waiter.Wait());
}
// 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::ThreadRestrictions::ScopedAllowIO allow_io_for_temp_dir;
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;
params.is_history_navigation_in_new_child = false;
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::RFH_ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait());
}
// Test that forging a frame's unique name and commit won't allow changing the
// PageState of a cross-process FrameNavigationEntry.
// See https://crbug.com/766262.
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PageStateToWrongEntry) {
IsolateAllSitesForTesting(base::CommandLine::ForCurrentProcess());
// Commit a page with nested iframes and a separate cross-process iframe.
GURL main_url(embedded_test_server()->GetURL(
"a.com", "/cross_site_iframe_factory.html?a(a(a),b)"));
EXPECT_TRUE(NavigateToURL(shell(), main_url));
NavigationEntryImpl* back_entry = static_cast<NavigationEntryImpl*>(
shell()->web_contents()->GetController().GetLastCommittedEntry());
int nav_entry_id = back_entry->GetUniqueID();
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetFrameTree()
->root();
FrameTreeNode* child0_0 = root->child_at(0)->child_at(0);
std::string child0_0_unique_name = child0_0->unique_name();
FrameTreeNode* child1 = root->child_at(1);
GURL child1_url = child1->current_url();
int child1_pid = child1->current_frame_host()->GetProcess()->GetID();
PageState child1_page_state = back_entry->GetFrameEntry(child1)->page_state();
// Add a history item in the nested frame. It's important to do it there and
// not the main frame for the repro to work, since we don't walk the subtree
// when navigating back/forward between same document items.
TestNavigationObserver fragment_observer(shell()->web_contents());
EXPECT_TRUE(ExecuteScript(child0_0, "location.href = '#foo';"));
fragment_observer.Wait();
// Simulate a name change IPC from the nested iframe, matching the cross-site
// iframe's unique name.
child0_0->SetFrameName("foo", child1->unique_name());
// Simulate a back navigation from the now renamed nested iframe, which would
// put a PageState on the cross-site iframe's FrameNavigationEntry. Forge a
// data URL within the PageState that differs from child1_url.
std::unique_ptr<FrameHostMsg_DidCommitProvisionalLoad_Params> params =
std::make_unique<FrameHostMsg_DidCommitProvisionalLoad_Params>();
params->nav_entry_id = nav_entry_id;
params->did_create_new_entry = false;
params->url = GURL("about:blank");
params->transition = ui::PAGE_TRANSITION_AUTO_SUBFRAME;
params->should_update_history = false;
params->gesture = NavigationGestureAuto;
params->method = "GET";
params->page_state = PageState::CreateFromURL(GURL("data:text/html,foo"));
params->origin = url::Origin::Create(GURL("about:blank"));
service_manager::mojom::InterfaceProviderPtr isolated_interface_provider;
static_cast<mojom::FrameHost*>(child0_0->current_frame_host())
->DidCommitProvisionalLoad(
std::move(params), mojo::MakeRequest(&isolated_interface_provider));
// 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());
}
} // namespace content