blob: 73cebdcb72113eaa8cb2a9778eb76329de17793d [file] [log] [blame]
// Copyright 2019 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 "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/hash/hash.h"
#include "base/memory/ref_counted_memory.h"
#include "base/path_service.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/browser/webui/web_ui_controller_factory_registry.h"
#include "content/common/content_navigation_policy.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/url_data_source.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_controller.h"
#include "content/public/browser/web_ui_data_source.h"
#include "content/public/browser/webui_config_map.h"
#include "content/public/common/bindings_policy.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.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/scoped_web_ui_controller_factory_registration.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/public/test/web_ui_browsertest_util.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/base/url_util.h"
#include "ui/webui/untrusted_web_ui_browsertest_util.h"
#include "url/gurl.h"
namespace content {
class WebUISecurityTest : public ContentBrowserTest {
public:
WebUISecurityTest() = default;
WebUISecurityTest(const WebUISecurityTest&) = delete;
WebUISecurityTest& operator=(const WebUISecurityTest&) = delete;
TestWebUIControllerFactory* factory() { return &factory_; }
private:
TestWebUIControllerFactory factory_;
ScopedWebUIControllerFactoryRegistration factory_registration_{&factory_};
};
// Verify chrome-untrusted:// have no bindings.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, UntrustedNoBindings) {
auto* web_contents = shell()->web_contents();
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>("test-host"));
const GURL untrusted_url(GetChromeUntrustedUIURL("test-host/title1.html"));
EXPECT_TRUE(NavigateToURL(web_contents, untrusted_url));
EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()->web_contents()->GetMainFrame()->GetProcess()->GetID()));
EXPECT_EQ(0, shell()->web_contents()->GetMainFrame()->GetEnabledBindings());
}
// Loads a WebUI which does not have any bindings.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, NoBindings) {
GURL test_url(GetWebUIURL("web-ui/title1.html?bindings=0"));
EXPECT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_FALSE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()->web_contents()->GetMainFrame()->GetProcess()->GetID()));
EXPECT_EQ(0, shell()->web_contents()->GetMainFrame()->GetEnabledBindings());
}
// Loads a WebUI which has WebUI bindings.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, WebUIBindings) {
GURL test_url(GetWebUIURL("web-ui/title1.html?bindings=" +
base::NumberToString(BINDINGS_POLICY_WEB_UI)));
EXPECT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()->web_contents()->GetMainFrame()->GetProcess()->GetID()));
EXPECT_EQ(BINDINGS_POLICY_WEB_UI,
shell()->web_contents()->GetMainFrame()->GetEnabledBindings());
}
// Loads a WebUI which has Mojo bindings.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, MojoBindings) {
GURL test_url(GetWebUIURL("web-ui/title1.html?bindings=" +
base::NumberToString(BINDINGS_POLICY_MOJO_WEB_UI)));
EXPECT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()->web_contents()->GetMainFrame()->GetProcess()->GetID()));
EXPECT_EQ(BINDINGS_POLICY_MOJO_WEB_UI,
shell()->web_contents()->GetMainFrame()->GetEnabledBindings());
}
// Loads a WebUI which has both WebUI and Mojo bindings.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, WebUIAndMojoBindings) {
GURL test_url(GetWebUIURL("web-ui/title1.html?bindings=" +
base::NumberToString(BINDINGS_POLICY_WEB_UI |
BINDINGS_POLICY_MOJO_WEB_UI)));
EXPECT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_TRUE(ChildProcessSecurityPolicyImpl::GetInstance()->HasWebUIBindings(
shell()->web_contents()->GetMainFrame()->GetProcess()->GetID()));
EXPECT_EQ(BINDINGS_POLICY_WEB_UI | BINDINGS_POLICY_MOJO_WEB_UI,
shell()->web_contents()->GetMainFrame()->GetEnabledBindings());
}
// Verify that reloading a WebUI document or navigating between documents on
// the same WebUI will result in using the same SiteInstance and will not
// create a new WebUI instance.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, WebUIReuse) {
GURL test_url(GetWebUIURL("web-ui/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), test_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
// Capture the SiteInstance and WebUI used in the first navigation to compare
// with the ones used after the reload.
scoped_refptr<SiteInstance> initial_site_instance =
root->current_frame_host()->GetSiteInstance();
WebUI* initial_web_ui = root->current_frame_host()->web_ui();
// Reload the document and check that SiteInstance and WebUI are reused.
TestFrameNavigationObserver observer(root);
shell()->web_contents()->GetController().Reload(ReloadType::NORMAL, false);
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(test_url, observer.last_committed_url());
EXPECT_EQ(initial_site_instance,
root->current_frame_host()->GetSiteInstance());
EXPECT_EQ(initial_web_ui, root->current_frame_host()->web_ui());
// Navigate to another document on the same WebUI and check that SiteInstance
// and WebUI are reused.
GURL next_url(GetWebUIURL("web-ui/title2.html"));
EXPECT_TRUE(NavigateToURL(shell(), next_url));
EXPECT_EQ(initial_site_instance,
root->current_frame_host()->GetSiteInstance());
EXPECT_EQ(initial_web_ui, root->current_frame_host()->web_ui());
}
// Verify that a WebUI can add a subframe for its own WebUI.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, WebUISameSiteSubframe) {
GURL test_url(GetWebUIURL("web-ui/page_with_blank_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), test_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(1U, root->child_count());
TestFrameNavigationObserver observer(root->child_at(0));
GURL subframe_url(GetWebUIURL("web-ui/title1.html?noxfo=true"));
NavigateFrameToURL(root->child_at(0), subframe_url);
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(subframe_url, observer.last_committed_url());
EXPECT_EQ(root->current_frame_host()->GetSiteInstance(),
root->child_at(0)->current_frame_host()->GetSiteInstance());
EXPECT_EQ(
GetWebUIURL("web-ui"),
root->child_at(0)->current_frame_host()->GetSiteInstance()->GetSiteURL());
// The subframe should have its own WebUI object different from the parent
// frame.
EXPECT_NE(nullptr, root->child_at(0)->current_frame_host()->web_ui());
EXPECT_NE(root->current_frame_host()->web_ui(),
root->child_at(0)->current_frame_host()->web_ui());
}
// Verify that a WebUI can add a subframe to another WebUI and they will be
// correctly isolated in separate SiteInstances and processes. The subframe
// also uses WebUI with bindings different than the parent to ensure this is
// successfully handled.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, WebUICrossSiteSubframe) {
GURL main_frame_url(GetWebUIURL("web-ui/page_with_blank_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
EXPECT_EQ(BINDINGS_POLICY_WEB_UI,
root->current_frame_host()->GetEnabledBindings());
EXPECT_EQ(shell()->web_contents()->GetSiteInstance(),
child->current_frame_host()->GetSiteInstance());
// Navigate the subframe using renderer-initiated navigation.
{
TestFrameNavigationObserver observer(child);
GURL child_frame_url(
GetWebUIURL("web-ui-subframe/title2.html?noxfo=true&bindings=" +
base::NumberToString(BINDINGS_POLICY_MOJO_WEB_UI)));
EXPECT_TRUE(ExecJs(shell(),
JsReplace("document.getElementById($1).src = $2;",
"test_iframe", child_frame_url),
EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */));
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(child_frame_url, observer.last_committed_url());
EXPECT_EQ(BINDINGS_POLICY_MOJO_WEB_UI,
child->current_frame_host()->GetEnabledBindings());
EXPECT_EQ(url::Origin::Create(child_frame_url),
child->current_frame_host()->GetLastCommittedOrigin());
}
EXPECT_EQ(GetWebUIURL("web-ui-subframe"),
child->current_frame_host()->GetSiteInstance()->GetSiteURL());
EXPECT_NE(root->current_frame_host()->GetSiteInstance(),
child->current_frame_host()->GetSiteInstance());
EXPECT_NE(root->current_frame_host()->GetProcess(),
child->current_frame_host()->GetProcess());
EXPECT_NE(root->current_frame_host()->web_ui(),
child->current_frame_host()->web_ui());
EXPECT_NE(root->current_frame_host()->GetEnabledBindings(),
child->current_frame_host()->GetEnabledBindings());
// Navigate once more using renderer-initiated navigation.
{
TestFrameNavigationObserver observer(child);
GURL child_frame_url(
GetWebUIURL("web-ui-subframe/title3.html?noxfo=true&bindings=" +
base::NumberToString(BINDINGS_POLICY_MOJO_WEB_UI)));
EXPECT_TRUE(ExecJs(shell(),
JsReplace("document.getElementById($1).src = $2;",
"test_iframe", child_frame_url),
EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */));
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(child_frame_url, observer.last_committed_url());
EXPECT_EQ(BINDINGS_POLICY_MOJO_WEB_UI,
child->current_frame_host()->GetEnabledBindings());
EXPECT_EQ(url::Origin::Create(child_frame_url),
child->current_frame_host()->GetLastCommittedOrigin());
}
// Navigate the subframe using browser-initiated navigation.
{
TestFrameNavigationObserver observer(child);
GURL child_frame_url(
GetWebUIURL("web-ui-subframe/title1.html?noxfo=true&bindings=" +
base::NumberToString(BINDINGS_POLICY_MOJO_WEB_UI)));
NavigateFrameToURL(child, child_frame_url);
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(child_frame_url, observer.last_committed_url());
EXPECT_EQ(BINDINGS_POLICY_MOJO_WEB_UI,
child->current_frame_host()->GetEnabledBindings());
}
}
// Verify that SiteInstance and WebUI reuse happens in subframes as well.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, WebUIReuseInSubframe) {
// Disable X-Frame-Options on all WebUIs in this test, since subframe WebUI
// reuse is expected. If the initial creation does not disable XFO, then
// subsequent navigations will fail.
factory()->set_disable_xfo(true);
GURL main_frame_url(GetWebUIURL("web-ui/page_with_iframe.html"));
EXPECT_TRUE(NavigateToURL(shell(), main_frame_url));
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
EXPECT_EQ(1U, root->child_count());
FrameTreeNode* child = root->child_at(0);
// Capture the SiteInstance and WebUI used in the first navigation to compare
// with the ones used after the reload.
scoped_refptr<SiteInstance> initial_site_instance =
child->current_frame_host()->GetSiteInstance();
WebUI* initial_web_ui = child->current_frame_host()->web_ui();
GlobalRenderFrameHostId initial_rfh_id =
child->current_frame_host()->GetGlobalId();
GURL subframe_same_site_url(GetWebUIURL("web-ui/title2.html"));
{
TestFrameNavigationObserver observer(child);
NavigateFrameToURL(child, subframe_same_site_url);
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(subframe_same_site_url, observer.last_committed_url());
}
EXPECT_EQ(initial_site_instance,
child->current_frame_host()->GetSiteInstance());
if (ShouldCreateNewHostForSameSiteSubframe()) {
EXPECT_NE(initial_web_ui, child->current_frame_host()->web_ui());
} else {
EXPECT_EQ(initial_web_ui, child->current_frame_host()->web_ui());
}
// Navigate the child frame cross-site.
GURL subframe_cross_site_url(GetWebUIURL("web-ui-subframe/title1.html"));
{
TestFrameNavigationObserver observer(child);
NavigateFrameToURL(child, subframe_cross_site_url);
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(subframe_cross_site_url, observer.last_committed_url());
}
EXPECT_NE(root->current_frame_host()->GetSiteInstance(),
child->current_frame_host()->GetSiteInstance());
EXPECT_NE(root->current_frame_host()->web_ui(),
child->current_frame_host()->web_ui());
EXPECT_NE(initial_web_ui, child->current_frame_host()->web_ui());
// Capture the new SiteInstance and WebUI of the subframe and navigate it to
// another document on the same site.
scoped_refptr<SiteInstance> second_site_instance =
child->current_frame_host()->GetSiteInstance();
WebUI* second_web_ui = child->current_frame_host()->web_ui();
GURL subframe_cross_site_url2(GetWebUIURL("web-ui-subframe/title2.html"));
{
TestFrameNavigationObserver observer(child);
NavigateFrameToURL(child, subframe_cross_site_url2);
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(subframe_cross_site_url2, observer.last_committed_url());
}
EXPECT_EQ(second_site_instance,
child->current_frame_host()->GetSiteInstance());
if (ShouldCreateNewHostForSameSiteSubframe()) {
EXPECT_NE(second_web_ui, child->current_frame_host()->web_ui());
} else {
EXPECT_EQ(second_web_ui, child->current_frame_host()->web_ui());
}
// Navigate back to the first document in the subframe, which should bring
// it back to the initial SiteInstance, but use a different RenderFrameHost
// and by that a different WebUI instance.
{
TestFrameNavigationObserver observer(child);
shell()->web_contents()->GetController().GoToOffset(-2);
observer.Wait();
EXPECT_TRUE(observer.last_navigation_succeeded());
EXPECT_EQ(subframe_same_site_url, observer.last_committed_url());
}
EXPECT_EQ(initial_site_instance,
child->current_frame_host()->GetSiteInstance());
// Use routing id comparison for the RenderFrameHost as the memory allocator
// sometime places the newly created RenderFrameHost for the back navigation
// at the same memory location as the initial one. For this reason too, it
// is not possible to check the web_ui() for inequality, since in some runs
// the memory in which two different WebUI instances of the same type are
// placed is the same.
EXPECT_NE(initial_rfh_id, child->current_frame_host()->GetGlobalId());
}
// Verify that if one WebUI does a window.open() to another WebUI, then the two
// are not sharing a BrowsingInstance, are isolated from each other, and both
// processes have bindings granted to them.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, WindowOpenWebUI) {
GURL test_url(GetWebUIURL("web-ui/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), test_url));
EXPECT_EQ(test_url, shell()->web_contents()->GetLastCommittedURL());
EXPECT_TRUE(shell()->web_contents()->GetMainFrame()->GetEnabledBindings() &
BINDINGS_POLICY_WEB_UI);
TestNavigationObserver new_contents_observer(nullptr, 1);
new_contents_observer.StartWatchingNewWebContents();
// Execute the script in isolated world since the default CSP disables eval
// which ExecJs depends on.
GURL new_tab_url(GetWebUIURL("another-web-ui/title2.html"));
EXPECT_TRUE(ExecJs(shell(), JsReplace("window.open($1);", new_tab_url),
EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */));
new_contents_observer.Wait();
EXPECT_TRUE(new_contents_observer.last_navigation_succeeded());
ASSERT_EQ(2u, Shell::windows().size());
Shell* new_shell = Shell::windows()[1];
EXPECT_EQ(new_tab_url, new_shell->web_contents()->GetLastCommittedURL());
EXPECT_TRUE(new_shell->web_contents()->GetMainFrame()->GetEnabledBindings() &
BINDINGS_POLICY_WEB_UI);
// SiteInstances should be different and unrelated due to the
// BrowsingInstance swaps on navigation.
EXPECT_NE(new_shell->web_contents()->GetMainFrame()->GetSiteInstance(),
shell()->web_contents()->GetMainFrame()->GetSiteInstance());
EXPECT_FALSE(
new_shell->web_contents()
->GetMainFrame()
->GetSiteInstance()
->IsRelatedSiteInstance(
shell()->web_contents()->GetMainFrame()->GetSiteInstance()));
EXPECT_NE(shell()->web_contents()->GetWebUI(),
new_shell->web_contents()->GetWebUI());
}
// Test to verify correctness of WebUI and process model in the following
// sequence of navigations:
// * successful navigation to WebUI
// * failed navigation to WebUI
// * failed navigation to http URL
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, WebUIFailedNavigation) {
GURL start_url(GetWebUIURL("web-ui/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), start_url));
EXPECT_EQ(start_url, shell()->web_contents()->GetLastCommittedURL());
EXPECT_EQ(BINDINGS_POLICY_WEB_UI,
shell()->web_contents()->GetMainFrame()->GetEnabledBindings());
FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryFrameTree()
.root();
GURL webui_error_url(GetWebUIURL("web-ui/error"));
EXPECT_FALSE(NavigateToURL(shell(), webui_error_url));
EXPECT_FALSE(root->current_frame_host()->web_ui());
EXPECT_EQ(0, root->current_frame_host()->GetEnabledBindings());
if (SiteIsolationPolicy::IsErrorPageIsolationEnabled(true)) {
EXPECT_EQ(root->current_frame_host()->GetSiteInstance()->GetSiteURL(),
GURL(kUnreachableWebDataURL));
}
ASSERT_TRUE(embedded_test_server()->Start());
GURL http_error_url(
embedded_test_server()->GetURL("foo.com", "/nonexistent"));
EXPECT_FALSE(NavigateToURL(shell(), http_error_url));
EXPECT_FALSE(root->current_frame_host()->web_ui());
EXPECT_EQ(0, root->current_frame_host()->GetEnabledBindings());
}
// Verify load script from chrome-untrusted:// is blocked.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest,
DisallowResourceRequestToChromeUntrusted) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL web_url(embedded_test_server()->GetURL("/title2.html"));
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>("test-host"));
EXPECT_TRUE(NavigateToURL(shell(), web_url));
EXPECT_EQ(web_url, shell()->web_contents()->GetLastCommittedURL());
const char kLoadResourceScript[] =
"new Promise((resolve) => {"
" const script = document.createElement('script');"
" script.onload = () => {"
" resolve('Script load should have failed');"
" };"
" script.onerror = () => {"
" resolve('Load failed');"
" };"
" script.src = $1;"
" document.body.appendChild(script);"
"});";
// There are no error messages in the console which is why we cannot check for
// them.
{
GURL untrusted_url(GetChromeUntrustedUIURL("test-host/script.js"));
EXPECT_EQ("Load failed",
EvalJs(shell(), JsReplace(kLoadResourceScript, untrusted_url),
EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */));
}
}
// Verify chrome-untrusted://resources can't be loaded from the Web.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, DisallowWebRequestToSharedResources) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title2.html")));
const char kLoadResourceScript[] =
"new Promise((resolve) => {"
" const script = document.createElement('script');"
" script.onload = () => {"
" resolve('Script load should have failed');"
" };"
" script.onerror = (e) => {"
" resolve('Load failed');"
" };"
" script.src = $1;"
" document.body.appendChild(script);"
"});";
GURL shared_resource_url =
GURL("chrome-untrusted://resources/mojo/mojo/public/js/bindings.js");
EXPECT_EQ("Load failed", EvalJs(shell(), JsReplace(kLoadResourceScript,
shared_resource_url)));
}
class WebUISecurityTestWithWebUIReportOnlyTrustedTypesEnabled
: public WebUISecurityTest {
public:
WebUISecurityTestWithWebUIReportOnlyTrustedTypesEnabled() {
feature_list_.InitAndEnableFeature(features::kWebUIReportOnlyTrustedTypes);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Verify Report-Only Trusted Types won't block assignment to a dangerous sink,
// but logs warning
IN_PROC_BROWSER_TEST_F(WebUISecurityTestWithWebUIReportOnlyTrustedTypesEnabled,
DoNotBlockSinkAssignmentOnReportOnlyTrustedTypes) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL test_url(GetWebUIURL("web-ui/title1.html"));
EXPECT_TRUE(NavigateToURL(shell(), test_url));
const char kDangerousSinkUse[] =
"(() => {"
" try {"
" document.body.innerHTML = 1;"
" throw 'Assignment should have blocked';"
" } catch(e) {"
" return 'Assignment blocked';"
" }"
"})();";
{
WebContentsConsoleObserver console_observer(shell()->web_contents());
console_observer.SetPattern(
"This document requires 'TrustedHTML' assignment.");
EXPECT_EQ("Assignment blocked",
EvalJs(shell(), kDangerousSinkUse, EXECUTE_SCRIPT_DEFAULT_OPTIONS,
1 /* world_id */));
console_observer.Wait();
}
}
namespace {
class UntrustedSourceWithCorsSupport : public URLDataSource {
public:
static std::unique_ptr<UntrustedSourceWithCorsSupport> CreateForHost(
std::string host) {
std::string source_name = base::StrCat(
{kChromeUIUntrustedScheme, url::kStandardSchemeSeparator, host, "/"});
return std::make_unique<UntrustedSourceWithCorsSupport>(source_name);
}
explicit UntrustedSourceWithCorsSupport(std::string name) : name_(name) {}
UntrustedSourceWithCorsSupport& operator=(
const UntrustedSourceWithCorsSupport&) = delete;
UntrustedSourceWithCorsSupport(const UntrustedSourceWithCorsSupport&) =
delete;
~UntrustedSourceWithCorsSupport() override = default;
// URLDataSource:
std::string GetSource() override { return name_; }
std::string GetAccessControlAllowOriginForOrigin(
const std::string& origin) override {
return origin;
}
std::string GetMimeType(const std::string& path) override {
return "text/html";
}
void StartDataRequest(const GURL& url,
const WebContents::Getter& wc_getter,
GotDataCallback callback) override {
std::string dummy_html = "<html><body>dummy</body></html>";
scoped_refptr<base::RefCountedString> response =
base::RefCountedString::TakeString(&dummy_html);
std::move(callback).Run(response.get());
}
private:
std::string name_;
};
enum FetchMode { SAME_ORIGIN, CORS, NO_CORS };
EvalJsResult PerformFetch(Shell* shell,
const GURL& fetch_url,
FetchMode fetch_mode = FetchMode::CORS) {
std::string fetch_mode_string;
switch (fetch_mode) {
case SAME_ORIGIN:
fetch_mode_string = "same-origin";
break;
case CORS:
fetch_mode_string = "cors";
break;
case NO_CORS:
fetch_mode_string = "no-cors";
break;
default:
NOTREACHED();
}
const char kFetchRequestScript[] =
"fetch($1, {mode: $2}).then("
" response => 'success',"
" error => error.message"
");";
return EvalJs(shell,
JsReplace(kFetchRequestScript, fetch_url, fetch_mode_string),
EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */);
}
} // namespace
// Verify fetch request from web pages to chrome-untrusted:// is blocked,
// because web pages don't have WebUIURLLoaderFactory for chrome-untrusted://
// scheme.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest,
DisallowWebPageFetchRequestToChromeUntrusted) {
const GURL untrusted_url = GURL("chrome-untrusted://test/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url.host()));
ASSERT_TRUE(embedded_test_server()->Start());
const GURL web_url = embedded_test_server()->GetURL("/title2.html");
EXPECT_TRUE(NavigateToURL(shell(), web_url));
{
DevToolsInspectorLogWatcher log_watcher(shell()->web_contents());
EXPECT_EQ("Failed to fetch",
PerformFetch(shell(), untrusted_url, FetchMode::CORS));
log_watcher.FlushAndStopWatching();
EXPECT_EQ(log_watcher.last_message(),
"Failed to load resource: net::ERR_UNKNOWN_URL_SCHEME");
}
{
DevToolsInspectorLogWatcher log_watcher(shell()->web_contents());
EXPECT_EQ("Failed to fetch",
PerformFetch(shell(), untrusted_url, FetchMode::NO_CORS));
log_watcher.FlushAndStopWatching();
EXPECT_EQ(log_watcher.last_message(),
"Failed to load resource: net::ERR_UNKNOWN_URL_SCHEME");
}
}
// Verify a chrome-untrusted:// document can fetch itself.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, ChromeUntrustedFetchRequestToSelf) {
const GURL untrusted_url = GURL("chrome-untrusted://test/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url.host()));
EXPECT_TRUE(NavigateToURL(shell(), untrusted_url));
EXPECT_EQ("success",
PerformFetch(shell(), untrusted_url, FetchMode::SAME_ORIGIN));
}
// Verify cross-origin fetch request from a chrome-untrusted:// page to another
// chrome-untrusted:// page is blocked by the default "default-src 'self'"
// Content Security Policy on URLDataSource.
IN_PROC_BROWSER_TEST_F(
WebUISecurityTest,
DisallowCrossOriginFetchRequestToChromeUntrustedByDefault) {
const GURL untrusted_url1 = GURL("chrome-untrusted://test1/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url1.host()));
const GURL untrusted_url2 = GURL("chrome-untrusted://test2/title2.html");
URLDataSource::Add(
shell()->web_contents()->GetBrowserContext(),
UntrustedSourceWithCorsSupport::CreateForHost(untrusted_url2.host()));
EXPECT_TRUE(NavigateToURL(shell(), untrusted_url1));
{
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ("Failed to fetch",
PerformFetch(shell(), untrusted_url2, FetchMode::CORS));
console_observer.Wait();
EXPECT_EQ(console_observer.GetMessageAt(0),
base::StringPrintf(
"Refused to connect to '%s' because it violates the "
"following Content Security Policy directive: \"default-src "
"'self'\". Note that 'connect-src' was not explicitly set, "
"so 'default-src' is used as a fallback.\n",
untrusted_url2.spec().c_str()));
}
{
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ("Failed to fetch",
PerformFetch(shell(), untrusted_url2, FetchMode::NO_CORS));
console_observer.Wait();
EXPECT_EQ(console_observer.GetMessageAt(0),
base::StringPrintf(
"Refused to connect to '%s' because it violates the "
"following Content Security Policy directive: \"default-src "
"'self'\". Note that 'connect-src' was not explicitly set, "
"so 'default-src' is used as a fallback.\n",
untrusted_url2.spec().c_str()));
}
}
// Verify cross-origin fetch request from a chrome-untrusted:// page to another
// chrome-untrusted:// page succeeds if Content Security Policy allows it.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest,
CrossOriginFetchRequestToChromeUntrusted) {
TestUntrustedDataSourceHeaders headers;
headers.default_src = "default-src chrome-untrusted://test2;";
const GURL untrusted_url1 = GURL("chrome-untrusted://test1/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url1.host(),
headers));
const GURL untrusted_url2 = GURL("chrome-untrusted://test2/title2.html");
URLDataSource::Add(
shell()->web_contents()->GetBrowserContext(),
UntrustedSourceWithCorsSupport::CreateForHost(untrusted_url2.host()));
EXPECT_TRUE(NavigateToURL(shell(), untrusted_url1));
EXPECT_EQ("success", PerformFetch(shell(), untrusted_url2, FetchMode::CORS));
EXPECT_EQ("success",
PerformFetch(shell(), untrusted_url2, FetchMode::NO_CORS));
}
// Verify fetch request from a chrome-untrusted:// page to a chrome:// page
// is blocked because chrome-untrusted:// pages don't have WebUIURLLoaderFactory
// for chrome:// scheme, even if CSP allows this.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest,
DisallowChromeUntrustedFetchRequestToChrome) {
TestUntrustedDataSourceHeaders headers;
headers.default_src = "default-src chrome://webui;";
const GURL untrusted_url = GURL("chrome-untrusted://test1/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url.host(),
headers));
const GURL chrome_url = GURL("chrome://webui/title2.html");
EXPECT_TRUE(NavigateToURL(shell(), untrusted_url));
{
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ("Failed to fetch",
PerformFetch(shell(), chrome_url, FetchMode::CORS));
console_observer.Wait();
EXPECT_EQ(
console_observer.GetMessageAt(0),
base::StringPrintf(
"Fetch API cannot load %s. URL scheme \"chrome\" is not supported.",
chrome_url.spec().c_str()));
}
{
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ("Failed to fetch",
PerformFetch(shell(), chrome_url, FetchMode::NO_CORS));
console_observer.Wait();
EXPECT_EQ(
console_observer.GetMessageAt(0),
base::StringPrintf(
"Fetch API cannot load %s. URL scheme \"chrome\" is not supported.",
chrome_url.spec().c_str()));
}
}
namespace {
EvalJsResult PerformXHRRequest(Shell* shell, const GURL& xhr_url) {
const char kXHRRequestScript[] =
"new Promise((resolve) => {"
" const xhr = new XMLHttpRequest();"
" xhr.open('GET', $1);"
" xhr.onload = () => resolve('success');"
" xhr.onerror = progress_event => resolve(progress_event.type);"
" xhr.send();"
"}); ";
return EvalJs(shell, JsReplace(kXHRRequestScript, xhr_url),
EXECUTE_SCRIPT_DEFAULT_OPTIONS, 1 /* world_id */);
}
} // namespace
// Verify XHR request from web pages to chrome-untrusted:// is blocked, because
// web pages don't have WebUIURLLoader required to load chrome-untrusted://
// resources.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest,
DisallowWebPageXHRRequestToChromeUntrusted) {
const GURL untrusted_url = GURL("chrome-untrusted://test/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url.host()));
ASSERT_TRUE(embedded_test_server()->Start());
const GURL web_url = embedded_test_server()->GetURL("/title2.html");
EXPECT_TRUE(NavigateToURL(shell(), web_url));
DevToolsInspectorLogWatcher log_watcher(shell()->web_contents());
EXPECT_EQ("error", PerformXHRRequest(shell(), untrusted_url));
log_watcher.FlushAndStopWatching();
EXPECT_EQ(log_watcher.last_message(),
"Failed to load resource: net::ERR_UNKNOWN_URL_SCHEME");
}
// Verify a chrome-untrusted:// document can XHR itself.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest,
AllowChromeUntrustedXHRRequestToSelf) {
const GURL untrusted_url = GURL("chrome-untrusted://test/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url.host()));
EXPECT_TRUE(NavigateToURL(shell(), untrusted_url));
EXPECT_EQ("success", PerformXHRRequest(shell(), untrusted_url));
}
// Verify cross-origin XHR request from a chrome-untrusted:// page to another
// chrome-untrusted:// page is blocked by "default-src 'self';" Content Security
// Policy.
IN_PROC_BROWSER_TEST_F(
WebUISecurityTest,
DisallowCrossOriginXHRRequestToChromeUntrustedByDefault) {
const GURL untrusted_url1 = GURL("chrome-untrusted://test1/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url1.host()));
const GURL untrusted_url2 = GURL("chrome-untrusted://test2/");
URLDataSource::Add(
shell()->web_contents()->GetBrowserContext(),
UntrustedSourceWithCorsSupport::CreateForHost(untrusted_url2.host()));
EXPECT_TRUE(NavigateToURL(shell(), untrusted_url1));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ("error", PerformXHRRequest(shell(), untrusted_url2));
console_observer.Wait();
EXPECT_EQ(console_observer.GetMessageAt(0),
base::StringPrintf(
"Refused to connect to '%s' because it violates the "
"following Content Security Policy directive: \"default-src "
"'self'\". Note that 'connect-src' was not explicitly set, "
"so 'default-src' is used as a fallback.\n",
untrusted_url2.spec().c_str()));
}
// Verify cross-origin XHR request from a chrome-untrusted:// page to another
// chrome-untrusted:// page is successful, if Content Security Policy allows it,
// and the requested resource presents an Access-Control-Allow-Origin header.
IN_PROC_BROWSER_TEST_F(
WebUISecurityTest,
CrossOriginXHRRequestToChromeUntrustedIfContenSecurityPolicyAllowsIt) {
TestUntrustedDataSourceHeaders headers;
headers.default_src = "default-src chrome-untrusted://test2;";
const GURL untrusted_url1 = GURL("chrome-untrusted://test1/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url1.host(),
headers));
const GURL untrusted_url2 = GURL("chrome-untrusted://test2/");
URLDataSource::Add(
shell()->web_contents()->GetBrowserContext(),
UntrustedSourceWithCorsSupport::CreateForHost(untrusted_url2.host()));
EXPECT_TRUE(NavigateToURL(shell(), untrusted_url1));
EXPECT_EQ("success", PerformXHRRequest(shell(), untrusted_url2));
}
// Verify XHR request from a chrome-untrusted:// page to a chrome:// page is
// blocked, even if CSP allows this.
IN_PROC_BROWSER_TEST_F(WebUISecurityTest,
DisallowChromeUntrustedXHRRequestToChrome) {
TestUntrustedDataSourceHeaders headers;
headers.default_src = "default-src chrome://webui;";
const GURL untrusted_url = GURL("chrome-untrusted://test1/title1.html");
WebUIConfigMap::GetInstance().AddUntrustedWebUIConfig(
std::make_unique<ui::TestUntrustedWebUIConfig>(untrusted_url.host(),
headers));
const GURL chrome_url = GURL("chrome://webui/title2.html");
EXPECT_TRUE(NavigateToURL(shell(), untrusted_url));
WebContentsConsoleObserver console_observer(shell()->web_contents());
EXPECT_EQ("error", PerformXHRRequest(shell(), chrome_url));
console_observer.Wait();
EXPECT_EQ(console_observer.GetMessageAt(0),
base::StringPrintf("Not allowed to load local resource: %s",
chrome_url.spec().c_str()));
}
// Test that there's no crash when a navigation to a WebUI page reuses an
// inactive RenderViewHost. Previously, this led to a browser process crash in
// WebUI pages that use MojoWebUIController, which tried to use the
// RenderViewHost's GetMainFrame() when it was invalid in RenderViewCreated().
// See https://crbug.com/627027.
// Flaky on Mac. See https://crbug.com/1044335.
#if BUILDFLAG(IS_MAC)
#define MAYBE_ReuseRVHWithWebUI DISABLED_ReuseRVHWithWebUI
#else
#define MAYBE_ReuseRVHWithWebUI ReuseRVHWithWebUI
#endif
IN_PROC_BROWSER_TEST_F(WebUISecurityTest, MAYBE_ReuseRVHWithWebUI) {
ASSERT_TRUE(embedded_test_server()->Start());
// Visit a WebUI page with bindings.
const GURL webui_url(
GetWebUIURL("web-ui/title1.html?bindings=" +
base::NumberToString(BINDINGS_POLICY_MOJO_WEB_UI)));
ASSERT_TRUE(NavigateToURL(shell(), webui_url));
// window.open a new tab. This will keep the WebUI page's process alive
// once we navigate away from it.
ShellAddedObserver new_shell_observer;
ASSERT_TRUE(ExecuteScript(shell()->web_contents(),
JsReplace("window.open($1);", webui_url)));
Shell* new_shell = new_shell_observer.GetShell();
WebContents* new_contents = new_shell->web_contents();
EXPECT_TRUE(WaitForLoadStop(new_contents));
RenderFrameHost* webui_rfh = new_contents->GetMainFrame();
EXPECT_EQ(webui_rfh->GetLastCommittedURL(), webui_url);
EXPECT_TRUE(BINDINGS_POLICY_MOJO_WEB_UI & webui_rfh->GetEnabledBindings());
RenderViewHostImpl* webui_rvh =
static_cast<RenderViewHostImpl*>(webui_rfh->GetRenderViewHost());
// Navigate to another page in the opened tab.
const GURL nonwebui_url(embedded_test_server()->GetURL("/title2.html"));
ASSERT_TRUE(NavigateToURL(new_shell, nonwebui_url));
EXPECT_NE(webui_rvh, new_contents->GetMainFrame()->GetRenderViewHost());
// Go back in the opened tab. This should finish without crashing and should
// reuse the old RenderViewHost.
TestNavigationObserver back_load_observer(new_contents);
new_contents->GetController().GoBack();
back_load_observer.Wait();
EXPECT_EQ(webui_rvh, new_contents->GetMainFrame()->GetRenderViewHost());
EXPECT_TRUE(webui_rvh->IsRenderViewLive());
EXPECT_TRUE(BINDINGS_POLICY_MOJO_WEB_UI &
new_contents->GetMainFrame()->GetEnabledBindings());
}
class WebUIBrowserSideSecurityTest : public WebUISecurityTest {
public:
WebUIBrowserSideSecurityTest() = default;
protected:
void SetUpCommandLine(base::CommandLine* command_line) override {
// Disable Web Security to skip renderer-side checks so that we can test
// browser-side checks.
command_line->AppendSwitch(switches::kDisableWebSecurity);
}
};
IN_PROC_BROWSER_TEST_F(WebUIBrowserSideSecurityTest,
DenyWebAccessToSharedResources) {
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL("/title2.html")));
const char kLoadResourceScript[] =
"new Promise((resolve) => {"
" const script = document.createElement('script');"
" script.onload = () => {"
" resolve('Script load should have failed');"
" };"
" script.onerror = (e) => {"
" resolve('Load failed');"
" };"
" script.src = $1;"
" document.body.appendChild(script);"
"});";
DevToolsInspectorLogWatcher log_watcher(shell()->web_contents());
GURL shared_resource_url =
GURL("chrome-untrusted://resources/mojo/mojo/public/js/bindings.js");
EXPECT_EQ("Load failed", EvalJs(shell(), JsReplace(kLoadResourceScript,
shared_resource_url)));
log_watcher.FlushAndStopWatching();
EXPECT_EQ(log_watcher.last_message(),
"Failed to load resource: net::ERR_UNKNOWN_URL_SCHEME");
}
} // namespace content