blob: 50c63fb3065aec50c5f82c2a75e0173ad13f61f5 [file] [log] [blame]
// Copyright 2021 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 "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/version_info/channel.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/process_map.h"
#include "extensions/common/features/feature.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/test/test_extension_dir.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace extensions {
namespace {
class CrossOriginIsolationTest : public ExtensionBrowserTest {
public:
CrossOriginIsolationTest() = default;
~CrossOriginIsolationTest() override = default;
CrossOriginIsolationTest(const CrossOriginIsolationTest&) = delete;
CrossOriginIsolationTest& operator=(const CrossOriginIsolationTest&) = delete;
const Extension* LoadExtension(TestExtensionDir& dir,
const char* coep_value,
const char* coop_value) {
constexpr char kManifestTemplate[] = R"(
{
"background": {
"scripts": ["background.js"]
},
"manifest_version": 2,
"name": "CrossOriginIsolation",
"version": "1.1",
"cross_origin_embedder_policy": {
"value": "%s"
},
"cross_origin_opener_policy": {
"value": "%s"
},
"web_accessible_resources": ["test.html"]
}
)";
dir.WriteManifest(
base::StringPrintf(kManifestTemplate, coep_value, coop_value));
dir.WriteFile(FILE_PATH_LITERAL("background.js"), "");
dir.WriteFile(FILE_PATH_LITERAL("test.html"), "");
return ExtensionBrowserTest::LoadExtension(dir.UnpackedPath());
}
bool IsCrossOriginIsolated(content::RenderFrameHost* host) {
bool result = false;
if (!content::ExecuteScriptAndExtractBool(
host, "window.domAutomationController.send(crossOriginIsolated)",
&result)) {
ADD_FAILURE() << "Script execution failed";
return false;
}
return result;
}
content::RenderFrameHost* GetBackgroundRenderFrameHost(
const Extension& extension) {
ExtensionHost* host =
ProcessManager::Get(profile())->GetBackgroundHostForExtension(
extension.id());
return host ? host->main_frame_host() : nullptr;
}
private:
// TODO(crbug.com/1199491): Remove once the related manifest keys are
// available on Stable.
ScopedCurrentChannel scoped_channel_{version_info::Channel::UNKNOWN};
};
// Tests that extensions can opt into cross origin isolation.
IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, CrossOriginIsolation) {
// Set the maximum number of processes to 1. This is a soft limit that
// we're allowed to exceed if processes *must* not share, which is the case
// for cross-origin-isolated origins vs non-cross-origin-isolated
// origins.
content::RenderProcessHost::SetMaxRendererProcessCount(1);
TestExtensionDir coi_test_dir;
const Extension* coi_extension =
LoadExtension(coi_test_dir, "require-corp", "same-origin");
ASSERT_TRUE(coi_extension);
content::RenderFrameHost* coi_background_rfh =
GetBackgroundRenderFrameHost(*coi_extension);
ASSERT_TRUE(coi_background_rfh);
EXPECT_TRUE(IsCrossOriginIsolated(coi_background_rfh));
TestExtensionDir non_coi_test_dir;
const Extension* non_coi_extension =
LoadExtension(non_coi_test_dir, "unsafe-none", "same-origin");
ASSERT_TRUE(non_coi_extension);
content::RenderFrameHost* non_coi_background_rfh =
GetBackgroundRenderFrameHost(*non_coi_extension);
ASSERT_TRUE(non_coi_background_rfh);
EXPECT_FALSE(IsCrossOriginIsolated(non_coi_background_rfh));
// A cross-origin-isolated extension should not share a process with a
// non-cross-origin-isolated one.
EXPECT_NE(coi_background_rfh->GetProcess(),
non_coi_background_rfh->GetProcess());
}
// Tests that a web accessible frame from a cross origin isolated extension is
// not cross origin isolated.
IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest, WebAccessibleFrame) {
ASSERT_TRUE(embedded_test_server()->Start());
// Set the maximum number of processes to 1. This is a soft limit that
// we're allowed to exceed if processes *must* not share, which is the case
// for cross-origin-isolated origins vs non-cross-origin-isolated
// origins.
content::RenderProcessHost::SetMaxRendererProcessCount(1);
TestExtensionDir coi_test_dir;
const Extension* coi_extension =
LoadExtension(coi_test_dir, "require-corp", "same-origin");
ASSERT_TRUE(coi_extension);
content::RenderFrameHost* coi_background_rfh =
GetBackgroundRenderFrameHost(*coi_extension);
ASSERT_TRUE(coi_background_rfh);
EXPECT_TRUE(IsCrossOriginIsolated(coi_background_rfh));
GURL extension_test_url = coi_extension->GetResourceURL("test.html");
ui_test_utils::NavigateToURL(browser(), extension_test_url);
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
EXPECT_TRUE(IsCrossOriginIsolated(web_contents->GetMainFrame()));
EXPECT_EQ(web_contents->GetMainFrame()->GetProcess(),
coi_background_rfh->GetProcess());
// Load test.html as a web accessible resource inside a web frame.
ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/iframe_blank.html"));
ASSERT_TRUE(
content::NavigateIframeToURL(web_contents, "test", extension_test_url));
content::RenderFrameHost* extension_iframe =
content::ChildFrameAt(web_contents->GetMainFrame(), 0);
ASSERT_TRUE(extension_iframe);
EXPECT_EQ(extension_test_url, extension_iframe->GetLastCommittedURL());
// The extension iframe is embedded within a web frame and won't be cross
// origin isolated. It should also not share a process with the extension's
// cross origin isolated context, nor with the web frame it's embedded in.
EXPECT_FALSE(IsCrossOriginIsolated(extension_iframe));
EXPECT_NE(extension_iframe->GetProcess(), coi_background_rfh->GetProcess());
EXPECT_NE(extension_iframe->GetProcess(),
web_contents->GetMainFrame()->GetProcess());
// Check ProcessManager APIs to ensure they work correctly for the case where
// an extension has multiple processes for the same profile.
{
ProcessManager* process_manager = ProcessManager::Get(profile());
ASSERT_TRUE(process_manager);
std::set<content::RenderFrameHost*> extension_hosts =
process_manager->GetRenderFrameHostsForExtension(coi_extension->id());
EXPECT_THAT(extension_hosts, ::testing::UnorderedElementsAre(
coi_background_rfh, extension_iframe));
EXPECT_EQ(coi_extension, process_manager->GetExtensionForRenderFrameHost(
coi_background_rfh));
EXPECT_EQ(coi_extension, process_manager->GetExtensionForRenderFrameHost(
extension_iframe));
}
// Check ProcessMap APIs to ensure they work correctly for the case where an
// extension has multiple processes for the same profile.
{
ProcessMap* process_map = ProcessMap::Get(profile());
ASSERT_TRUE(process_map);
EXPECT_TRUE(process_map->Contains(
coi_extension->id(), coi_background_rfh->GetProcess()->GetID()));
EXPECT_TRUE(process_map->Contains(coi_extension->id(),
extension_iframe->GetProcess()->GetID()));
GURL* url = nullptr;
EXPECT_EQ(
Feature::BLESSED_EXTENSION_CONTEXT,
process_map->GetMostLikelyContextType(
coi_extension, coi_background_rfh->GetProcess()->GetID(), url));
EXPECT_EQ(Feature::BLESSED_EXTENSION_CONTEXT,
process_map->GetMostLikelyContextType(
coi_extension, extension_iframe->GetProcess()->GetID(), url));
}
}
// Tests certain extension APIs which retrieve in-process extension windows.
// Test these for a cross origin isolated extension with non-cross origin
// isolated contexts.
IN_PROC_BROWSER_TEST_F(CrossOriginIsolationTest,
WebAccessibleFrame_WindowApis) {
ASSERT_TRUE(embedded_test_server()->Start());
TestExtensionDir coi_test_dir;
const Extension* coi_extension =
LoadExtension(coi_test_dir, "require-corp", "same-origin");
ASSERT_TRUE(coi_extension);
content::RenderFrameHost* coi_background_rfh =
GetBackgroundRenderFrameHost(*coi_extension);
ASSERT_TRUE(coi_background_rfh);
GURL extension_test_url = coi_extension->GetResourceURL("test.html");
ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/iframe_blank.html"));
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(
content::NavigateIframeToURL(web_contents, "test", extension_test_url));
content::RenderFrameHost* extension_iframe =
content::ChildFrameAt(web_contents->GetMainFrame(), 0);
ASSERT_TRUE(extension_iframe);
content::RenderFrameHost* extension_tab =
ui_test_utils::NavigateToURLWithDisposition(
browser(), extension_test_url,
WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
ASSERT_TRUE(extension_tab);
// getBackgroundPage API.
{
auto test_get_background_page = [](content::RenderFrameHost* host,
bool expect_background_page) {
constexpr char kScript[] = R"(
const expectBackgroundPage = %s;
const hasBackgroundPage = !!chrome.extension.getBackgroundPage();
window.domAutomationController.send(
hasBackgroundPage === expectBackgroundPage);
)";
bool result = false;
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
host,
base::StringPrintf(kScript,
expect_background_page ? "true" : "false"),
&result));
EXPECT_TRUE(result);
};
test_get_background_page(coi_background_rfh, true);
test_get_background_page(extension_tab, true);
// The extension iframe should be non-cross origin isolated and hence in a
// different process than the extension background page. Since the API can
// only retrieve the background page if it's in the same process,
// getBackgroundPage should return null here.
test_get_background_page(extension_iframe, false);
}
// getViews API.
{
auto verify_get_tabs = [](content::RenderFrameHost* host,
int num_tabs_expected) {
constexpr char kScript[] = R"(
const numTabsExpected = %d;
const tabs = chrome.extension.getViews({type: 'tab'});
window.domAutomationController.send(tabs.length === numTabsExpected);
)";
bool result = false;
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
host, base::StringPrintf(kScript, num_tabs_expected), &result));
EXPECT_TRUE(result);
};
verify_get_tabs(coi_background_rfh, 1);
verify_get_tabs(extension_tab, 1);
// The extension iframe should be non-cross origin isolated and hence in a
// different process than the background page. Since the API can only
// retrieve windows in the same process, no windows will be returned.
verify_get_tabs(extension_iframe, 0);
}
}
} // namespace
} // namespace extensions