Block tabs.executeScript() from executing until user grants permission

Prevent extensions with <all_urls> from executing scripts using executeScript()
without user consent if the scripts-require-action switch is on.
Coming up next: Content scripts.

BUG=362353

Review URL: https://codereview.chromium.org/286003004

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@271528 0039d316-1c4b-4281-b951-d872f2087c98
parent 80aae3ab
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
#include "chrome/browser/extensions/active_script_controller.h" #include "chrome/browser/extensions/active_script_controller.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/scoped_ptr.h"
#include "base/metrics/histogram.h" #include "base/metrics/histogram.h"
#include "base/stl_util.h" #include "base/stl_util.h"
#include "chrome/browser/extensions/extension_action.h" #include "chrome/browser/extensions/extension_action.h"
...@@ -12,6 +15,7 @@ ...@@ -12,6 +15,7 @@
#include "chrome/common/extensions/api/extension_action/action_info.h" #include "chrome/common/extensions/api/extension_action/action_info.h"
#include "content/public/browser/navigation_controller.h" #include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h" #include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "extensions/browser/extension_registry.h" #include "extensions/browser/extension_registry.h"
#include "extensions/common/extension.h" #include "extensions/common/extension.h"
...@@ -23,10 +27,25 @@ ...@@ -23,10 +27,25 @@
namespace extensions { namespace extensions {
ActiveScriptController::PendingRequest::PendingRequest() :
page_id(-1) {
}
ActiveScriptController::PendingRequest::PendingRequest(
const base::Closure& closure,
int page_id)
: closure(closure),
page_id(page_id) {
}
ActiveScriptController::PendingRequest::~PendingRequest() {
}
ActiveScriptController::ActiveScriptController( ActiveScriptController::ActiveScriptController(
content::WebContents* web_contents) content::WebContents* web_contents)
: content::WebContentsObserver(web_contents), : content::WebContentsObserver(web_contents),
enabled_(FeatureSwitch::scripts_require_action()->IsEnabled()) { enabled_(FeatureSwitch::scripts_require_action()->IsEnabled()) {
CHECK(web_contents);
} }
ActiveScriptController::~ActiveScriptController() { ActiveScriptController::~ActiveScriptController() {
...@@ -48,43 +67,51 @@ ActiveScriptController* ActiveScriptController::GetForWebContents( ...@@ -48,43 +67,51 @@ ActiveScriptController* ActiveScriptController::GetForWebContents(
return location_bar_controller->active_script_controller(); return location_bar_controller->active_script_controller();
} }
void ActiveScriptController::NotifyScriptExecuting( bool ActiveScriptController::RequiresUserConsentForScriptInjection(
const std::string& extension_id, int page_id) { const Extension* extension) {
content::NavigationEntry* visible_entry = CHECK(extension);
web_contents()->GetController().GetVisibleEntry(); if (!PermissionsData::RequiresActionForScriptExecution(extension))
if (!visible_entry || return false;
extensions_executing_scripts_.count(extension_id) ||
visible_entry->GetPageID() != page_id) {
return;
}
const Extension* extension = // If the feature is not enabled, we automatically allow all extensions to
ExtensionRegistry::Get(web_contents()->GetBrowserContext()) // run scripts.
->enabled_extensions().GetByID(extension_id); if (!enabled_)
if (extension && permitted_extensions_.insert(extension->id());
PermissionsData::RequiresActionForScriptExecution(extension)) {
extensions_executing_scripts_.insert(extension_id); return permitted_extensions_.count(extension->id()) == 0;
}
void ActiveScriptController::RequestScriptInjection(
const Extension* extension,
int page_id,
const base::Closure& callback) {
CHECK(extension);
PendingRequestList& list = pending_requests_[extension->id()];
list.push_back(PendingRequest(callback, page_id));
// If this was the first entry, notify the location bar that there's a new
// icon.
if (list.size() == 1u)
LocationBarController::NotifyChange(web_contents()); LocationBarController::NotifyChange(web_contents());
}
} }
void ActiveScriptController::OnAdInjectionDetected( void ActiveScriptController::OnAdInjectionDetected(
const std::vector<std::string> ad_injectors) { const std::vector<std::string> ad_injectors) {
size_t num_preventable_ad_injectors = size_t num_preventable_ad_injectors =
base::STLSetIntersection<std::set<std::string> >( base::STLSetIntersection<std::set<std::string> >(
ad_injectors, extensions_executing_scripts_).size(); ad_injectors, permitted_extensions_).size();
UMA_HISTOGRAM_COUNTS_100( UMA_HISTOGRAM_COUNTS_100(
"Extensions.ActiveScriptController.PreventableAdInjectors", "Extensions.ActiveScriptController.PreventableAdInjectors",
num_preventable_ad_injectors); num_preventable_ad_injectors);
UMA_HISTOGRAM_COUNTS_100( UMA_HISTOGRAM_COUNTS_100(
"Extensions.ActiveScriptController.PreventableAdInjectors", "Extensions.ActiveScriptController.UnpreventableAdInjectors",
ad_injectors.size() - num_preventable_ad_injectors); ad_injectors.size() - num_preventable_ad_injectors);
} }
ExtensionAction* ActiveScriptController::GetActionForExtension( ExtensionAction* ActiveScriptController::GetActionForExtension(
const Extension* extension) { const Extension* extension) {
if (!enabled_ || extensions_executing_scripts_.count(extension->id()) == 0) if (!enabled_ || pending_requests_.count(extension->id()) == 0)
return NULL; // No action for this extension. return NULL; // No action for this extension.
ActiveScriptMap::iterator existing = ActiveScriptMap::iterator existing =
...@@ -112,13 +139,70 @@ ExtensionAction* ActiveScriptController::GetActionForExtension( ...@@ -112,13 +139,70 @@ ExtensionAction* ActiveScriptController::GetActionForExtension(
LocationBarController::Action ActiveScriptController::OnClicked( LocationBarController::Action ActiveScriptController::OnClicked(
const Extension* extension) { const Extension* extension) {
DCHECK(extensions_executing_scripts_.count(extension->id()) > 0); DCHECK(extension);
PendingRequestMap::iterator iter =
pending_requests_.find(extension->id());
DCHECK(iter != pending_requests_.end());
content::NavigationEntry* visible_entry =
web_contents()->GetController().GetVisibleEntry();
// Refuse to run if there's no visible entry, because we have no idea of
// determining if it's the proper page. This should rarely, if ever, happen.
if (!visible_entry)
return LocationBarController::ACTION_NONE;
int page_id = visible_entry->GetPageID();
// We add this to the list of permitted extensions and erase pending entries
// *before* running them to guard against the crazy case where running the
// callbacks adds more entries.
permitted_extensions_.insert(extension->id());
PendingRequestList requests;
iter->second.swap(requests);
pending_requests_.erase(extension->id());
// Run all pending injections for the given extension.
for (PendingRequestList::iterator request = requests.begin();
request != requests.end();
++request) {
// Only run if it's on the proper page.
if (request->page_id == page_id)
request->closure.Run();
}
// Inform the location bar that the action is now gone.
LocationBarController::NotifyChange(web_contents());
return LocationBarController::ACTION_NONE; return LocationBarController::ACTION_NONE;
} }
void ActiveScriptController::OnNavigated() { void ActiveScriptController::OnNavigated() {
LogUMA(); LogUMA();
extensions_executing_scripts_.clear(); permitted_extensions_.clear();
pending_requests_.clear();
}
void ActiveScriptController::OnNotifyExtensionScriptExecution(
const std::string& extension_id,
int page_id) {
if (!Extension::IdIsValid(extension_id)) {
NOTREACHED() << "'" << extension_id << "' is not a valid id.";
return;
}
const Extension* extension =
ExtensionRegistry::Get(web_contents()->GetBrowserContext())
->enabled_extensions().GetByID(extension_id);
// We shouldn't allow extensions which are no longer enabled to run any
// scripts. Ignore the request.
if (!extension)
return;
// Right now, we allow all content scripts to execute, but notify the
// controller of them.
// TODO(rdevlin.cronin): Fix this in a future CL.
if (RequiresUserConsentForScriptInjection(extension))
RequestScriptInjection(extension, page_id, base::Bind(&base::DoNothing));
} }
bool ActiveScriptController::OnMessageReceived(const IPC::Message& message) { bool ActiveScriptController::OnMessageReceived(const IPC::Message& message) {
...@@ -131,20 +215,21 @@ bool ActiveScriptController::OnMessageReceived(const IPC::Message& message) { ...@@ -131,20 +215,21 @@ bool ActiveScriptController::OnMessageReceived(const IPC::Message& message) {
return handled; return handled;
} }
void ActiveScriptController::OnNotifyExtensionScriptExecution(
const std::string& extension_id,
int page_id) {
if (!Extension::IdIsValid(extension_id)) {
NOTREACHED() << "'" << extension_id << "' is not a valid id.";
return;
}
NotifyScriptExecuting(extension_id, page_id);
}
void ActiveScriptController::LogUMA() const { void ActiveScriptController::LogUMA() const {
UMA_HISTOGRAM_COUNTS_100( UMA_HISTOGRAM_COUNTS_100(
"Extensions.ActiveScriptController.ShownActiveScriptsOnPage", "Extensions.ActiveScriptController.ShownActiveScriptsOnPage",
extensions_executing_scripts_.size()); pending_requests_.size());
// We only log the permitted extensions metric if the feature is enabled,
// because otherwise the data will be boring (100% allowed).
if (enabled_) {
UMA_HISTOGRAM_COUNTS_100(
"Extensions.ActiveScriptController.PermittedExtensions",
permitted_extensions_.size());
UMA_HISTOGRAM_COUNTS_100(
"Extensions.ActiveScriptController.DeniedExtensions",
pending_requests_.size());
}
} }
} // namespace extensions } // namespace extensions
...@@ -8,7 +8,9 @@ ...@@ -8,7 +8,9 @@
#include <map> #include <map>
#include <set> #include <set>
#include <string> #include <string>
#include <vector>
#include "base/callback.h"
#include "base/compiler_specific.h" #include "base/compiler_specific.h"
#include "base/memory/linked_ptr.h" #include "base/memory/linked_ptr.h"
#include "chrome/browser/extensions/location_bar_controller.h" #include "chrome/browser/extensions/location_bar_controller.h"
...@@ -42,10 +44,17 @@ class ActiveScriptController : public LocationBarController::ActionProvider, ...@@ -42,10 +44,17 @@ class ActiveScriptController : public LocationBarController::ActionProvider,
static ActiveScriptController* GetForWebContents( static ActiveScriptController* GetForWebContents(
content::WebContents* web_contents); content::WebContents* web_contents);
// Notify the ActiveScriptController that an extension is running a script. // Returns true if the extension requesting script injection requires
// TODO(rdevlin.cronin): Soon, this should be ask the user for permission, // user consent. If this is true, the caller should then register a request
// rather than simply notifying them. // via RequestScriptInjection().
void NotifyScriptExecuting(const std::string& extension_id, int page_id); bool RequiresUserConsentForScriptInjection(const Extension* extension);
// Register a request for a script injection, to be executed by running
// |callback|. The only assumption that can be made about when (or if)
// |callback| is run is that, if it is run, it will run on the current page.
void RequestScriptInjection(const Extension* extension,
int page_id,
const base::Closure& callback);
// Notifies the ActiveScriptController of detected ad injection. // Notifies the ActiveScriptController of detected ad injection.
void OnAdInjectionDetected(const std::vector<std::string> ad_injectors); void OnAdInjectionDetected(const std::vector<std::string> ad_injectors);
...@@ -58,13 +67,26 @@ class ActiveScriptController : public LocationBarController::ActionProvider, ...@@ -58,13 +67,26 @@ class ActiveScriptController : public LocationBarController::ActionProvider,
virtual void OnNavigated() OVERRIDE; virtual void OnNavigated() OVERRIDE;
private: private:
// content::WebContentsObserver implementation. // A single pending request. This could be a pair, but we'd have way too many
virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE; // stl typedefs, and "request.closure" is nicer than "request.first".
struct PendingRequest {
// Handle the NotifyExtensionScriptExecution message. PendingRequest(); // For STL.
PendingRequest(const base::Closure& closure, int page_id);
~PendingRequest();
base::Closure closure;
int page_id;
};
typedef std::vector<PendingRequest> PendingRequestList;
typedef std::map<std::string, PendingRequestList> PendingRequestMap;
// Handles the NotifyExtensionScriptExecution message.
void OnNotifyExtensionScriptExecution(const std::string& extension_id, void OnNotifyExtensionScriptExecution(const std::string& extension_id,
int page_id); int page_id);
// content::WebContentsObserver implementation.
virtual bool OnMessageReceived(const IPC::Message& message) OVERRIDE;
// Log metrics. // Log metrics.
void LogUMA() const; void LogUMA() const;
...@@ -73,11 +95,14 @@ class ActiveScriptController : public LocationBarController::ActionProvider, ...@@ -73,11 +95,14 @@ class ActiveScriptController : public LocationBarController::ActionProvider,
// always allowing scripts to run and never displaying actions. // always allowing scripts to run and never displaying actions.
bool enabled_; bool enabled_;
// The extensions that have called ExecuteScript on the current frame. // The map of extension_id:pending_request of all pending requests.
std::set<std::string> extensions_executing_scripts_; PendingRequestMap pending_requests_;
// The extensions which have injected ads. // The extensions which have been granted permission to run on the given page.
std::set<std::string> ad_injectors_; // TODO(rdevlin.cronin): Right now, this just keeps track of extensions that
// have been permitted to run on the page via this interface. Instead, it
// should incorporate more fully with ActiveTab.
std::set<std::string> permitted_extensions_;
// Script badges that have been generated for extensions. This is both those // Script badges that have been generated for extensions. This is both those
// with actions already declared that are copied and normalised, and actions // with actions already declared that are copied and normalised, and actions
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#include "chrome/browser/extensions/script_executor.h" #include "chrome/browser/extensions/script_executor.h"
#include "base/bind.h"
#include "base/callback.h" #include "base/callback.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/pickle.h" #include "base/pickle.h"
...@@ -14,6 +15,7 @@ ...@@ -14,6 +15,7 @@
#include "content/public/browser/render_view_host.h" #include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_observer.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension_messages.h" #include "extensions/common/extension_messages.h"
#include "ipc/ipc_message.h" #include "ipc/ipc_message.h"
#include "ipc/ipc_message_macros.h" #include "ipc/ipc_message_macros.h"
...@@ -127,33 +129,64 @@ void ScriptExecutor::ExecuteScript(const std::string& extension_id, ...@@ -127,33 +129,64 @@ void ScriptExecutor::ExecuteScript(const std::string& extension_id,
bool user_gesture, bool user_gesture,
ScriptExecutor::ResultType result_type, ScriptExecutor::ResultType result_type,
const ExecuteScriptCallback& callback) { const ExecuteScriptCallback& callback) {
ActiveScriptController* active_script_controller = // Don't execute if the extension has been unloaded.
ActiveScriptController::GetForWebContents(web_contents_); const Extension* extension =
ExtensionRegistry::Get(web_contents_->GetBrowserContext())
->enabled_extensions().GetByID(extension_id);
if (!extension)
return;
// Don't execute if there's no visible entry. If this is the case, then our
// permissions checking is useless (because we can't evaluate the URL).
// TODO(rdevlin.cronin): This might be better somewhere higher up the
// callstack, but we know it's caught here.
content::NavigationEntry* visible_entry = content::NavigationEntry* visible_entry =
web_contents_->GetController().GetVisibleEntry(); web_contents_->GetController().GetVisibleEntry();
if (active_script_controller && visible_entry) { if (!visible_entry)
// TODO(rdevlin.cronin): Now, this is just a notification. Soon, it should return;
// block until the user gives the OK to execute.
active_script_controller->NotifyScriptExecuting(extension_id, scoped_ptr<ExtensionMsg_ExecuteCode_Params> params(
visible_entry->GetPageID()); new ExtensionMsg_ExecuteCode_Params());
params->request_id = next_request_id_++;
params->extension_id = extension_id;
params->is_javascript = (script_type == JAVASCRIPT);
params->code = code;
params->all_frames = (frame_scope == ALL_FRAMES);
params->match_about_blank = (about_blank == MATCH_ABOUT_BLANK);
params->run_at = static_cast<int>(run_at);
params->in_main_world = (world_type == MAIN_WORLD);
params->is_web_view = (process_type == WEB_VIEW_PROCESS);
params->webview_src = webview_src;
params->file_url = file_url;
params->wants_result = (result_type == JSON_SERIALIZED_RESULT);
params->user_gesture = user_gesture;
ActiveScriptController* active_script_controller =
ActiveScriptController::GetForWebContents(web_contents_);
if (active_script_controller &&
active_script_controller->RequiresUserConsentForScriptInjection(
extension)) {
// The base::Unretained(this) is safe, because this and the
// ActiveScriptController are both attached to the TabHelper. Thus, if the
// ActiveScriptController is still alive to invoke the callback, this is
// alive, too.
active_script_controller->RequestScriptInjection(
extension,
visible_entry->GetPageID(),
base::Closure(base::Bind(&ScriptExecutor::ExecuteScriptHelper,
base::Unretained(this),
base::Passed(params.Pass()),
callback)));
} else {
ExecuteScriptHelper(params.Pass(), callback);
} }
ExtensionMsg_ExecuteCode_Params params; }
params.request_id = next_request_id_++;
params.extension_id = extension_id;
params.is_javascript = (script_type == JAVASCRIPT);
params.code = code;
params.all_frames = (frame_scope == ALL_FRAMES);
params.match_about_blank = (about_blank == MATCH_ABOUT_BLANK);
params.run_at = static_cast<int>(run_at);
params.in_main_world = (world_type == MAIN_WORLD);
params.is_web_view = (process_type == WEB_VIEW_PROCESS);
params.webview_src = webview_src;
params.file_url = file_url;
params.wants_result = (result_type == JSON_SERIALIZED_RESULT);
params.user_gesture = user_gesture;
void ScriptExecutor::ExecuteScriptHelper(
scoped_ptr<ExtensionMsg_ExecuteCode_Params> params,
const ExecuteScriptCallback& callback) {
// Handler handles IPCs and deletes itself on completion. // Handler handles IPCs and deletes itself on completion.
new Handler(script_observers_, web_contents_, params, callback); new Handler(script_observers_, web_contents_, *params, callback);
} }
} // namespace extensions } // namespace extensions
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
#include "extensions/common/user_script.h" #include "extensions/common/user_script.h"
class GURL; class GURL;
struct ExtensionMsg_ExecuteCode_Params;
namespace base { namespace base {
class ListValue; class ListValue;
...@@ -101,6 +102,10 @@ class ScriptExecutor { ...@@ -101,6 +102,10 @@ class ScriptExecutor {
const ExecuteScriptCallback& callback); const ExecuteScriptCallback& callback);
private: private:
// Called upon a request being given to execute the script.
void ExecuteScriptHelper(scoped_ptr<ExtensionMsg_ExecuteCode_Params> params,
const ExecuteScriptCallback& callback);
// The next value to use for request_id in ExtensionMsg_ExecuteCode_Params. // The next value to use for request_id in ExtensionMsg_ExecuteCode_Params.
int next_request_id_; int next_request_id_;
......
// Copyright 2014 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.
chrome.test.sendMessage('content_scripts_all_hosts');
{
"name": "Active Script Test: Content Scripts All Hosts",
"description": "An extension with all hosts that has a content script.",
"version": "2.0",
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["content_script.js"]
}
],
"manifest_version": 2
}
// Copyright 2014 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.
chrome.test.sendMessage('content_scripts_explicit_hosts');
{
"name": "Active Script Test: Content Scripts Explicit Hosts",
"description": "An extension with explicit hosts that has a content script.",
"version": "2.0",
"content_scripts": [
{
"matches": ["http://127.0.0.1/*"],
"js": ["content_script.js"]
}
],
"manifest_version": 2
}
// Copyright 2014 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.
chrome.tabs.onUpdated.addListener(function(tabId) {
chrome.tabs.executeScript(
tabId,
{ code: 'chrome.test.sendMessage("inject_scripts_all_hosts");' });
});
{
"name": "Active Script Test: Inject Scripts All Hosts",
"description": "An extension with all hosts that injects scripts.",
"version": "1.5",
"background": {
"scripts": ["background.js"]
},
"permissions": [
"tabs", "*://*/*"
],
"manifest_version": 2
}
...@@ -5962,6 +5962,26 @@ Therefore, the affected-histogram name has to have at least one dot in it. ...@@ -5962,6 +5962,26 @@ Therefore, the affected-histogram name has to have at least one dot in it.
</summary> </summary>
</histogram> </histogram>
<histogram name="Extensions.ActiveScriptController.DeniedExtensions"
units="Extension Count">
<owner>kalman@chromium.org</owner>
<owner>rdevlin.cronin@chromium.org</owner>
<summary>
The number of extensions on a page that wanted to execute a script, required
explicit user consent, and were denied permission.
</summary>
</histogram>
<histogram name="Extensions.ActiveScriptController.PermittedExtensions"
units="Extension Count">
<owner>kalman@chromium.org</owner>
<owner>rdevlin.cronin@chromium.org</owner>
<summary>
The number of extensions on a page that wanted to execute a script, required
explicit user consent, and were granted permission.
</summary>
</histogram>
<histogram name="Extensions.ActiveScriptController.PreventableAdInjectors" <histogram name="Extensions.ActiveScriptController.PreventableAdInjectors"
units="Extension Count"> units="Extension Count">
<owner>kalman@chromium.org</owner> <owner>kalman@chromium.org</owner>
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