Commit 618e202b authored by Devlin Cronin's avatar Devlin Cronin Committed by Commit Bot

[Extensions] Add support for allFrames in scripting.executeScript()

Add support for an allFrames boolean in the scripting.executeScript()
API. If specified, the code is executed in all frames within the tab
(that the extension has permission to access).

Bug: 1144839
Change-Id: Idecc69cf9599dfc821e2d309078324a6e34b0a59
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2532901Reviewed-by: default avatarDavid Bertoni <dbertoni@chromium.org>
Commit-Queue: Devlin <rdevlin.cronin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#827032}
parent 6afb5772
...@@ -65,11 +65,12 @@ bool HasPermissionToInject(const PermissionsData& permissions, ...@@ -65,11 +65,12 @@ bool HasPermissionToInject(const PermissionsData& permissions,
void ExecuteScript(ScriptExecutor* script_executor, void ExecuteScript(ScriptExecutor* script_executor,
const std::string& code, const std::string& code,
const Extension& extension, const Extension& extension,
ScriptExecutor::FrameScope frame_scope,
bool user_gesture, bool user_gesture,
ScriptExecutor::ScriptFinishedCallback callback) { ScriptExecutor::ScriptFinishedCallback callback) {
script_executor->ExecuteScript( script_executor->ExecuteScript(
HostID(HostID::EXTENSIONS, extension.id()), UserScript::ADD_JAVASCRIPT, HostID(HostID::EXTENSIONS, extension.id()), UserScript::ADD_JAVASCRIPT,
code, ScriptExecutor::SINGLE_FRAME, ExtensionApiFrameIdMap::kTopFrameId, code, frame_scope, ExtensionApiFrameIdMap::kTopFrameId,
ScriptExecutor::MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE, ScriptExecutor::MATCH_ABOUT_BLANK, UserScript::DOCUMENT_IDLE,
ScriptExecutor::DEFAULT_PROCESS, ScriptExecutor::DEFAULT_PROCESS,
/* webview_src */ GURL(), /* script_url */ GURL(), user_gesture, /* webview_src */ GURL(), /* script_url */ GURL(), user_gesture,
...@@ -103,9 +104,20 @@ ExtensionFunction::ResponseAction ScriptingExecuteScriptFunction::Run() { ...@@ -103,9 +104,20 @@ ExtensionFunction::ResponseAction ScriptingExecuteScriptFunction::Run() {
DCHECK(script_executor); DCHECK(script_executor);
std::string error; std::string error;
if (!HasPermissionToInject(*extension()->permissions_data(),
injection.target.tab_id, tab, &error)) { ScriptExecutor::FrameScope frame_scope =
return RespondNow(Error(std::move(error))); injection.target.all_frames && *injection.target.all_frames == true
? ScriptExecutor::INCLUDE_SUB_FRAMES
: ScriptExecutor::SINGLE_FRAME;
// TODO(devlin): It'd be best to do all the permission checks for the frames
// on the browser side, including child frames. Today, we only check the
// parent frame, and then let the ScriptExecutor inject into all child frames
// (there's a permission check at the time of the injection).
if (frame_scope == ScriptExecutor::SINGLE_FRAME) {
if (!HasPermissionToInject(*extension()->permissions_data(),
injection.target.tab_id, tab, &error)) {
return RespondNow(Error(std::move(error)));
}
} }
EXTENSION_FUNCTION_VALIDATE(injection.function); EXTENSION_FUNCTION_VALIDATE(injection.function);
...@@ -117,7 +129,8 @@ ExtensionFunction::ResponseAction ScriptingExecuteScriptFunction::Run() { ...@@ -117,7 +129,8 @@ ExtensionFunction::ResponseAction ScriptingExecuteScriptFunction::Run() {
base::StringPrintf("(%s)()", injection.function->c_str()); base::StringPrintf("(%s)()", injection.function->c_str());
ExecuteScript( ExecuteScript(
script_executor, code_to_execute, *extension(), user_gesture(), script_executor, code_to_execute, *extension(), frame_scope,
user_gesture(),
base::BindOnce(&ScriptingExecuteScriptFunction::OnScriptExecuted, this)); base::BindOnce(&ScriptingExecuteScriptFunction::OnScriptExecuted, this));
return RespondLater(); return RespondLater();
...@@ -129,9 +142,6 @@ void ScriptingExecuteScriptFunction::OnScriptExecuted( ...@@ -129,9 +142,6 @@ void ScriptingExecuteScriptFunction::OnScriptExecuted(
const base::ListValue& result) { const base::ListValue& result) {
std::vector<api::scripting::InjectionResult> injection_results; std::vector<api::scripting::InjectionResult> injection_results;
// TODO(devlin): Remove this check when we support multiple frame injection.
DCHECK_EQ(1u, result.GetList().size());
// TODO(devlin): This results in a few copies of values. It'd be better if our // TODO(devlin): This results in a few copies of values. It'd be better if our
// auto-generated code supported moved-in parameters for result construction. // auto-generated code supported moved-in parameters for result construction.
for (const auto& value : result.GetList()) { for (const auto& value : result.GetList()) {
......
...@@ -27,6 +27,7 @@ class ScriptingAPITest : public ExtensionApiTest { ...@@ -27,6 +27,7 @@ class ScriptingAPITest : public ExtensionApiTest {
void SetUpOnMainThread() override { void SetUpOnMainThread() override {
ExtensionApiTest::SetUpOnMainThread(); ExtensionApiTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1"); host_resolver()->AddRule("*", "127.0.0.1");
content::SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(StartEmbeddedTestServer()); ASSERT_TRUE(StartEmbeddedTestServer());
} }
...@@ -71,4 +72,41 @@ IN_PROC_BROWSER_TEST_F(ScriptingAPITest, MainFrameTests) { ...@@ -71,4 +72,41 @@ IN_PROC_BROWSER_TEST_F(ScriptingAPITest, MainFrameTests) {
<< message_; << message_;
} }
IN_PROC_BROWSER_TEST_F(ScriptingAPITest, SubFramesTests) {
// Open up two tabs, each with cross-site iframes, one at a.com and one at
// d.com.
// In both cases, the cross-site iframes point to b.com and c.com.
{
const GURL a_com =
embedded_test_server()->GetURL("a.com", "/iframe_cross_site.html");
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
ASSERT_TRUE(web_contents);
content::TestNavigationObserver nav_observer(web_contents);
ui_test_utils::NavigateToURL(browser(), a_com);
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(a_com, web_contents->GetLastCommittedURL());
}
{
const GURL d_com =
embedded_test_server()->GetURL("d.com", "/iframe_cross_site.html");
content::TestNavigationObserver nav_observer(d_com);
nav_observer.StartWatchingNewWebContents();
ui_test_utils::NavigateToURLWithDisposition(
browser(), d_com, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
nav_observer.Wait();
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(d_com, browser()
->tab_strip_model()
->GetActiveWebContents()
->GetLastCommittedURL());
}
// From there, the test continues in the JS.
ASSERT_TRUE(RunExtensionTestIgnoreManifestWarnings("scripting/sub_frames"));
}
} // namespace extensions } // namespace extensions
...@@ -10,6 +10,9 @@ namespace scripting { ...@@ -10,6 +10,9 @@ namespace scripting {
dictionary InjectionTarget { dictionary InjectionTarget {
// The ID of the tab into which to inject. // The ID of the tab into which to inject.
long tabId; long tabId;
// Whether the script should inject into all frames within the tab.
boolean? allFrames;
}; };
dictionary ScriptInjection { dictionary ScriptInjection {
......
{
"name": "Scripting API Test",
"manifest_version": 3,
"version": "0.1",
"permissions": ["scripting", "tabs"],
"background": {"service_worker": "worker.js"},
"host_permissions": ["http://a.com/*", "http://b.com/*"]
}
// Copyright 2020 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.
function injectedFunction() {
return location.href;
}
async function getSingleTab(query) {
const tabs = await new Promise(resolve => {
chrome.tabs.query(query, resolve);
});
chrome.test.assertEq(1, tabs.length);
return tabs[0];
}
chrome.test.runTests([
async function allowedTopFrameAccess() {
const query = {url: 'http://a.com/*'};
let tab = await getSingleTab(query);
const results = await new Promise(resolve => {
chrome.scripting.executeScript(
{
target: {
tabId: tab.id,
allFrames: true,
},
function: injectedFunction,
},
resolve);
});
chrome.test.assertNoLastError();
chrome.test.assertEq(2, results.length);
// Note: The 'a.com' result is guaranteed to be first, since it's the root
// frame.
const url1 = new URL(results[0].result);
chrome.test.assertEq('a.com', url1.hostname);
const url2 = new URL(results[1].result);
chrome.test.assertEq('b.com', url2.hostname);
chrome.test.succeed();
},
async function disallowedTopFrameAccess() {
const query = {url: 'http://d.com/*'};
let tab = await getSingleTab(query);
const results = await new Promise(resolve => {
chrome.scripting.executeScript(
{
target: {
tabId: tab.id,
allFrames: true,
},
function: injectedFunction,
},
resolve);
});
chrome.test.assertNoLastError();
// The extension doesn't have access to the top frame (d.com), but does to
// one of the subframes (b.com). This injection should succeed (leading to a
// single result).
chrome.test.assertEq(1, results.length);
const url1 = new URL(results[0].result);
chrome.test.assertEq('b.com', url1.hostname);
chrome.test.succeed();
},
]);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment