
This replaces: - base::Optional -> absl::optional - include "base/optional.h" -> include "third_party/abseil-cpp/absl/types/optional.h" - base::nullopt -> absl::nullopt - base::make_optional -> absl::make_optional Bug: 1202909 Change-Id: Ie9f37bcbf6115632a19f4d063387d07b3723926f Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2897246 Commit-Queue: Peter Kasting <pkasting@chromium.org> Reviewed-by: Peter Kasting <pkasting@chromium.org> Owners-Override: Peter Kasting <pkasting@chromium.org> Cr-Commit-Position: refs/heads/master@{#883270}
1629 lines
70 KiB
C++
1629 lines
70 KiB
C++
// 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/files/file_util.h"
|
|
#include "base/macros.h"
|
|
#include "base/memory/ptr_util.h"
|
|
#include "base/strings/stringprintf.h"
|
|
#include "base/strings/utf_string_conversions.h"
|
|
#include "base/test/scoped_feature_list.h"
|
|
#include "build/build_config.h"
|
|
#include "content/browser/bad_message.h"
|
|
#include "content/browser/child_process_security_policy_impl.h"
|
|
#include "content/browser/dom_storage/dom_storage_context_wrapper.h"
|
|
#include "content/browser/dom_storage/session_storage_namespace_impl.h"
|
|
#include "content/browser/renderer_host/navigator.h"
|
|
#include "content/browser/renderer_host/render_frame_host_impl.h"
|
|
#include "content/browser/renderer_host/render_frame_proxy_host.h"
|
|
#include "content/browser/renderer_host/render_process_host_impl.h"
|
|
#include "content/browser/renderer_host/render_view_host_factory.h"
|
|
#include "content/browser/renderer_host/render_view_host_impl.h"
|
|
#include "content/browser/web_contents/file_chooser_impl.h"
|
|
#include "content/browser/web_contents/web_contents_impl.h"
|
|
#include "content/common/frame.mojom.h"
|
|
#include "content/common/render_message_filter.mojom.h"
|
|
#include "content/public/browser/blob_handle.h"
|
|
#include "content/public/browser/browser_context.h"
|
|
#include "content/public/browser/browser_task_traits.h"
|
|
#include "content/public/browser/browser_thread.h"
|
|
#include "content/public/browser/content_browser_client.h"
|
|
#include "content/public/browser/file_select_listener.h"
|
|
#include "content/public/browser/navigation_handle.h"
|
|
#include "content/public/browser/resource_context.h"
|
|
#include "content/public/browser/storage_partition.h"
|
|
#include "content/public/common/bindings_policy.h"
|
|
#include "content/public/common/content_features.h"
|
|
#include "content/public/common/content_switches.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/test_navigation_observer.h"
|
|
#include "content/public/test/test_renderer_host.h"
|
|
#include "content/public/test/test_utils.h"
|
|
#include "content/shell/browser/shell.h"
|
|
#include "content/test/content_browser_test_utils_internal.h"
|
|
#include "content/test/did_commit_navigation_interceptor.h"
|
|
#include "content/test/frame_host_interceptor.h"
|
|
#include "content/test/test_content_browser_client.h"
|
|
#include "ipc/ipc_message.h"
|
|
#include "ipc/ipc_security_test_util.h"
|
|
#include "mojo/core/embedder/embedder.h"
|
|
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
|
|
#include "mojo/public/cpp/bindings/pending_receiver.h"
|
|
#include "mojo/public/cpp/bindings/pending_remote.h"
|
|
#include "mojo/public/cpp/bindings/remote.h"
|
|
#include "mojo/public/cpp/bindings/self_owned_associated_receiver.h"
|
|
#include "mojo/public/cpp/test_support/test_utils.h"
|
|
#include "net/base/filename_util.h"
|
|
#include "net/base/network_isolation_key.h"
|
|
#include "net/dns/mock_host_resolver.h"
|
|
#include "net/test/embedded_test_server/controllable_http_response.h"
|
|
#include "net/test/embedded_test_server/embedded_test_server.h"
|
|
#include "net/test/embedded_test_server/http_request.h"
|
|
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
|
|
#include "services/network/public/cpp/features.h"
|
|
#include "services/network/public/cpp/network_switches.h"
|
|
#include "services/network/public/cpp/resource_request.h"
|
|
#include "services/network/public/mojom/trust_tokens.mojom.h"
|
|
#include "services/network/public/mojom/url_loader.mojom.h"
|
|
#include "services/network/test/test_url_loader_client.h"
|
|
#include "storage/browser/blob/blob_registry_impl.h"
|
|
#include "testing/gmock/include/gmock/gmock.h"
|
|
#include "testing/gtest/include/gtest/gtest.h"
|
|
#include "third_party/abseil-cpp/absl/types/optional.h"
|
|
#include "third_party/blink/public/common/blob/blob_utils.h"
|
|
#include "third_party/blink/public/common/navigation/navigation_policy.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/mojom/choosers/file_chooser.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/loader/mixed_content.mojom.h"
|
|
|
|
using IPC::IpcSecurityTestUtil;
|
|
using ::testing::HasSubstr;
|
|
using ::testing::Optional;
|
|
|
|
namespace content {
|
|
|
|
namespace {
|
|
|
|
// This is a helper function for the tests which attempt to create a
|
|
// duplicate RenderViewHost or RenderWidgetHost. It tries to create two objects
|
|
// with the same process and routing ids, which causes a collision.
|
|
// It creates a couple of windows in process 1, which causes a few routing ids
|
|
// to be allocated. Then a cross-process navigation is initiated, which causes a
|
|
// new process 2 to be created and have a pending RenderViewHost for it. The
|
|
// routing id of the RenderViewHost which is target for a duplicate is set
|
|
// into |target_routing_id| and the pending RenderFrameHost which is used for
|
|
// the attempt is the return value.
|
|
RenderFrameHostImpl* PrepareToDuplicateHosts(Shell* shell,
|
|
net::EmbeddedTestServer* server,
|
|
int* target_routing_id) {
|
|
GURL foo("http://foo.com/simple_page.html");
|
|
|
|
if (IsIsolatedOriginRequiredToGuaranteeDedicatedProcess()) {
|
|
// Isolate "bar.com" so we are guaranteed to get a different process
|
|
// for navigations to this origin.
|
|
IsolateOriginsForTesting(server, shell->web_contents(), {"bar.com"});
|
|
}
|
|
|
|
// Start off with initial navigation, so we get the first process allocated.
|
|
EXPECT_TRUE(NavigateToURL(shell, foo));
|
|
EXPECT_EQ(u"OK", shell->web_contents()->GetTitle());
|
|
|
|
// Open another window, so we generate some more routing ids.
|
|
ShellAddedObserver shell2_observer;
|
|
EXPECT_TRUE(ExecJs(shell, "window.open(document.URL + '#2');"));
|
|
Shell* shell2 = shell2_observer.GetShell();
|
|
|
|
// The new window must be in the same process, but have a new routing id.
|
|
EXPECT_EQ(shell->web_contents()->GetMainFrame()->GetProcess()->GetID(),
|
|
shell2->web_contents()->GetMainFrame()->GetProcess()->GetID());
|
|
*target_routing_id = shell2->web_contents()
|
|
->GetMainFrame()
|
|
->GetRenderViewHost()
|
|
->GetRoutingID();
|
|
EXPECT_NE(*target_routing_id, shell->web_contents()
|
|
->GetMainFrame()
|
|
->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,
|
|
nullptr /* initiator_frame_token */,
|
|
ChildProcessHost::kInvalidUniqueID /* initiator_process_id */,
|
|
url::Origin::Create(foo), nullptr, std::string(), Referrer(),
|
|
WindowOpenDisposition::CURRENT_TAB, false, true,
|
|
blink::mojom::TriggeringEventInfo::kFromTrustedEvent, std::string(),
|
|
nullptr /* blob_url_loader_factory */, absl::nullopt /* impression */);
|
|
|
|
// 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;
|
|
}
|
|
|
|
blink::mojom::OpenURLParamsPtr CreateOpenURLParams(const GURL& url) {
|
|
auto params = blink::mojom::OpenURLParams::New();
|
|
params->url = url;
|
|
params->disposition = WindowOpenDisposition::CURRENT_TAB;
|
|
params->should_replace_current_entry = false;
|
|
params->user_gesture = true;
|
|
return params;
|
|
}
|
|
|
|
std::unique_ptr<content::BlobHandle> CreateMemoryBackedBlob(
|
|
BrowserContext* browser_context,
|
|
const std::string& contents,
|
|
const std::string& content_type) {
|
|
std::unique_ptr<content::BlobHandle> result;
|
|
base::RunLoop loop;
|
|
browser_context->CreateMemoryBackedBlob(
|
|
base::as_bytes(base::make_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;
|
|
}
|
|
|
|
// 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::SelfOwnedAssociatedReceiverRef<blink::mojom::BlobURLStore>
|
|
receiver) {
|
|
auto interceptor =
|
|
base::WrapUnique(new BlobURLStoreInterceptor(target_url));
|
|
auto* raw_interceptor = interceptor.get();
|
|
auto impl = receiver->SwapImplForTesting(std::move(interceptor));
|
|
raw_interceptor->url_store_ = std::move(impl);
|
|
}
|
|
|
|
blink::mojom::BlobURLStore* GetForwardingInterface() override {
|
|
return url_store_.get();
|
|
}
|
|
|
|
void Register(mojo::PendingRemote<blink::mojom::Blob> 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, std::u16string()));
|
|
std::vector<blink::mojom::FileChooserFileInfoPtr> files;
|
|
files.push_back(std::move(file_info));
|
|
listener_->FileSelected(std::move(files), base::FilePath(),
|
|
blink::mojom::FileChooserParams::Mode::kOpen);
|
|
listener_.reset();
|
|
}
|
|
|
|
// WebContentsDelegate overrides
|
|
void RunFileChooser(RenderFrameHost* render_frame_host,
|
|
scoped_refptr<FileSelectListener> listener,
|
|
const blink::mojom::FileChooserParams& params) override {
|
|
listener_ = std::move(listener);
|
|
}
|
|
|
|
void EnumerateDirectory(WebContents* web_contents,
|
|
scoped_refptr<FileSelectListener> listener,
|
|
const base::FilePath& directory_path) override {
|
|
listener->FileSelectionCanceled();
|
|
}
|
|
|
|
private:
|
|
scoped_refptr<FileSelectListener> listener_;
|
|
};
|
|
|
|
void FileChooserCallback(base::RunLoop* run_loop,
|
|
blink::mojom::FileChooserResultPtr result) {
|
|
run_loop->Quit();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// The goal of these tests will be to "simulate" exploited renderer processes,
|
|
// which can send arbitrary IPC messages and confuse browser process internal
|
|
// state, leading to security bugs. We are trying to verify that the browser
|
|
// doesn't perform any dangerous operations in such cases.
|
|
class SecurityExploitBrowserTest : public ContentBrowserTest {
|
|
public:
|
|
SecurityExploitBrowserTest() {}
|
|
|
|
void SetUpCommandLine(base::CommandLine* command_line) override {
|
|
// EmbeddedTestServer::InitializeAndListen() initializes its |base_url_|
|
|
// which is required below. This cannot invoke Start() however as that kicks
|
|
// off the "EmbeddedTestServer IO Thread" which then races with
|
|
// initialization in ContentBrowserTest::SetUp(), http://crbug.com/674545.
|
|
ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
|
|
|
|
// Add a host resolver rule to map all outgoing requests to the test server.
|
|
// This allows us to use "real" hostnames in URLs, which we can use to
|
|
// create arbitrary SiteInstances.
|
|
command_line->AppendSwitchASCII(
|
|
network::switches::kHostResolverRules,
|
|
"MAP * " +
|
|
net::HostPortPair::FromURL(embedded_test_server()->base_url())
|
|
.ToString() +
|
|
",EXCLUDE localhost");
|
|
}
|
|
|
|
void SetUpOnMainThread() override {
|
|
// Complete the manual Start() after ContentBrowserTest's own
|
|
// initialization, ref. comment on InitializeAndListen() above.
|
|
embedded_test_server()->StartAcceptingConnections();
|
|
}
|
|
|
|
protected:
|
|
// Tests that a given file path sent in a FrameHostMsg_RunFileChooser will
|
|
// cause renderer to be killed.
|
|
void TestFileChooserWithPath(const base::FilePath& path);
|
|
|
|
void IsolateOrigin(const std::string& hostname) {
|
|
IsolateOriginsForTesting(embedded_test_server(), shell()->web_contents(),
|
|
{hostname});
|
|
}
|
|
};
|
|
|
|
void SecurityExploitBrowserTest::TestFileChooserWithPath(
|
|
const base::FilePath& path) {
|
|
GURL foo("http://foo.com/simple_page.html");
|
|
EXPECT_TRUE(NavigateToURL(shell(), foo));
|
|
EXPECT_EQ(u"OK", shell()->web_contents()->GetTitle());
|
|
|
|
RenderFrameHost* compromised_renderer =
|
|
shell()->web_contents()->GetMainFrame();
|
|
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_EQ(0, shell()->web_contents()->GetMainFrame()->GetEnabledBindings());
|
|
|
|
RenderFrameHost* compromised_renderer =
|
|
shell()->web_contents()->GetMainFrame();
|
|
RenderProcessHostBadIpcMessageWaiter kill_waiter(
|
|
compromised_renderer->GetProcess());
|
|
compromised_renderer->SetWebUIProperty("toolkit", "views");
|
|
EXPECT_EQ(bad_message::RVH_WEB_UI_BINDINGS_MISMATCH, kill_waiter.Wait());
|
|
}
|
|
|
|
// This is a test for crbug.com/312016 attempting to create duplicate
|
|
// RenderViewHosts. SetupForDuplicateHosts sets up this test case and leaves
|
|
// it in a state with pending RenderViewHost. Before the commit of the new
|
|
// pending RenderViewHost, this test case creates a new window through the new
|
|
// process.
|
|
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
|
|
AttemptDuplicateRenderViewHost) {
|
|
int32_t duplicate_routing_id = MSG_ROUTING_NONE;
|
|
RenderFrameHostImpl* pending_rfh = PrepareToDuplicateHosts(
|
|
shell(), embedded_test_server(), &duplicate_routing_id);
|
|
EXPECT_NE(MSG_ROUTING_NONE, duplicate_routing_id);
|
|
|
|
mojom::CreateNewWindowParamsPtr params = mojom::CreateNewWindowParams::New();
|
|
params->target_url = GURL("about:blank");
|
|
pending_rfh->CreateNewWindow(
|
|
std::move(params), base::BindOnce([](mojom::CreateNewWindowStatus,
|
|
mojom::CreateNewWindowReplyPtr) {}));
|
|
// If the above operation doesn't cause a crash, the test has succeeded!
|
|
}
|
|
|
|
// This is a test for crbug.com/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);
|
|
|
|
// 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->CreateNewPopupWidget(
|
|
mojo::PendingAssociatedReceiver<blink::mojom::PopupWidgetHost>(),
|
|
mojo::PendingAssociatedReceiver<blink::mojom::WidgetHost>(),
|
|
mojo::PendingAssociatedRemote<blink::mojom::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) {
|
|
EXPECT_TRUE(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());
|
|
|
|
mojo::Remote<blink::mojom::FileChooser> chooser =
|
|
FileChooserImpl::CreateBoundForTesting(
|
|
static_cast<RenderFrameHostImpl*>(compromised_renderer));
|
|
base::RunLoop run_loop1;
|
|
base::RunLoop run_loop2;
|
|
chooser->OpenFileChooser(blink::mojom::FileChooserParams::New(),
|
|
base::BindOnce(FileChooserCallback, &run_loop2));
|
|
// The following EnumerateChosenDirectory() runs the specified callback
|
|
// immediately regardless of the content of the first argument FilePath.
|
|
chooser->EnumerateChosenDirectory(
|
|
base::FilePath(FILE_PATH_LITERAL(":*?\"<>|")),
|
|
base::BindOnce(FileChooserCallback, &run_loop1));
|
|
run_loop1.Run();
|
|
|
|
delegate->Choose(base::FilePath(FILE_PATH_LITERAL("foo.txt")));
|
|
run_loop2.Run();
|
|
|
|
// The test passes if it doesn't crash.
|
|
}
|
|
|
|
class CorsExploitBrowserTest : public ContentBrowserTest {
|
|
public:
|
|
CorsExploitBrowserTest() = default;
|
|
|
|
void SetUpOnMainThread() override {
|
|
host_resolver()->AddRule("*", "127.0.0.1");
|
|
SetupCrossSiteRedirector(embedded_test_server());
|
|
}
|
|
|
|
private:
|
|
DISALLOW_COPY_AND_ASSIGN(CorsExploitBrowserTest);
|
|
};
|
|
|
|
// 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());
|
|
|
|
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());
|
|
}
|
|
|
|
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,
|
|
mojom::DidCommitProvisionalLoadParamsPtr*,
|
|
mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
|
|
override {
|
|
interface_params->Swap(¶ms_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
|
|
// 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()->GetMainFrame());
|
|
RenderProcessHostBadIpcMessageWaiter kill_waiter(frame->GetProcess());
|
|
|
|
NavigationHandleObserver navigation_observer(shell()->web_contents(),
|
|
non_same_document_url);
|
|
ScopedInterfaceParamsReplacer replacer(shell()->web_contents(), nullptr);
|
|
EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), non_same_document_url));
|
|
EXPECT_EQ(bad_message::RFH_INTERFACE_PROVIDER_MISSING, kill_waiter.Wait());
|
|
|
|
// Verify that the death of the renderer process doesn't leave behind and
|
|
// leak NavigationRequests - see https://crbug.com/869193.
|
|
EXPECT_FALSE(frame->HasPendingCommitNavigation());
|
|
EXPECT_FALSE(navigation_observer.has_committed());
|
|
EXPECT_TRUE(navigation_observer.is_error());
|
|
EXPECT_TRUE(navigation_observer.last_committed_url().is_empty());
|
|
EXPECT_EQ(net::OK, navigation_observer.net_error_code());
|
|
}
|
|
|
|
// Test that a compromised renderer cannot ask to upload an arbitrary file in
|
|
// OpenURL. This is a regression test for https://crbug.com/726067.
|
|
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
|
|
OpenUrl_ResourceRequestBody) {
|
|
GURL start_url(embedded_test_server()->GetURL("/title1.html"));
|
|
GURL target_url(embedded_test_server()->GetURL("/echoall"));
|
|
EXPECT_TRUE(NavigateToURL(shell(), start_url));
|
|
|
|
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
|
|
->GetFrameTree()
|
|
->root();
|
|
|
|
RenderProcessHostBadIpcMessageWaiter kill_waiter(
|
|
root->current_frame_host()->GetProcess());
|
|
|
|
// Prepare a file to upload.
|
|
base::ScopedAllowBlockingForTesting allow_blocking;
|
|
base::ScopedTempDir temp_dir;
|
|
base::FilePath file_path;
|
|
std::string file_content("test-file-content");
|
|
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
|
|
ASSERT_TRUE(base::CreateTemporaryFileInDir(temp_dir.GetPath(), &file_path));
|
|
ASSERT_TRUE(base::WriteFile(file_path, file_content));
|
|
|
|
// Simulate an OpenURL Mojo method asking to POST a file that the renderer
|
|
// shouldn't have access to.
|
|
auto params = CreateOpenURLParams(target_url);
|
|
params->post_body = new network::ResourceRequestBody;
|
|
params->post_body->AppendFileRange(file_path, 0, file_content.size(),
|
|
base::Time());
|
|
params->should_replace_current_entry = true;
|
|
|
|
static_cast<mojom::FrameHost*>(root->current_frame_host())
|
|
->OpenURL(std::move(params));
|
|
|
|
// Verify that the malicious navigation did not commit the navigation to
|
|
// |target_url|.
|
|
EXPECT_EQ(start_url, root->current_frame_host()->GetLastCommittedURL());
|
|
|
|
// Verify that the malicious renderer got killed.
|
|
EXPECT_EQ(bad_message::ILLEGAL_UPLOAD_PARAMS, kill_waiter.Wait());
|
|
}
|
|
|
|
// Forging a navigation commit after the initial empty document will result in a
|
|
// renderer kill, even if the URL used is about:blank.
|
|
// See https://crbug.com/766262 for an example advanced case that involves
|
|
// forging a frame's unique name.
|
|
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
|
|
NonInitialAboutBlankRendererKill) {
|
|
// Navigate normally.
|
|
GURL url(embedded_test_server()->GetURL("/title1.html"));
|
|
EXPECT_TRUE(NavigateToURL(shell(), url));
|
|
RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
|
|
shell()->web_contents()->GetMainFrame());
|
|
|
|
// 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->gesture = NavigationGestureAuto;
|
|
params->method = "GET";
|
|
params->page_state = blink::PageState::CreateFromURL(GURL("about:blank"));
|
|
params->origin = url::Origin::Create(GURL("about:blank"));
|
|
params->embedding_token = base::UnguessableToken::Create();
|
|
RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
|
|
static_cast<mojom::FrameHost*>(rfh)->DidCommitProvisionalLoad(
|
|
std::move(params),
|
|
mojom::DidCommitProvisionalLoadInterfaceParams::New(
|
|
mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker>()
|
|
.InitWithNewPipeAndPassReceiver()));
|
|
|
|
// Verify that the malicious renderer got killed.
|
|
EXPECT_EQ(bad_message::RFH_NO_MATCHING_NAVIGATION_REQUEST_ON_COMMIT,
|
|
kill_waiter.Wait());
|
|
}
|
|
|
|
class SecurityExploitBrowserTestMojoBlobURLs
|
|
: public SecurityExploitBrowserTest {
|
|
public:
|
|
SecurityExploitBrowserTestMojoBlobURLs() = default;
|
|
|
|
void TearDown() override {
|
|
storage::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.
|
|
content::RenderProcessHostBadMojoMessageWaiter crash_observer(
|
|
rfh->GetProcess());
|
|
|
|
// The renderer should always get killed, but sometimes ExecuteScript returns
|
|
// true anyway, so just ignore the result.
|
|
ignore_result(ExecJs(rfh, "URL.createObjectURL(new Blob(['foo']))"));
|
|
|
|
// If the process is killed, this test passes.
|
|
EXPECT_EQ(
|
|
"Received bad user message: "
|
|
"Non committable URL 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));
|
|
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(
|
|
u"var r = new XMLHttpRequest();"
|
|
u"r.open('GET', '/slow?99999', false);"
|
|
u"r.send(null);"
|
|
u"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.
|
|
SiteInstance* a_com_instance = root->current_frame_host()->GetSiteInstance();
|
|
RenderFrameProxyHost* proxy =
|
|
child->render_manager()->GetRenderFrameProxyHost(a_com_instance);
|
|
EXPECT_TRUE(proxy);
|
|
|
|
TestNavigationObserver observer(shell()->web_contents());
|
|
static_cast<mojom::FrameHost*>(proxy->frame_tree_node()->current_frame_host())
|
|
->OpenURL(CreateOpenURLParams(GURL("file:///")));
|
|
observer.Wait();
|
|
|
|
// Verify that the malicious navigation was blocked. Currently, this happens
|
|
// by rewriting the target URL to about:blank#blocked.
|
|
//
|
|
// TODO(alexmos): Consider killing the renderer process in this case, since
|
|
// this security check is already enforced in the renderer process.
|
|
EXPECT_EQ(GURL(kBlockedURL),
|
|
child->current_frame_host()->GetLastCommittedURL());
|
|
|
|
// Navigate to the starting page again to recreate the proxy, then try the
|
|
// same malicious navigation with a chrome:// URL.
|
|
EXPECT_TRUE(NavigateToURL(shell(), main_url));
|
|
child = root->child_at(0);
|
|
proxy = child->render_manager()->GetRenderFrameProxyHost(a_com_instance);
|
|
EXPECT_TRUE(proxy);
|
|
|
|
TestNavigationObserver observer_2(shell()->web_contents());
|
|
GURL chrome_url(std::string(kChromeUIScheme) + "://" +
|
|
std::string(kChromeUIGpuHost));
|
|
static_cast<mojom::FrameHost*>(proxy->frame_tree_node()->current_frame_host())
|
|
->OpenURL(CreateOpenURLParams(chrome_url));
|
|
observer_2.Wait();
|
|
EXPECT_EQ(GURL(kBlockedURL),
|
|
child->current_frame_host()->GetLastCommittedURL());
|
|
}
|
|
|
|
class RemoteFrameHostInterceptor
|
|
: public blink::mojom::RemoteFrameHostInterceptorForTesting {
|
|
public:
|
|
explicit RemoteFrameHostInterceptor(
|
|
RenderFrameProxyHost* render_frame_proxy_host,
|
|
const std::u16string& evil_origin)
|
|
: render_frame_proxy_host_(render_frame_proxy_host),
|
|
evil_origin_(evil_origin) {
|
|
render_frame_proxy_host_->frame_host_receiver_for_testing()
|
|
.SwapImplForTesting(this);
|
|
}
|
|
|
|
RemoteFrameHost* GetForwardingInterface() override {
|
|
return render_frame_proxy_host_;
|
|
}
|
|
|
|
void RouteMessageEvent(
|
|
const absl::optional<blink::LocalFrameToken>& source_frame_token,
|
|
const std::u16string& source_origin,
|
|
const std::u16string& target_origin,
|
|
blink::TransferableMessage message) override {
|
|
// Forward the message to the actual RFPH replacing |source_origin| with the
|
|
// "evil origin" as especified in SetEvilSourceOriginAndWaitForMessage().
|
|
GetForwardingInterface()->RouteMessageEvent(
|
|
std::move(source_frame_token), std::move(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:
|
|
RenderFrameProxyHost* render_frame_proxy_host_;
|
|
|
|
std::u16string evil_origin_;
|
|
blink::mojom::OpenURLParamsPtr intercepted_params_;
|
|
};
|
|
|
|
// 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->GetMainFrame();
|
|
RenderFrameHost* subframe = web_contents->GetAllFrames()[1];
|
|
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())
|
|
->GetFrameTree()
|
|
->root();
|
|
FrameTreeNode* subframe_node = main_frame_node->child_at(0);
|
|
SiteInstance* b_com_instance =
|
|
subframe_node->current_frame_host()->GetSiteInstance();
|
|
RenderFrameProxyHost* main_frame_proxy_host =
|
|
main_frame_node->render_manager()->GetRenderFrameProxyHost(
|
|
b_com_instance);
|
|
|
|
// Prepare to intercept the RouteMessageEvent IPC message that will come
|
|
// from the subframe process.
|
|
url::Origin invalid_origin =
|
|
web_contents->GetMainFrame()->GetLastCommittedOrigin();
|
|
std::u16string evil_source_origin =
|
|
base::UTF8ToUTF16(invalid_origin.Serialize());
|
|
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());
|
|
}
|
|
|
|
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()->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 OpenURL Mojo message that will come from
|
|
// the main frame.
|
|
FrameTreeNode* main_frame_node =
|
|
static_cast<WebContentsImpl*>(shell()->web_contents())
|
|
->GetFrameTree()
|
|
->root();
|
|
FrameTreeNode* child_node = main_frame_node->child_at(0);
|
|
SiteInstance* a_com_instance =
|
|
main_frame_node->current_frame_host()->GetSiteInstance();
|
|
RenderFrameProxyHost* proxy =
|
|
child_node->render_manager()->GetRenderFrameProxyHost(a_com_instance);
|
|
|
|
RemoteFrameHostInterceptor interceptor(proxy, std::u16string());
|
|
|
|
// 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()->GetMainFrame(),
|
|
"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.
|
|
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);
|
|
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,
|
|
absl::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,
|
|
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:
|
|
absl::optional<url::Origin> initiator_to_inject_;
|
|
bool is_activated_ = false;
|
|
|
|
DISALLOW_COPY_AND_ASSIGN(BeginNavigationInitiatorReplacer);
|
|
};
|
|
|
|
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->GetMainFrame()->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());
|
|
}
|
|
|
|
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, absl::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();
|
|
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);
|
|
|
|
// Have the main frame submit a BeginNavigation IPC with a missing initiator.
|
|
injector.Activate();
|
|
// Don't expect a response for the script, as the process may be killed
|
|
// before the script sends its completion message.
|
|
ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");
|
|
|
|
// Verify that the renderer was terminated.
|
|
EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_MISSING_INITIATOR_ORIGIN,
|
|
kill_waiter.Wait());
|
|
}
|
|
|
|
namespace {
|
|
|
|
// An interceptor class that allows replacing the URL of the commit IPC from
|
|
// the renderer process to the browser process.
|
|
class DidCommitUrlReplacer : public DidCommitNavigationInterceptor {
|
|
public:
|
|
DidCommitUrlReplacer(WebContents* web_contents, const GURL& replacement_url)
|
|
: DidCommitNavigationInterceptor(web_contents),
|
|
replacement_url_(replacement_url) {}
|
|
~DidCommitUrlReplacer() 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_;
|
|
|
|
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");
|
|
|
|
RenderFrameDeletedObserver initial_frame_deleted_observer(
|
|
shell()->web_contents()->GetMainFrame());
|
|
|
|
// 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_ASSUMES_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()->GetMainFrame()->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,
|
|
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()->GetMainFrame());
|
|
|
|
// 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_ASSUMES_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()->GetMainFrame()->GetProcess());
|
|
GURL data_url(R"(data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E)");
|
|
EXPECT_TRUE(
|
|
NavigateIframeToURL(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()->GetMainFrame()->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, committing a non-WebUI URL in it is detected and the process is
|
|
// correctly terminated.
|
|
IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
|
|
DidCommitNonWebUIURLInProcessWithBindings) {
|
|
// Navigate to a web URL.
|
|
GURL initial_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
|
|
EXPECT_TRUE(NavigateToURL(shell(), initial_url));
|
|
|
|
// Start a second navigation.
|
|
GURL web_url(embedded_test_server()->GetURL("foo.com", "/title2.html"));
|
|
TestNavigationManager navigation(shell()->web_contents(), web_url);
|
|
RenderProcessHostBadIpcMessageWaiter kill_waiter(
|
|
shell()->web_contents()->GetMainFrame()->GetProcess());
|
|
|
|
shell()->LoadURL(web_url);
|
|
EXPECT_TRUE(navigation.WaitForResponse());
|
|
|
|
// Grant WebUI bindings to the navigated frame to simulate a bug in the code
|
|
// that incorrectly does it for a navigation that does not require it.
|
|
navigation.GetNavigationHandle()->GetRenderFrameHost()->AllowBindings(
|
|
BINDINGS_POLICY_WEB_UI);
|
|
|
|
// Resume the navigation and upon receiving the commit message the renderer
|
|
// process will be terminated.
|
|
navigation.ResumeNavigation();
|
|
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,
|
|
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;
|
|
|
|
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();
|
|
RenderProcessHostBadIpcMessageWaiter kill_waiter(main_process);
|
|
|
|
// Have the main frame submit a BeginNavigation IPC with a missing
|
|
// initiator.
|
|
injector.Activate();
|
|
// Don't expect a response for the script, as the process may be killed
|
|
// before the script sends its completion message.
|
|
ExecuteScriptAsync(web_contents, "window.location = '/title2.html';");
|
|
|
|
// Verify that the renderer was terminated.
|
|
EXPECT_EQ(bad_message::RFHI_BEGIN_NAVIGATION_NON_WEBBY_TRANSITION,
|
|
kill_waiter.Wait());
|
|
}
|
|
}
|
|
|
|
class SecurityExploitViaDisabledWebSecurityTest
|
|
: public SecurityExploitBrowserTest {
|
|
public:
|
|
SecurityExploitViaDisabledWebSecurityTest() {
|
|
// To get around BlockedSchemeNavigationThrottle. Other attempts at getting
|
|
// around it don't work, i.e.:
|
|
// -if the request is made in a child frame then the frame is torn down
|
|
// immediately on process killing so the navigation doesn't complete
|
|
// -if it's classified as same document, then a DCHECK in
|
|
// NavigationRequest::CreateRendererInitiated fires
|
|
feature_list_.InitAndEnableFeature(
|
|
features::kAllowContentInitiatedDataUrlNavigations);
|
|
}
|
|
|
|
protected:
|
|
void SetUpCommandLine(base::CommandLine* command_line) override {
|
|
// Simulate a compromised renderer, otherwise the cross-origin request to
|
|
// file: is blocked.
|
|
command_line->AppendSwitch(switches::kDisableWebSecurity);
|
|
SecurityExploitBrowserTest::SetUpCommandLine(command_line);
|
|
}
|
|
|
|
private:
|
|
base::test::ScopedFeatureList feature_list_;
|
|
};
|
|
|
|
// Test to verify that an exploited renderer process trying to specify a
|
|
// non-empty URL for base_url_for_data_url on navigation is correctly
|
|
// terminated.
|
|
IN_PROC_BROWSER_TEST_F(SecurityExploitViaDisabledWebSecurityTest,
|
|
ValidateBaseUrlForDataUrl) {
|
|
GURL start_url(embedded_test_server()->GetURL("/title1.html"));
|
|
EXPECT_TRUE(NavigateToURL(shell(), start_url));
|
|
|
|
RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
|
|
shell()->web_contents()->GetMainFrame());
|
|
|
|
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.
|
|
mojom::CommonNavigationParamsPtr common_params =
|
|
mojom::CommonNavigationParams::New(
|
|
data_url, url::Origin::Create(data_url),
|
|
blink::mojom::Referrer::New(), ui::PAGE_TRANSITION_LINK,
|
|
mojom::NavigationType::DIFFERENT_DOCUMENT,
|
|
blink::NavigationDownloadPolicy(),
|
|
false /* should_replace_current_entry */,
|
|
file_url, /* base_url_for_data_url */
|
|
GURL() /* history_url_for_data_url */,
|
|
blink::PreviewsTypes::PREVIEWS_UNSPECIFIED,
|
|
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 */);
|
|
mojom::BeginNavigationParamsPtr begin_params =
|
|
mojom::BeginNavigationParams::New(
|
|
absl::nullopt /* initiator_frame_token */,
|
|
std::string() /* headers */, net::LOAD_NORMAL,
|
|
false /* skip_service_worker */,
|
|
blink::mojom::RequestContextType::LOCATION,
|
|
network::mojom::RequestDestination::kDocument,
|
|
blink::mojom::MixedContentContextType::kBlockable,
|
|
false /* is_form_submission */,
|
|
false /* was_initiated_by_link_click */,
|
|
GURL() /* searchable_form_url */,
|
|
std::string() /* searchable_form_encoding */,
|
|
GURL() /* client_side_redirect_url */,
|
|
absl::nullopt /* devtools_initiator_info */,
|
|
nullptr /* trust_token_params */, absl::nullopt /* impression */,
|
|
base::TimeTicks() /* renderer_before_unload_start */,
|
|
base::TimeTicks() /* renderer_before_unload_end */,
|
|
absl::nullopt /* web_bundle_token */);
|
|
|
|
// 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());
|
|
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());
|
|
}
|
|
|
|
// 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->GetMainFrame()->GetLastCommittedURL());
|
|
EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetLastCommittedOrigin(),
|
|
new_window->GetMainFrame()->GetLastCommittedOrigin());
|
|
EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetProcess(),
|
|
new_window->GetMainFrame()->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->GetMainFrame()->GetLastCommittedURL());
|
|
EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetLastCommittedOrigin(),
|
|
new_window->GetMainFrame()->GetLastCommittedOrigin());
|
|
EXPECT_EQ(shell()->web_contents()->GetMainFrame()->GetProcess(),
|
|
new_window->GetMainFrame()->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,
|
|
mojom::CommonNavigationParamsPtr* common_params,
|
|
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() {
|
|
feature_list_.InitAndEnableFeature(network::features::kTrustTokens);
|
|
}
|
|
|
|
private:
|
|
base::test::ScopedFeatureList feature_list_;
|
|
};
|
|
|
|
// Test that the browser correctly reports a bad message when a child frame
|
|
// attempts to navigate with a Trust Tokens redemption operation associated with
|
|
// the navigation, but its parent lacks the trust-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->type = 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->GetMainFrame();
|
|
ASSERT_FALSE(parent->IsFeatureEnabled(
|
|
blink::mojom::PermissionsPolicyFeature::kTrustTokenRedemption));
|
|
|
|
RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents)
|
|
->GetFrameTree()
|
|
->root()
|
|
->child_at(0)
|
|
->current_frame_host();
|
|
RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess());
|
|
|
|
replacer.Activate();
|
|
|
|
// Note: this can't use NavigateFrameToURL, because that method doesn't
|
|
// route through RFHI::BeginNavigation. It also can't use NavigateIframeToURL,
|
|
// because that navigation will hang.
|
|
//
|
|
// It also can't EXPECT_TRUE or EXPECT_FALSE: sometimes the ExecJs call will
|
|
// finish before the renderer gets killed, and sometimes it won't.
|
|
ignore_result(
|
|
ExecJs(child, JsReplace("location.href=$1;", GURL("/title2.html"))));
|
|
|
|
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 Trust Tokens signing operation associated with
|
|
// the navigation, but its parent lacks the trust-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->type = 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->GetMainFrame();
|
|
ASSERT_FALSE(parent->IsFeatureEnabled(
|
|
blink::mojom::PermissionsPolicyFeature::kTrustTokenRedemption));
|
|
|
|
RenderFrameHost* child = static_cast<WebContentsImpl*>(web_contents)
|
|
->GetFrameTree()
|
|
->root()
|
|
->child_at(0)
|
|
->current_frame_host();
|
|
RenderProcessHostBadMojoMessageWaiter kill_waiter(child->GetProcess());
|
|
|
|
replacer.Activate();
|
|
|
|
// Note: this can't use NavigateFrameToURL, because that method doesn't
|
|
// route through RFHI::BeginNavigation. It also can't use NavigateIframeToURL,
|
|
// because that navigation will hang.
|
|
//
|
|
// It also can't EXPECT_TRUE or EXPECT_FALSE: sometimes the ExecJs call will
|
|
// finish before the renderer gets killed, and sometimes it won't.
|
|
ignore_result(
|
|
ExecJs(child, JsReplace("location.href=$1;", GURL("/title2.html"))));
|
|
|
|
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));
|
|
|
|
RenderFrameHost* compromised_renderer = web_contents->GetMainFrame();
|
|
RenderProcessHostBadMojoMessageWaiter kill_waiter(
|
|
compromised_renderer->GetProcess());
|
|
|
|
replacer.Activate();
|
|
|
|
// Can't use NavigateToURL here because it would hang. Additionally, we can't
|
|
// EXPECT_TRUE or EXPECT_FALSE: sometimes the ExecJs call will finish
|
|
// before the renderer gets killed, and sometimes it won't.
|
|
ignore_result(ExecJs(compromised_renderer,
|
|
JsReplace("location.href=$1", GURL("/title2.html"))));
|
|
|
|
EXPECT_THAT(kill_waiter.Wait(),
|
|
Optional(HasSubstr("Trust Token params in main frame nav")));
|
|
}
|
|
|
|
} // namespace content
|