blob: cd775a8be23a052136e120df97ff6062aa79562c [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/process_map.h"
#include <memory>
#include <string_view>
#include <vector>
#include "base/strings/stringprintf.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#include "extensions/common/constants.h"
#include "extensions/common/mojom/context_type.mojom.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/test_extension_dir.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace extensions {
class ProcessMapBrowserTest : public ExtensionBrowserTest {
public:
ProcessMapBrowserTest() = default;
ProcessMapBrowserTest(const ProcessMapBrowserTest&) = delete;
ProcessMapBrowserTest& operator=(const ProcessMapBrowserTest&) = delete;
~ProcessMapBrowserTest() override = default;
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
ASSERT_TRUE(embedded_test_server()->Start());
}
content::RenderProcessHost& GetActiveMainFrameProcess() {
return *GetActiveWebContents()->GetPrimaryMainFrame()->GetProcess();
}
int GetActiveMainFrameProcessID() {
return GetActiveMainFrameProcess().GetDeprecatedID();
}
// Adds a new extension with the given `extension_name` and host permission to
// the given `host_pattern`.
const Extension* AddExtensionWithHostPermission(
std::string_view extension_name,
std::string_view host_pattern) {
static constexpr char kManifestTemplate[] =
R"({
"name": "%s",
"manifest_version": 3,
"version": "0.1",
"host_permissions": ["%s"]
})";
auto extension_dir = std::make_unique<TestExtensionDir>();
extension_dir->WriteManifest(base::StringPrintf(
kManifestTemplate, extension_name.data(), host_pattern.data()));
const Extension* extension = LoadExtension(extension_dir->UnpackedPath());
extension_dirs_.push_back(std::move(extension_dir));
return extension;
}
// Adds a new extension with the given `extension_name` and a content script
// that runs on `content_script_pattern`, sending a message when the script
// injects.
const Extension* AddExtensionWithContentScript(
std::string_view extension_name,
std::string_view content_script_pattern) {
static constexpr char kManifestTemplate[] =
R"({
"name": "%s",
"manifest_version": 3,
"version": "0.1",
"content_scripts": [{
"matches": ["%s"],
"js": ["script.js"]
}]
})";
auto extension_dir = std::make_unique<TestExtensionDir>();
extension_dir->WriteManifest(
base::StringPrintf(kManifestTemplate, extension_name.data(),
content_script_pattern.data()));
extension_dir->WriteFile(FILE_PATH_LITERAL("script.js"),
"chrome.test.sendMessage('script injected');");
const Extension* extension = LoadExtension(extension_dir->UnpackedPath());
extension_dirs_.push_back(std::move(extension_dir));
return extension;
}
void ExecuteUserScriptInActiveTab(const ExtensionId& extension_id) {
base::RunLoop run_loop;
content::WebContents* web_contents = GetActiveWebContents();
// TODO(crbug.com/40262660): Add a utility method for user script
// injection in browser tests.
ScriptExecutor script_executor(web_contents);
std::vector<mojom::JSSourcePtr> sources;
sources.push_back(
mojom::JSSource::New("document.title = 'injected';", GURL()));
script_executor.ExecuteScript(
mojom::HostID(mojom::HostID::HostType::kExtensions, extension_id),
mojom::CodeInjection::NewJs(mojom::JSInjection::New(
std::move(sources), mojom::ExecutionWorld::kUserScript,
/*world_id=*/std::nullopt,
blink::mojom::WantResultOption::kWantResult,
blink::mojom::UserActivationOption::kDoNotActivate,
blink::mojom::PromiseResultOption::kAwait)),
ScriptExecutor::SPECIFIED_FRAMES, {ExtensionApiFrameIdMap::kTopFrameId},
mojom::MatchOriginAsFallbackBehavior::kNever,
mojom::RunLocation::kDocumentIdle, ScriptExecutor::DEFAULT_PROCESS,
GURL() /* webview_src */,
base::IgnoreArgs<std::vector<ScriptExecutor::FrameResult>>(
run_loop.QuitWhenIdleClosure()));
run_loop.Run();
EXPECT_EQ(u"injected", web_contents->GetTitle());
}
// Helper function to define the test body for tests that use
// AddExtensionWithSandboxedWebpage, defined below so it's near the tests that
// use it.
void VerifyWhetherSubframesAreIsolated(
const GURL& frame_url,
const std::string& content,
bool expect_subframes_isolated_from_each_other,
bool expect_sandboxed_subframe_isolated_from_extension_page,
bool expect_non_sandboxed_subframe_isolated_from_extension_page);
// Helper function for data: and srcdoc tests regarding resource access from
// sandboxed frames, defined below so it's near the tests that use it.
// Expects that `parent_script_template` contains a `%s` which this function
// will replace with the extension origin. `is_subframe_data_url` should be
// true if the `parent_script_template` is for a data url frame, so that this
// function doesn't have to infer that from the template.
void VerifySandboxedSubframeHasResourceAccessButMaybeApiAccess(
const Extension* extension,
std::string_view parent_script,
const bool is_subframe_data_url,
const bool expects_api_access);
// Adds a new extension with a parent frame that in turn loads `url` in two
// iframes, one of which is sandboxed. If `url` is about:srcdoc, then the
// srcdoc attribute is set instead using the value contained in `content`.
const Extension* AddExtensionWithSandboxedWebpage(
const GURL& url,
const std::string& content) {
static constexpr char kManifest[] =
R"({
"name": "Sandboxed Page",
"manifest_version": 3,
"version": "0.1"
})";
auto extension_dir = std::make_unique<TestExtensionDir>();
extension_dir->WriteManifest(kManifest);
std::string page_content;
if (url.IsAboutSrcdoc()) {
page_content = base::StringPrintf(
R"(<html>
<iframe sandbox srcdoc="%s"></iframe>
<iframe srcdoc="%s"></iframe>
</html>)",
content.c_str(), content.c_str());
} else {
page_content = base::StringPrintf(
R"(<html>
<iframe sandbox src="%s"></iframe>
<iframe src="%s"></iframe>
</html>)",
url.spec().c_str(), url.spec().c_str());
}
extension_dir->WriteFile(FILE_PATH_LITERAL("parent.html"), page_content);
const Extension* extension = LoadExtension(extension_dir->UnpackedPath());
extension_dirs_.push_back(std::move(extension_dir));
return extension;
}
// Create an extension with a page that loads a non-extension page, which in
// turn contains an about:srcdoc subframe.
const Extension* AddExtensionWithNonExtensionSubframeWithSrcdocSubframe(
bool srcdoc_is_sandboxed) {
static constexpr char kManifest[] =
R"({
"name": "Sandboxed Page",
"manifest_version": 3,
"version": "0.1"
})";
auto extension_dir = std::make_unique<TestExtensionDir>();
extension_dir->WriteManifest(kManifest);
GURL non_extension_url = embedded_test_server()->GetURL(
"example.com", srcdoc_is_sandboxed ? "/iframe_sandboxed_srcdoc.html"
: "/iframe_srcdoc.html");
const char kPageContentTemplate[] =
R"(<html>
<body>
<iframe src="%s"></iframe>
</body>
</html>)";
extension_dir->WriteFile(
FILE_PATH_LITERAL("parent.html"),
base::StringPrintf(kPageContentTemplate,
non_extension_url.spec().c_str()));
// Including a non-web-accessible extension resource for testing access.
extension_dir->WriteFile(FILE_PATH_LITERAL("data.json"),
"{ \"answer\" : 42 }");
const Extension* extension = LoadExtension(extension_dir->UnpackedPath());
extension_dirs_.push_back(std::move(extension_dir));
return extension;
}
// Adds an extension with a page with a sandboxed subframe (that can be
// manipulated by individual tests), and a simple resource that the subframe
// might load.
const Extension* AddExtensionWithResource() {
static constexpr char kManifest[] =
R"({
"name": "Page With Sandboxed Subframe and Resource To Load",
"manifest_version": 3,
"version": "0.1"
})";
auto extension_dir = std::make_unique<TestExtensionDir>();
extension_dir->WriteManifest(kManifest);
std::string page_content =
R"(<html>
<iframe id='test_frame' sandbox="allow-scripts"></iframe>
</html>)";
extension_dir->WriteFile(FILE_PATH_LITERAL("parent.html"), page_content);
std::string resource_js = R"(let foo = "bar";)";
extension_dir->WriteFile(FILE_PATH_LITERAL("resource.js"), resource_js);
std::string page_requesting_resource_content =
R"(<script src="resource.js"></script>)";
extension_dir->WriteFile(FILE_PATH_LITERAL("page_requesting_resource.html"),
page_requesting_resource_content);
const Extension* extension = LoadExtension(extension_dir->UnpackedPath());
extension_dirs_.push_back(std::move(extension_dir));
return extension;
}
// Create a pair of nested extensions, where `page.html` from the first
// extension is nested inside `parent.html` from the second extension.
std::pair<const Extension*, const Extension*> AddNestedExtensions() {
const Extension* extension1 = nullptr;
{
static constexpr char kManifestTemplate[] =
R"({
"name": "Extension1",
"manifest_version": 3,
"version": "0.1",
"web_accessible_resources": [
{
"resources": [ "page.html" ],
"matches": [ "%s://*/*" ]
}
]
})";
auto extension_dir = std::make_unique<TestExtensionDir>();
extension_dir->WriteManifest(
base::StringPrintf(kManifestTemplate, kExtensionScheme));
extension_dir->WriteFile(FILE_PATH_LITERAL("page.html"),
R"(<html>E1</html>)");
extension1 = LoadExtension(extension_dir->UnpackedPath());
extension_dirs_.push_back(std::move(extension_dir));
}
GURL e1_page_url = extension1->GetResourceURL("page.html");
const Extension* extension2 = nullptr;
{
static constexpr char kManifest[] =
R"({
"name": "Extension2",
"manifest_version": 3,
"version": "0.1"
})";
auto extension_dir = std::make_unique<TestExtensionDir>();
extension_dir->WriteManifest(kManifest);
static constexpr char kPageContent[] =
R"(<html>E2
<iframe sandbox="allow-scripts" src="%s"></iframe>
</html>)";
extension_dir->WriteFile(
FILE_PATH_LITERAL("parent.html"),
base::StringPrintf(kPageContent, e1_page_url.spec().c_str()));
// Create a page that is not listed as a web_accessible_resource.
extension_dir->WriteFile(FILE_PATH_LITERAL("private_page.html"),
R"(<html>E2 Private</html>)");
extension2 = LoadExtension(extension_dir->UnpackedPath());
extension_dirs_.push_back(std::move(extension_dir));
}
return std::make_pair(extension1, extension2);
}
// Adds a new extension with two sandboxed frames, `sandboxed.html` and
// `sandboxed2.html`, and a parent page, `parent.html` to host it.
// Having two manifest-sandboxed pages facilitates testing that there is
// just one sandbox process per extension.
const Extension* AddExtensionWithSandboxedFrame() {
static constexpr char kManifest[] =
R"({
"name": "Sandboxed Page",
"manifest_version": 3,
"version": "0.1",
"sandbox": {
"pages": [ "sandboxed.html", "sandboxed2.html" ]
}
})";
auto extension_dir = std::make_unique<TestExtensionDir>();
extension_dir->WriteManifest(kManifest);
extension_dir->WriteFile(FILE_PATH_LITERAL("sandboxed.html"),
"<html>Sandboxed</html>");
extension_dir->WriteFile(FILE_PATH_LITERAL("sandboxed2.html"),
"<html>Sandboxed 2</html>");
extension_dir->WriteFile(FILE_PATH_LITERAL("parent.html"),
R"(<html>
<iframe src="sandboxed.html"></iframe>
<iframe src="sandboxed2.html"></iframe>
</html>)");
const Extension* extension = LoadExtension(extension_dir->UnpackedPath());
extension_dirs_.push_back(std::move(extension_dir));
return extension;
}
const Extension* AddExtensionWithWebViewAndOpen() {
static constexpr char kManifest[] =
R"({
"name": "Web View",
"manifest_version": 2,
"version": "0.1",
"app": {
"background": { "scripts": ["background.js"] }
},
"webview": {
"partitions": [{
"name": "foo",
"accessible_resources": ["accessible.html"]
}]
},
"permissions": ["webview"]
})";
static constexpr char kBackgroundJs[] =
R"(chrome.app.runtime.onLaunched.addListener(() => {
chrome.app.window.create('embedder.html', {}, function () {});
});)";
static constexpr char kEmbedderHtml[] =
R"(<html>
<body>
<webview partition="foo"></webview>
<script src="embedder.js"></script>
</body>
</html>)";
static constexpr char kEmbedderJs[] =
R"(onload = () => {
let webview = document.querySelector('webview');
webview.addEventListener('loadstop', () => {
chrome.test.sendMessage('webview loaded');
});
webview.addEventListener('loadabort', (e) => {
console.error('Webview aborted load: ' + e.toString());
});
webview.src = 'accessible.html';
};)";
auto extension_dir = std::make_unique<TestExtensionDir>();
extension_dir->WriteManifest(kManifest);
extension_dir->WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
extension_dir->WriteFile(FILE_PATH_LITERAL("embedder.html"), kEmbedderHtml);
extension_dir->WriteFile(FILE_PATH_LITERAL("embedder.js"), kEmbedderJs);
extension_dir->WriteFile(FILE_PATH_LITERAL("accessible.html"), "hello");
ExtensionTestMessageListener webview_listener("webview loaded");
const Extension* extension = LoadAndLaunchApp(extension_dir->UnpackedPath(),
/*uses_guest_view=*/true);
extension_dirs_.push_back(std::move(extension_dir));
EXPECT_TRUE(webview_listener.WaitUntilSatisfied());
return extension;
}
content::WebContents* GetAppWindowContents() {
AppWindowRegistry* registry = AppWindowRegistry::Get(profile());
if (registry->app_windows().size() != 1) {
ADD_FAILURE() << "Incorrect number of app windows: "
<< registry->app_windows().size();
return nullptr;
}
return (*registry->app_windows().begin())->web_contents();
}
content::WebContents* GetWebViewFromEmbedder(content::WebContents* embedder) {
std::vector<content::WebContents*> inner_web_contents =
embedder->GetInnerWebContents();
if (inner_web_contents.size() != 1) {
ADD_FAILURE() << "Unexpected number of inner web contents: "
<< inner_web_contents.size();
return nullptr;
}
content::WebContents* inner_contents = inner_web_contents[0];
if (!WebViewGuest::FromWebContents(inner_contents)) {
return nullptr;
}
return inner_contents;
}
// Opens a new tab to the given `domain`.
void OpenDomain(std::string_view domain) {
ASSERT_TRUE(
NavigateToURL(GetActiveWebContents(),
embedded_test_server()->GetURL(domain, "/simple.html")));
}
// Opens a new tab to a Web UI page.
void OpenWebUi() {
ASSERT_TRUE(
NavigateToURL(GetActiveWebContents(), GURL("chrome://settings")));
}
// Opens a new tab to a page in the given `extension`.
void OpenExtensionPage(const Extension& extension) {
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
extension.GetResourceURL("manifest.json")));
}
// Opens a new tab to the given `domain` and waits for a content script to
// inject.
void OpenDomainAndWaitForContentScript(std::string_view domain) {
ExtensionTestMessageListener listener("script injected");
OpenDomain(domain);
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
// Opens a new tab to the page with a sandboxed frame in the given
// `extension`.
void OpenExtensionPageWithSandboxedFrame(const Extension& extension) {
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
extension.GetResourceURL("parent.html")));
}
// Determines if a given `frame` is sandboxed. Sandboxed frames don't
// have access to any special extension APIs, even those that require no
// specific permissions (like chrome.tabs).
bool ExtensionFrameIsSandboxed(content::RenderFrameHost* frame) {
EXPECT_TRUE(frame->GetLastCommittedURL().SchemeIs(kExtensionScheme));
return FrameHasOriginRestrictedSandboxed(frame) &&
!FrameHasAccessToExtensionApis(frame);
}
bool FrameHasOriginRestrictedSandboxed(content::RenderFrameHost* frame) {
return frame->IsSandboxed(network::mojom::WebSandboxFlags::kOrigin);
}
bool FrameHasAccessToExtensionApis(content::RenderFrameHost* frame) {
// Verify extension api access by actually running a simple api function.
static constexpr char api_access_script[] =
R"(
(async function hasAccessToExtensionAPIs() {
try {
let tabs = await chrome.tabs.query({});
return tabs && tabs.length && tabs.length != 0;
} catch(err) {
return false;
}
})();
)";
// Note: Calling ExtractBool on EvalJsResult below is expected to be safe as
// the script above will always return a boolean. But, if called on a
// sandboxed frame without 'allow-scripts' it will throw a CHECK.
return content::EvalJs(frame, api_access_script).ExtractBool();
}
// Iterates over every context type and checks if it could be hosted given the
// pairing of `extension` and `process`, expecting it to be allowed if and
// only if the context type is in `allowed_contexts`. `debug_string` is used
// in a scoped trace to make test failures more meaningful.
void RunCanProcessHostContextTypeChecks(
const Extension* extension,
const content::RenderProcessHost& process,
const std::vector<mojom::ContextType>& allowed_contexts,
std::string_view debug_string) {
std::vector<mojom::ContextType> all_types = {
mojom::ContextType::kUnspecified,
mojom::ContextType::kPrivilegedExtension,
mojom::ContextType::kUnprivilegedExtension,
mojom::ContextType::kContentScript,
mojom::ContextType::kWebPage,
mojom::ContextType::kPrivilegedWebPage,
mojom::ContextType::kWebUi,
mojom::ContextType::kUntrustedWebUi,
mojom::ContextType::kOffscreenExtension,
mojom::ContextType::kUserScript,
};
for (auto context_type : all_types) {
SCOPED_TRACE(testing::Message()
<< "Testing Context Type: " << context_type
<< ", Extension: "
<< (extension ? extension->name() : "<no extension>")
<< ", Debug String: " << debug_string);
bool expected_to_be_allowed =
base::Contains(allowed_contexts, context_type);
EXPECT_EQ(expected_to_be_allowed,
process_map()->CanProcessHostContextType(extension, process,
context_type));
}
}
ProcessMap* process_map() { return ProcessMap::Get(profile()); }
private:
// Dirs for our test extensions; these have to stay in-scope for the duration
// of the test.
std::vector<std::unique_ptr<TestExtensionDir>> extension_dirs_;
};
// Verify that an injected content script can successfully use dynamic imports
// when operating in a sandboxed srcdoc iframe.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
ContentScriptDynamicImportsWorkInSandboxedSrcdocFrames) {
// Create extension with a content script that relies on dynamic imports.
static constexpr char kManifest[] = R"(
{
"name": "Test Dynamic Import",
"manifest_version": 2,
"version": "1.0",
"web_accessible_resources": [
"content-import.js"
],
"content_scripts": [{
"matches": [ "*://*/*" ],
"all_frames": true,
"js": [ "content-script.js" ],
"match_origin_as_fallback": true
}]
})";
TestExtensionDir dir;
dir.WriteManifest(kManifest);
dir.WriteFile(FILE_PATH_LITERAL("content-import.js"),
R"(export function main() {
document.body.innerHTML =
document.body.innerHTML.replace('sandboxed',
'SANDBOXED');
chrome.test.sendMessage('dynamic import success');
})");
dir.WriteFile(FILE_PATH_LITERAL("content-script.js"), R"(
const src = chrome.runtime.getURL("content-import.js");
import(src).then((contentImport) => {
contentImport.main();
}).catch ((error) => {
console.log('Error: import failed: ' + error.message);
});
)");
const Extension* extension = LoadExtension(dir.UnpackedPath());
ASSERT_TRUE(extension);
// Load a page and give it a sandboxed-srcdoc frame.
ExtensionTestMessageListener listener_mainframe("dynamic import success");
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(NavigateToURL(
web_contents, embedded_test_server()->GetURL("a.test", "/simple.html")));
ASSERT_TRUE(listener_mainframe.WaitUntilSatisfied());
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
content::TestNavigationObserver observer(web_contents, 1);
ExtensionTestMessageListener listener_subframe("dynamic import success");
EXPECT_TRUE(ExecJs(main_frame, R"(
let frame = document.createElement('iframe');
frame.sandbox = '';
frame.srcdoc = '<html><body>sandboxed</body></html>';
document.body.appendChild(frame);
)"));
observer.Wait();
// Wait for the injected content script to complete.
ASSERT_TRUE(listener_subframe.WaitUntilSatisfied());
// Verify that the action performed by the dynamically imported code was
// successful.
content::RenderFrameHost* sandboxed_child_frame =
content::ChildFrameAt(main_frame, 0);
EXPECT_TRUE(content::EvalJs(sandboxed_child_frame,
"document.body.innerHTML == 'SANDBOXED';")
.ExtractBool());
}
// Check that when an extension frame is inadvertently loaded as sandboxed
// because it inherits sandbox flags from its parent, the extension frame can
// still use extension messaging APIs without triggering a renderer kill due
// to sandboxed frame checks in ChildProcessSecurityPolicy.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest, SandboxedWebPageEmbedsExtension) {
GURL sandboxed_url =
embedded_test_server()->GetURL("a.test", "/csp-sandbox.html");
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), sandboxed_url));
content::WebContents* web_contents = GetActiveWebContents();
content::RenderFrameHost* sandboxed_main_frame =
web_contents->GetPrimaryMainFrame();
ASSERT_TRUE(sandboxed_main_frame->IsSandboxed(
network::mojom::WebSandboxFlags::kOrigin));
// Set up an extension with a web-accessible page that sends a message to a
// background worker and waits for a response.
static constexpr char kManifest[] = R"(
{
"name": "Foo",
"version": "1.0",
"web_accessible_resources": [{
"resources": ["foo.html"],
"matches": ["*://*/*"]
}],
"manifest_version": 3,
"background": { "service_worker": "worker.js" }
})";
TestExtensionDir dir;
dir.WriteManifest(kManifest);
dir.WriteFile(FILE_PATH_LITERAL("foo.html"),
R"(<script src="foo.js"></script>)");
dir.WriteFile(FILE_PATH_LITERAL("foo.js"), R"(
(async function() {
const response = await chrome.runtime.sendMessage('ping');
chrome.test.assertEq('pong', response);
chrome.test.sendMessage('done');
})();
)");
dir.WriteFile(FILE_PATH_LITERAL("worker.js"), R"(
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request == 'ping') {
sendResponse('pong');
}
});
)");
const Extension* extension = LoadExtension(dir.UnpackedPath());
GURL extension_url = extension->GetResourceURL("foo.html");
// Insert an extension subframe into the sandboxed main frame and ensure that
// the the sendMessage exchange finishes successfully.
const char kAddFrameScript[] =
R"(
let f = document.createElement('iframe');
f.src = $1;
document.body.appendChild(f);
)";
ExtensionTestMessageListener listener("done");
content::TestNavigationObserver observer(web_contents, 1);
EXPECT_TRUE(ExecJs(sandboxed_main_frame,
content::JsReplace(kAddFrameScript, extension_url)));
observer.Wait();
// Double-check that the extension frame was sandboxed but maintained access
// to extension APIs.
content::RenderFrameHost* sandboxed_extension_frame =
content::ChildFrameAt(sandboxed_main_frame, 0);
EXPECT_TRUE(sandboxed_extension_frame->IsSandboxed(
network::mojom::WebSandboxFlags::kOrigin));
EXPECT_TRUE(FrameHasAccessToExtensionApis(sandboxed_extension_frame));
EXPECT_TRUE(listener.WaitUntilSatisfied());
}
// Tests that extension E1 containing a sandboxed webpage A which then contains
// extension E2 in a subframe results in the E2 frame being sandboxed.
IN_PROC_BROWSER_TEST_F(
ProcessMapBrowserTest,
ExtensionFrameContainingSandboxedFrameContainingOtherExtensionFrame) {
GURL a_frame_url =
embedded_test_server()->GetURL("example.com", "/iframe_blank.html");
// Create extension 1 (E1).
static constexpr char kManifestE1[] =
R"({
"name": "E1",
"manifest_version": 3,
"version": "0.1"
})";
TestExtensionDir extension_dir1;
extension_dir1.WriteManifest(kManifestE1);
static constexpr char kPageWithSandboxedFrame[] =
R"(<html>
<h1>E1</h1>
<iframe sandbox="allow-scripts" src="%s"></iframe>
</html>)";
extension_dir1.WriteFile(
FILE_PATH_LITERAL("main.html"),
base::StringPrintf(kPageWithSandboxedFrame, a_frame_url.spec().c_str()));
const Extension* extension1 = LoadExtension(extension_dir1.UnpackedPath());
// Create extension 2 (E2).
static constexpr char kManifestE2[] =
R"({
"name": "E2",
"manifest_version": 3,
"version": "0.1",
"web_accessible_resources": [
{
"resources": [ "main.html" ],
"matches": [ "*://*/*" ]
}
]
})";
TestExtensionDir extension_dir2;
extension_dir2.WriteManifest(kManifestE2);
extension_dir2.WriteFile(FILE_PATH_LITERAL("main.html"),
"<html><h1>E2</h2></html>");
const Extension* extension2 = LoadExtension(extension_dir2.UnpackedPath());
// Load E1.
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(
NavigateToURL(web_contents, extension1->GetResourceURL("main.html")));
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
content::RenderFrameHost* sandboxed_a_frame =
content::ChildFrameAt(main_frame, 0);
// Navigate frame in A's subframe to E2.
GURL e2_main_url = extension2->GetResourceURL("main.html");
content::TestNavigationObserver observer(web_contents);
static constexpr char kScriptE2Load[] =
R"(
document.getElementById('test').src = $1;
)";
EXPECT_TRUE(content::ExecJs(sandboxed_a_frame,
content::JsReplace(kScriptE2Load, e2_main_url)));
observer.Wait();
// Verify that the E2 is sandboxed.
content::RenderFrameHost* sandboxed_E2_frame =
content::ChildFrameAt(sandboxed_a_frame, 0);
ASSERT_NE(nullptr, sandboxed_E2_frame);
// The E2 frame has an origin-restricted sandbox flag.
EXPECT_TRUE(FrameHasOriginRestrictedSandboxed(sandboxed_E2_frame));
EXPECT_TRUE(sandboxed_E2_frame->GetLastCommittedOrigin().opaque());
EXPECT_TRUE(content::EvalJs(sandboxed_E2_frame, "window.origin == 'null';")
.ExtractBool());
// The E2 frame has access to extension APIs.
EXPECT_TRUE(process_map()->Contains(
sandboxed_E2_frame->GetProcess()->GetDeprecatedID()));
EXPECT_TRUE(FrameHasAccessToExtensionApis(sandboxed_E2_frame));
// The E2 frame is sandboxed by virtue of being loaded in an iframe with
// a sandbox attribute set, but it is not a manifest-sandboxed frame. As such,
// it gets placed in the main extension process, has access to extension APIs
// and is not places in a sandboxed SiteInstance.
EXPECT_FALSE(content::HasSandboxedSiteInstance(sandboxed_E2_frame));
// Each frame will be in a separate process due to site isolation.
EXPECT_NE(main_frame->GetProcess(), sandboxed_a_frame->GetProcess());
EXPECT_NE(main_frame->GetProcess(), sandboxed_E2_frame->GetProcess());
EXPECT_NE(sandboxed_a_frame->GetProcess(), sandboxed_E2_frame->GetProcess());
}
// Tests that web pages are not considered privileged extension processes.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
IsPrivilegedExtensionProcess_WebPages) {
// For fun, make sure an extension with access to the given web page is
// loaded (just to validate we're not doing anything related to
// extension permissions in our calculations).
const Extension* extension =
AddExtensionWithHostPermission("test", "*://example.com/*");
ASSERT_TRUE(extension);
OpenDomain("example.com");
EXPECT_FALSE(process_map()->IsPrivilegedExtensionProcess(
*extension, GetActiveMainFrameProcessID()));
}
// Tests the type of contexts that can be hosted in web page processes.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest, CanHostContextType_WebPages) {
// For fun, make sure an extension with access to the given web page is
// loaded (just to validate we're not doing anything related to
// extension permissions in our calculations).
const Extension* extension =
AddExtensionWithHostPermission("test", "*://example.com/*");
ASSERT_TRUE(extension);
OpenDomain("example.com");
content::RenderProcessHost& web_page_process = GetActiveMainFrameProcess();
RunCanProcessHostContextTypeChecks(extension, web_page_process,
{mojom::ContextType::kContentScript},
"web page with extension passed");
RunCanProcessHostContextTypeChecks(
nullptr, web_page_process,
{mojom::ContextType::kWebPage, mojom::ContextType::kUntrustedWebUi},
"web page without extension passed");
}
// Tests that web ui pages are not considered privileged extension processes.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
IsPrivilegedExtensionProcess_WebUiPages) {
const Extension* extension =
AddExtensionWithHostPermission("test", "*://example.com/*");
ASSERT_TRUE(extension);
OpenWebUi();
EXPECT_FALSE(process_map()->IsPrivilegedExtensionProcess(
*extension, GetActiveMainFrameProcessID()));
}
// Tests the type of processes that can be hosted in web ui processes.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest, CanHostContextType_WebUiPages) {
const Extension* extension =
AddExtensionWithHostPermission("test", "*://example.com/*");
ASSERT_TRUE(extension);
OpenWebUi();
content::RenderProcessHost& webui_process = GetActiveMainFrameProcess();
RunCanProcessHostContextTypeChecks(extension, webui_process,
{mojom::ContextType::kContentScript},
"webui page with extension passed");
RunCanProcessHostContextTypeChecks(nullptr, webui_process,
{mojom::ContextType::kWebUi},
"webui page without extension passed");
}
// Tests that normal extension pages are considered privileged extension
// processes.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
IsPrivilegedExtensionProcess_ExtensionPages) {
// Load up two extensions, each with the same permissions.
const Extension* extension1 =
AddExtensionWithHostPermission("test1", "*://example.com/*");
const Extension* extension2 =
AddExtensionWithHostPermission("test2", "*://example.com/*");
ASSERT_TRUE(extension1);
ASSERT_TRUE(extension2);
// Navigate to a page within the first extension. It should be a privileged
// page for that extension, but not the other.
OpenExtensionPage(*extension1);
EXPECT_TRUE(process_map()->IsPrivilegedExtensionProcess(
*extension1, GetActiveMainFrameProcessID()));
EXPECT_FALSE(process_map()->IsPrivilegedExtensionProcess(
*extension2, GetActiveMainFrameProcessID()));
// Inversion: Navigate to the page of the second extension. It should be a
// privileged page in the second, but not the first.
OpenExtensionPage(*extension2);
EXPECT_FALSE(process_map()->IsPrivilegedExtensionProcess(
*extension1, GetActiveMainFrameProcessID()));
EXPECT_TRUE(process_map()->IsPrivilegedExtensionProcess(
*extension2, GetActiveMainFrameProcessID()));
}
// Tests the type of contexts that can be hosted in regular extension processes.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
CanHostContextType_ExtensionPages) {
// Load up two extensions, each with the same permissions.
const Extension* extension1 =
AddExtensionWithHostPermission("test1", "*://example.com/*");
const Extension* extension2 =
AddExtensionWithHostPermission("test2", "*://example.com/*");
ASSERT_TRUE(extension1);
ASSERT_TRUE(extension2);
// Navigate to a page within the first extension. It should be a privileged
// page for that extension, but not the other.
OpenExtensionPage(*extension1);
content::RenderProcessHost& extension1_process = GetActiveMainFrameProcess();
RunCanProcessHostContextTypeChecks(extension1, extension1_process,
{mojom::ContextType::kContentScript,
mojom::ContextType::kPrivilegedExtension,
mojom::ContextType::kOffscreenExtension},
"extension1 page with extension1 passed");
RunCanProcessHostContextTypeChecks(extension2, extension1_process,
{mojom::ContextType::kContentScript},
"extension1 page with extension2 passed");
RunCanProcessHostContextTypeChecks(
nullptr, extension1_process, {},
"extension1 page without extension passed");
// Inversion: Navigate to the page of the second extension. It should be a
// privileged page in the second, but not the first.
OpenExtensionPage(*extension2);
content::RenderProcessHost& extension2_process = GetActiveMainFrameProcess();
RunCanProcessHostContextTypeChecks(extension2, extension2_process,
{mojom::ContextType::kContentScript,
mojom::ContextType::kPrivilegedExtension,
mojom::ContextType::kOffscreenExtension},
"extension2 page with extension2 passed");
RunCanProcessHostContextTypeChecks(extension1, extension2_process,
{mojom::ContextType::kContentScript},
"extension2 page with extension1 passed");
RunCanProcessHostContextTypeChecks(
nullptr, extension2_process, {},
"extension2 page without extension passed");
}
// Tests that a web page with injected content scripts is not considered a
// privileged extension process.
IN_PROC_BROWSER_TEST_F(
ProcessMapBrowserTest,
IsPrivilegedExtensionProcess_WebPagesWithContentScripts) {
const Extension* extension =
AddExtensionWithContentScript("test", "*://example.com/*");
ASSERT_TRUE(extension);
// Navigate to a web page and wait for the content script to inject.
OpenDomainAndWaitForContentScript("example.com");
EXPECT_FALSE(process_map()->IsPrivilegedExtensionProcess(
*extension, GetActiveMainFrameProcessID()));
}
// Tests the type of contexts that can be hosted in a web page process that has
// had a content script injected in it.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
CanHostContextType_WebPagesWithContentScripts) {
const Extension* extension =
AddExtensionWithContentScript("test", "*://example.com/*");
ASSERT_TRUE(extension);
// Navigate to a web page and wait for the content script to inject.
OpenDomainAndWaitForContentScript("example.com");
content::RenderProcessHost& page_process = GetActiveMainFrameProcess();
RunCanProcessHostContextTypeChecks(extension, page_process,
{mojom::ContextType::kContentScript},
"web page with extension passed");
RunCanProcessHostContextTypeChecks(
nullptr, page_process,
{mojom::ContextType::kWebPage, mojom::ContextType::kUntrustedWebUi},
"web page without extension passed");
}
// The following defines a common test body used by the
// Sandboxed*Are*Isolated tests that follow. `frame_url` defines the page to
// be loaded, and may be an regular (http/s) page, a data url, or an
// about:srcdoc url. If it's about:srcdoc, the iframe srcdoc attribute will be
// used, and set to the value of `content`. `expect_isolated_from_each_other`
// indicates whether the subframes are expected to be isolated from each other,
// and if the sandboxed frame should have a sandboxed SiteInstance.
// `expect_sandboxed_subframe_isolated_from_extension_page` indicates we
// expect the sandboxed frame to be isolated from the extension mainframe,
// and `expect_non_sandboxed_subframe_isolated_from_extension_page` indicates
// that we expect the non-sandboxed subframe to be process isolated from the
// extension mainframe. This function is defined here to keep it close to the
// tests that use it, for easier reference.
void ProcessMapBrowserTest::VerifyWhetherSubframesAreIsolated(
const GURL& frame_url,
const std::string& content,
bool expect_subframes_isolated_from_each_other,
bool expect_sandboxed_subframe_isolated_from_extension_page,
bool expect_non_sandboxed_subframe_isolated_from_extension_page) {
const Extension* extension =
AddExtensionWithSandboxedWebpage(frame_url, content);
ASSERT_TRUE(extension);
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(
NavigateToURL(web_contents, extension->GetResourceURL("parent.html")));
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
content::RenderFrameHost* sandboxed_child_frame =
content::ChildFrameAt(main_frame, 0);
content::RenderFrameHost* non_sandboxed_child_frame =
content::ChildFrameAt(main_frame, 1);
EXPECT_FALSE(ExtensionFrameIsSandboxed(main_frame));
int main_frame_process_id = main_frame->GetProcess()->GetDeprecatedID();
int sandboxed_frame_process_id =
sandboxed_child_frame->GetProcess()->GetDeprecatedID();
int non_sandboxed_frame_process_id =
non_sandboxed_child_frame->GetProcess()->GetDeprecatedID();
if (expect_subframes_isolated_from_each_other) {
EXPECT_NE(sandboxed_frame_process_id, non_sandboxed_frame_process_id);
EXPECT_TRUE(content::HasSandboxedSiteInstance(sandboxed_child_frame));
} else {
EXPECT_EQ(sandboxed_frame_process_id, non_sandboxed_frame_process_id);
EXPECT_FALSE(content::HasSandboxedSiteInstance(sandboxed_child_frame));
}
if (expect_sandboxed_subframe_isolated_from_extension_page) {
EXPECT_NE(main_frame_process_id, sandboxed_frame_process_id);
} else {
EXPECT_EQ(main_frame_process_id, sandboxed_frame_process_id);
}
if (expect_non_sandboxed_subframe_isolated_from_extension_page) {
EXPECT_NE(main_frame_process_id, non_sandboxed_frame_process_id);
} else {
EXPECT_EQ(main_frame_process_id, non_sandboxed_frame_process_id);
}
EXPECT_FALSE(ExtensionFrameIsSandboxed(main_frame));
EXPECT_FALSE(content::HasSandboxedSiteInstance(non_sandboxed_child_frame));
}
// Tests that web pages loaded in sandboxed iframes inside an extension are
// isolated from the extension and from non-sandboxed iframes of the same web
// origin, if IsolateSandboxedIframes is enabled. There are three variations,
// one for a web url, one for a data: url, and one for about:srcdoc.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
SandboxedNonExtensionWebPagesAreIsolated) {
GURL frame_url =
embedded_test_server()->GetURL("example.com", "/simple.html");
bool expect_subframes_isolated_from_each_other =
content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled();
// The subframes should be cross-process to each other, and the sandboxed
// frame should be in a sandboxed SiteInstance. Web-based content inside an
// extension is always cross-process to the extension frame that contains it.
VerifyWhetherSubframesAreIsolated(
frame_url, /*content=*/std::string(),
expect_subframes_isolated_from_each_other,
/*expect_sandboxed_subframe_isolated_from_extension_page=*/true,
/*expect_non_sandboxed_subframe_isolated_from_extension_page=*/true);
}
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
SandboxedDataFramesAreMaybeIsolated) {
GURL frame_url("data:text/html, foo");
// Srcdoc/data-url content inside a sandboxed frame in an extension is
// same-process to the extension frame that contains it, unless
// IsolateSandboxedIframes is enabled, in which case it is cross-process.
bool expect_subframes_isolated_from_each_other =
content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled();
bool expect_sandboxed_subframe_isolated_from_extension_page =
content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled();
VerifyWhetherSubframesAreIsolated(
frame_url, /*content=*/std::string(),
expect_subframes_isolated_from_each_other,
expect_sandboxed_subframe_isolated_from_extension_page,
/*expect_non_sandboxed_subframe_isolated_from_extension_page=*/false);
}
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
SandboxedSrcdocFramesAreMaybeIsolated) {
GURL frame_url("about:srcdoc");
// Srcdoc/data-url content inside a sandboxed frame in an extension is
// same-process to the extension frame that contains it, unless
// IsolateSandboxedIframes is enabled, in which case it is cross-process.
bool expect_subframes_isolated_from_each_other =
content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled();
bool expect_sandboxed_subframe_isolated_from_extension_page =
content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled();
VerifyWhetherSubframesAreIsolated(
frame_url, /*content=*/std::string("foo"),
expect_subframes_isolated_from_each_other,
expect_sandboxed_subframe_isolated_from_extension_page,
/*expect_non_sandboxed_subframe_isolated_from_extension_page=*/false);
}
// Function implementation defined here to be close to the tests that use it.
void ProcessMapBrowserTest::
VerifySandboxedSubframeHasResourceAccessButMaybeApiAccess(
const Extension* extension,
std::string_view parent_script,
const bool is_subframe_data_url,
const bool expects_api_access) {
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(
NavigateToURL(web_contents, extension->GetResourceURL("parent.html")));
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
// Use JS to add content to the child frame.
content::TestNavigationObserver observer(web_contents);
EXPECT_TRUE(content::ExecJs(main_frame, parent_script));
observer.Wait();
content::RenderFrameHost* sandboxed_child_frame =
content::ChildFrameAt(main_frame, 0);
int sandboxed_frame_process_id =
sandboxed_child_frame->GetProcess()->GetDeprecatedID();
// Sandboxed extension frames should still have access to other extension
// resources. Verify the extension script (resource.js) was properly loaded
// by looking for foo variable.
EXPECT_EQ("bar",
content::EvalJs(sandboxed_child_frame, "foo;").ExtractString());
// Sandboxed data and about:srcdoc frames, as well as manifest-sandboxed
// extension pages, do not expect API access. As such, they are placed in
// a non-privileged process. Extension pages that are sandboxed, but not
// listed as sandboxed in the manifest, do get API access and are placed in an
// privileged extension process.
EXPECT_EQ(expects_api_access, process_map()->IsPrivilegedExtensionProcess(
*extension, sandboxed_frame_process_id));
// Verify expected api access.
EXPECT_EQ(expects_api_access,
FrameHasAccessToExtensionApis(sandboxed_child_frame));
}
// Tests that a data: url in a sandboxed frame in an extension still has access
// to resources.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
SandboxedDataUrlStillHasAccessToExtensionResources) {
const Extension* extension = AddExtensionWithResource();
ASSERT_TRUE(extension);
// The %s in the string below will be filled in with the extension's origin by
// VerifySandboxedSubframeHasResourceAccessButMaybeApiAccess.
std::string parent_script = base::StrCat({
R"(let test_frame = document.getElementById('test_frame');
test_frame.src = 'data:text/html, <script src=")",
extension->origin().GetURL().spec(), R"(resource.js"></script>';)"});
VerifySandboxedSubframeHasResourceAccessButMaybeApiAccess(
extension, parent_script, /*is_subframe_data_url=*/true,
/*expects_api_access=*/false);
}
// Tests that a srcdoc in a sandboxed frame in an extension still has access to
// resources.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
SandboxedSrcdocStillHasAccessToExtensionResources) {
const Extension* extension = AddExtensionWithResource();
ASSERT_TRUE(extension);
// The %s in the string below will be filled in with the extension's origin by
// VerifySandboxedSubframeHasResourceAccessButMaybeApiAccess.
std::string parent_script = base::StrCat({
R"(let test_frame = document.getElementById('test_frame');
test_frame.srcdoc = '<script src=")",
extension->origin().GetURL().spec(), R"(resource.js"></script>';)"});
VerifySandboxedSubframeHasResourceAccessButMaybeApiAccess(
extension, parent_script, /*is_subframe_data_url=*/false,
/*expects_api_access=*/false);
}
// Tests that an extension page in a sandboxed frame in an extension still has
// access to resources.
IN_PROC_BROWSER_TEST_F(
ProcessMapBrowserTest,
SandboxedExtensionPageStillHasAccessToExtensionResources) {
const Extension* extension = AddExtensionWithResource();
ASSERT_TRUE(extension);
VerifySandboxedSubframeHasResourceAccessButMaybeApiAccess(
extension, R"(let test_frame = document.getElementById('test_frame');
test_frame.src = 'page_requesting_resource.html';)",
/*is_subframe_data_url=*/false,
/*expects_api_access=*/true);
}
// Tests that an extension inside a sandboxed subframe of another extension
// still has privileges. It will be process isolated regardless of the sandbox
// attribute since extensions are isolated from one another.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
SandboxedSubframeExtensionHasPrivilege) {
std::pair<const Extension*, const Extension*> nested_extensions =
AddNestedExtensions();
const Extension* extension1 = nested_extensions.first;
const Extension* extension2 = nested_extensions.second;
ASSERT_TRUE(extension1);
ASSERT_TRUE(extension2);
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(
NavigateToURL(web_contents, extension2->GetResourceURL("parent.html")));
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
content::RenderFrameHost* sandboxed_child_frame =
content::ChildFrameAt(main_frame, 0);
int main_frame_process_id = main_frame->GetProcess()->GetDeprecatedID();
int sandboxed_frame_process_id =
sandboxed_child_frame->GetProcess()->GetDeprecatedID();
// Since we normally process-isolate E1 from E2, placing E1 in a sandboxed
// iframe will make no difference.
EXPECT_NE(main_frame_process_id, sandboxed_frame_process_id);
EXPECT_TRUE(process_map()->IsPrivilegedExtensionProcess(
*extension2, main_frame_process_id));
EXPECT_TRUE(process_map()->IsPrivilegedExtensionProcess(
*extension1, sandboxed_frame_process_id));
// From an extensions point of view, applying 'sandbox' to the child iframe
// in the manifest prevents it from having access to extension APIs, and
// also places it in a non-privileged process if IsolateSandboxedFrames is
// enabled.
EXPECT_FALSE(ExtensionFrameIsSandboxed(main_frame));
EXPECT_FALSE(ExtensionFrameIsSandboxed(sandboxed_child_frame));
// Attempt to have `extension1` (in `sandboxed_child_frame`) load a
// non-web-accessible resource from `extension2`. This should fail. The fact
// that the child is sandboxed doesn't matter.
GURL e2_private_page_url = extension2->GetResourceURL("private_page.html");
const char kJsScript[] =
R"(
frm = document.createElement('iframe');
frm.src = $1;
document.body.appendChild(frm);
)";
content::TestNavigationObserver observer(GetActiveWebContents(), 1);
EXPECT_TRUE(ExecJs(sandboxed_child_frame,
content::JsReplace(kJsScript, e2_private_page_url)));
observer.Wait();
EXPECT_FALSE(observer.last_navigation_succeeded());
EXPECT_EQ(net::ERR_BLOCKED_BY_CLIENT, observer.last_net_error_code());
content::RenderFrameHost* grand_child_frame =
content::ChildFrameAt(sandboxed_child_frame, 0);
EXPECT_NE(nullptr, grand_child_frame);
EXPECT_EQ(e2_private_page_url, grand_child_frame->GetLastCommittedURL());
}
// At present, the default mode is IsolatedSandboxedIframes mode (which isolates
// manifest-sandboxed extension pages in a different process that is not
// privileged). If there are multiple manifest-sandboxed extension pages,
// they will share a SiteInstance and non-privileged process. This test verifies
// that all manifest-sandboxed frames load into the same (non-privileged)
// process.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
IsPrivilegedExtensionProcess_SandboxedExtensionFrame) {
const Extension* extension = AddExtensionWithSandboxedFrame();
ASSERT_TRUE(extension);
OpenExtensionPageWithSandboxedFrame(*extension);
content::WebContents* web_contents = GetActiveWebContents();
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
content::RenderFrameHost* sandboxed_frame =
content::ChildFrameAt(main_frame, 0);
content::RenderFrameHost* other_sandboxed_frame =
content::ChildFrameAt(main_frame, 1);
EXPECT_FALSE(ExtensionFrameIsSandboxed(main_frame));
EXPECT_TRUE(ExtensionFrameIsSandboxed(sandboxed_frame));
EXPECT_TRUE(ExtensionFrameIsSandboxed(other_sandboxed_frame));
int main_frame_process_id = main_frame->GetProcess()->GetDeprecatedID();
int sandboxed_frame_process_id =
sandboxed_frame->GetProcess()->GetDeprecatedID();
int other_sandboxed_frame_process_id =
other_sandboxed_frame->GetProcess()->GetDeprecatedID();
// The two manifest-sandboxed frames will be in the same process, regardless
// of whether IsolateSandboxedIframes is enabled or not.
EXPECT_EQ(other_sandboxed_frame_process_id, sandboxed_frame_process_id);
if (content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled()) {
EXPECT_NE(main_frame_process_id, sandboxed_frame_process_id);
EXPECT_FALSE(process_map()->IsPrivilegedExtensionProcess(
*extension, sandboxed_frame_process_id));
} else {
EXPECT_EQ(main_frame_process_id, sandboxed_frame_process_id);
EXPECT_TRUE(process_map()->IsPrivilegedExtensionProcess(
*extension, sandboxed_frame_process_id));
}
EXPECT_TRUE(process_map()->IsPrivilegedExtensionProcess(
*extension, main_frame_process_id));
}
// Test class to run tests both with and without sandboxing.
class ProcessMapAboutSrcdocBrowserTest
: public ProcessMapBrowserTest,
public ::testing::WithParamInterface<bool> {
public:
ProcessMapAboutSrcdocBrowserTest() = default;
};
// This test verifies that an about:srcdoc frame with a non-extension parent
// cannot inherit an extension precursor origin that allows it to incorrectly
// access extension resources. The srcdoc frame should also not inherit the
// base URI of the extension.
IN_PROC_BROWSER_TEST_P(ProcessMapAboutSrcdocBrowserTest,
ExtensionCannotNavigateAboutSrcdocGrandchild) {
bool srcdoc_is_sandboxed = GetParam();
const Extension* extension =
AddExtensionWithNonExtensionSubframeWithSrcdocSubframe(
srcdoc_is_sandboxed);
ASSERT_TRUE(extension);
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(
NavigateToURL(web_contents, extension->GetResourceURL("parent.html")));
content::RenderFrameHost* extension_frame =
web_contents->GetPrimaryMainFrame();
content::RenderFrameHost* non_extension_frame =
content::ChildFrameAt(extension_frame, 0);
content::RenderFrameHost* srcdoc_frame =
content::ChildFrameAt(non_extension_frame, 0);
// Verify that srcdoc frame has baseURI from it's parent.
std::string extension_base_uri =
EvalJs(extension_frame, "document.baseURI").ExtractString();
std::string non_extension_base_uri =
EvalJs(non_extension_frame, "document.baseURI").ExtractString();
std::string srcdoc_base_uri =
EvalJs(srcdoc_frame, "document.baseURI").ExtractString();
EXPECT_EQ(non_extension_base_uri, srcdoc_base_uri);
// Attempt to have `extension_frame` navigate the srcdoc frame to
// about:srcdoc.
content::TestNavigationObserver observer(web_contents, 1);
EXPECT_TRUE(
ExecJs(extension_frame, "frames[0][0].location.href = 'about:srcdoc';"));
observer.Wait();
// Verify that the srcdoc frame doesn't have the access to the extension's
// origin, or any privileges.
srcdoc_frame = content::ChildFrameAt(non_extension_frame, 0);
std::string new_srcdoc_base_uri =
EvalJs(srcdoc_frame, "document.baseURI").ExtractString();
// The srcdoc gets a baseURI for an error page, but at least it's not the
// extension's baseURI.
EXPECT_NE(extension_base_uri, new_srcdoc_base_uri);
EXPECT_FALSE(content::EvalJs(srcdoc_frame, "!!chrome && !!chrome.tabs;")
.ExtractBool());
EXPECT_FALSE(
process_map()->Contains(srcdoc_frame->GetProcess()->GetDeprecatedID()));
// Make sure the resulting srcdoc frame cannot fetch() extension resources.
// The only way `success` in the JS below can become true is if the fetch()
// fails and the error is 'Failed to fetch'. If the fetch() succeeds and
// a response is received, the test fails.
const char jsTemplate[] = R"(
(async () => {
success = await fetch($1, { mode: 'no-cors'})
.then(response => { return false; })
.catch(err => {
return (err instanceof TypeError) &&
(err.message == 'Failed to fetch');
});
return success;
})();
)";
GURL json_resource_url = extension->GetResourceURL("data.json");
EXPECT_TRUE(
EvalJs(srcdoc_frame, content::JsReplace(jsTemplate, json_resource_url))
.ExtractBool());
}
INSTANTIATE_TEST_SUITE_P(
All,
ProcessMapAboutSrcdocBrowserTest,
testing::Values(true, false),
[](const testing::TestParamInfo<bool>& info) {
bool srcdoc_is_sandboxed = info.param;
std::string label = base::StringPrintf(
"kBlockCrossOriginInitiatedAboutSrcdocNavigation_%s",
srcdoc_is_sandboxed ? "Sandboxed" : "NotSandboxed");
return label;
});
// Tests the type of contexts that can be hosted in extension processes with
// a sandboxed process frame.
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
CanHostContextType_SandboxedExtensionFrame) {
const Extension* extension = AddExtensionWithSandboxedFrame();
ASSERT_TRUE(extension);
OpenExtensionPageWithSandboxedFrame(*extension);
content::WebContents* web_contents = GetActiveWebContents();
content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
content::RenderFrameHost* sandboxed_frame =
content::ChildFrameAt(main_frame, 0);
EXPECT_FALSE(ExtensionFrameIsSandboxed(main_frame));
EXPECT_TRUE(ExtensionFrameIsSandboxed(sandboxed_frame));
content::RenderProcessHost& main_frame_process = *main_frame->GetProcess();
content::RenderProcessHost& sandboxed_frame_process =
*sandboxed_frame->GetProcess();
if (content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled()) {
EXPECT_NE(main_frame_process.GetDeprecatedID(),
sandboxed_frame_process.GetDeprecatedID());
} else {
EXPECT_EQ(main_frame_process.GetDeprecatedID(),
sandboxed_frame_process.GetDeprecatedID());
}
RunCanProcessHostContextTypeChecks(
extension, main_frame_process,
{mojom::ContextType::kContentScript,
mojom::ContextType::kPrivilegedExtension,
mojom::ContextType::kOffscreenExtension},
"main frame process with extension passed");
RunCanProcessHostContextTypeChecks(
nullptr, main_frame_process, {},
"main frame process without extension passed");
if (content::SiteIsolationPolicy::AreIsolatedSandboxedIframesEnabled()) {
RunCanProcessHostContextTypeChecks(
extension, sandboxed_frame_process,
{mojom::ContextType::kContentScript},
"sandboxed frame process with extension passed");
RunCanProcessHostContextTypeChecks(
nullptr, sandboxed_frame_process,
{mojom::ContextType::kWebPage, mojom::ContextType::kUntrustedWebUi},
"sandboxed frame process without extension passed");
} else {
RunCanProcessHostContextTypeChecks(
extension, sandboxed_frame_process,
{mojom::ContextType::kContentScript,
mojom::ContextType::kPrivilegedExtension,
mojom::ContextType::kOffscreenExtension},
"sandboxed frame process with extension passed");
RunCanProcessHostContextTypeChecks(
nullptr, sandboxed_frame_process, {},
"sandboxed frame process without extension passed");
}
}
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
IsPrivilegedExtensionProcess_WebViews) {
const Extension* extension = AddExtensionWithWebViewAndOpen();
ASSERT_TRUE(extension);
content::WebContents* embedder = GetAppWindowContents();
ASSERT_TRUE(embedder);
content::WebContents* webview = GetWebViewFromEmbedder(embedder);
ASSERT_TRUE(webview);
// The embedder (the app window) should be a privileged extension process,
// but the webview should not.
EXPECT_TRUE(process_map()->IsPrivilegedExtensionProcess(
*extension,
embedder->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID()));
EXPECT_FALSE(process_map()->IsPrivilegedExtensionProcess(
*extension,
webview->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID()));
}
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest, CanHostContextType_WebViews) {
const Extension* extension = AddExtensionWithWebViewAndOpen();
ASSERT_TRUE(extension);
content::WebContents* embedder = GetAppWindowContents();
ASSERT_TRUE(embedder);
content::WebContents* webview = GetWebViewFromEmbedder(embedder);
ASSERT_TRUE(webview);
// The embedder (the app window) can host any kind of extension context
// except an unprivileged extension context (which is only available to
// webviews).
RunCanProcessHostContextTypeChecks(
extension, *embedder->GetPrimaryMainFrame()->GetProcess(),
{mojom::ContextType::kContentScript,
mojom::ContextType::kPrivilegedExtension,
mojom::ContextType::kOffscreenExtension},
"embedder process");
// The webview can only host content scripts, user scripts, and
// unprivileged extension contexts (accessible resources).
RunCanProcessHostContextTypeChecks(
extension, *webview->GetPrimaryMainFrame()->GetProcess(),
{mojom::ContextType::kContentScript,
mojom::ContextType::kUnprivilegedExtension},
"webview process with extension passed");
// If the extension isn't associated with the call, the webview could only
// possibly contain web pages and untrusted web ui.
RunCanProcessHostContextTypeChecks(
nullptr, *webview->GetPrimaryMainFrame()->GetProcess(),
{mojom::ContextType::kWebPage, mojom::ContextType::kUntrustedWebUi},
"webview process without extension passed");
}
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest,
IsPrivilegedExtensionProcess_UserScripts) {
const Extension* extension =
AddExtensionWithHostPermission("test", "*://example.com/*");
ASSERT_TRUE(extension);
OpenDomain("example.com");
ExecuteUserScriptInActiveTab(extension->id());
EXPECT_FALSE(process_map()->IsPrivilegedExtensionProcess(
*extension, GetActiveMainFrameProcessID()));
}
IN_PROC_BROWSER_TEST_F(ProcessMapBrowserTest, CanHostContextType_UserScripts) {
const Extension* extension =
AddExtensionWithHostPermission("test", "*://example.com/*");
ASSERT_TRUE(extension);
OpenDomain("example.com");
ExecuteUserScriptInActiveTab(extension->id());
content::RenderProcessHost& web_page_process = GetActiveMainFrameProcess();
RunCanProcessHostContextTypeChecks(
extension, web_page_process,
{mojom::ContextType::kContentScript, mojom::ContextType::kUserScript},
"page with injected user script with extension passed");
RunCanProcessHostContextTypeChecks(
nullptr, web_page_process,
{mojom::ContextType::kWebPage, mojom::ContextType::kUntrustedWebUi},
"page with injected user script without extension passed");
}
} // namespace extensions