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
......
...@@ -2,13 +2,16 @@ ...@@ -2,13 +2,16 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
#include "base/files/file_path.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/strings/stringprintf.h"
#include "chrome/browser/extensions/active_script_controller.h" #include "chrome/browser/extensions/active_script_controller.h"
#include "chrome/browser/extensions/extension_action.h" #include "chrome/browser/extensions/extension_action.h"
#include "chrome/browser/extensions/extension_browsertest.h" #include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_test_message_listener.h" #include "chrome/browser/extensions/extension_test_message_listener.h"
#include "chrome/browser/extensions/location_bar_controller.h" #include "chrome/browser/extensions/location_bar_controller.h"
#include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/extensions/test_extension_dir.h"
#include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h" #include "chrome/test/base/ui_test_utils.h"
...@@ -18,113 +21,344 @@ ...@@ -18,113 +21,344 @@
namespace extensions { namespace extensions {
namespace {
const char kAllHostsScheme[] = "*://*/*";
const char kExplicitHostsScheme[] = "http://127.0.0.1/*";
const char kBackgroundScript[] =
"\"background\": {\"scripts\": [\"script.js\"]}";
const char kBackgroundScriptSource[] =
"var listener = function(tabId) {\n"
" chrome.tabs.onUpdated.removeListener(listener);\n"
" chrome.tabs.executeScript(tabId, {\n"
" code: \"chrome.test.sendMessage('inject succeeded');\"\n"
" });"
" chrome.test.sendMessage('inject attempted');\n"
"};\n"
"chrome.tabs.onUpdated.addListener(listener);";
const char kContentScriptSource[] =
"chrome.test.sendMessage('inject succeeded');";
const char kInjectAttempted[] = "inject attempted";
const char kInjectSucceeded[] = "inject succeeded";
enum InjectionType {
CONTENT_SCRIPT,
EXECUTE_SCRIPT
};
enum HostType {
ALL_HOSTS,
EXPLICIT_HOSTS
};
enum RequiresConsent {
REQUIRES_CONSENT,
DOES_NOT_REQUIRE_CONSENT
};
} // namespace
class ActiveScriptControllerBrowserTest : public ExtensionBrowserTest { class ActiveScriptControllerBrowserTest : public ExtensionBrowserTest {
public: public:
ActiveScriptControllerBrowserTest() ActiveScriptControllerBrowserTest()
: feature_override_(FeatureSwitch::scripts_require_action(), : feature_override_(FeatureSwitch::scripts_require_action(),
FeatureSwitch::OVERRIDE_ENABLED) {} FeatureSwitch::OVERRIDE_ENABLED) {}
virtual void CleanUpOnMainThread() OVERRIDE;
// Returns an extension with the given |host_type| and |injection_type|. If
// one already exists, the existing extension will be returned. Othewrwise,
// one will be created.
// This could potentially return NULL if LoadExtension() fails.
const Extension* GetOrCreateExtension(HostType host_type,
InjectionType injection_type);
private: private:
FeatureSwitch::ScopedOverride feature_override_; FeatureSwitch::ScopedOverride feature_override_;
ScopedVector<TestExtensionDir> test_extension_dirs_;
std::vector<const Extension*> extensions_;
}; };
void ActiveScriptControllerBrowserTest::CleanUpOnMainThread() {
test_extension_dirs_.clear();
}
const Extension* ActiveScriptControllerBrowserTest::GetOrCreateExtension(
HostType host_type, InjectionType injection_type) {
std::string name =
base::StringPrintf(
"%s %s",
injection_type == CONTENT_SCRIPT ?
"content_script" : "execute_script",
host_type == ALL_HOSTS ? "all_hosts" : "explicit_hosts");
for (std::vector<const Extension*>::const_iterator iter = extensions_.begin();
iter != extensions_.end();
++iter) {
if ((*iter)->name() == name)
return *iter;
}
const char* permission_scheme =
host_type == ALL_HOSTS ? kAllHostsScheme : kExplicitHostsScheme;
std::string permissions = base::StringPrintf(
"\"permissions\": [\"tabs\", \"%s\"]", permission_scheme);
std::string scripts;
std::string script_source;
if (injection_type == CONTENT_SCRIPT) {
scripts = base::StringPrintf(
"\"content_scripts\": ["
" {"
" \"matches\": [\"%s\"],"
" \"js\": [\"script.js\"],"
" \"run_at\": \"document_start\""
" }"
"]",
permission_scheme);
} else {
scripts = kBackgroundScript;
}
std::string manifest = base::StringPrintf(
"{"
" \"name\": \"%s\","
" \"version\": \"1.0\","
" \"manifest_version\": 2,"
" %s,"
" %s"
"}",
name.c_str(),
permissions.c_str(),
scripts.c_str());
scoped_ptr<TestExtensionDir> dir(new TestExtensionDir);
dir->WriteManifest(manifest);
dir->WriteFile(FILE_PATH_LITERAL("script.js"),
injection_type == CONTENT_SCRIPT ? kContentScriptSource :
kBackgroundScriptSource);
const Extension* extension = LoadExtension(dir->unpacked_path());
if (extension) {
test_extension_dirs_.push_back(dir.release());
extensions_.push_back(extension);
}
// If extension is NULL here, it will be caught later in the test.
return extension;
}
class ActiveScriptTester { class ActiveScriptTester {
public: public:
ActiveScriptTester(const std::string& name, ActiveScriptTester(const std::string& name,
const Extension* extension, const Extension* extension,
bool expect_has_action); Browser* browser,
RequiresConsent requires_consent,
InjectionType type);
~ActiveScriptTester(); ~ActiveScriptTester();
testing::AssertionResult Verify(Browser* browser); testing::AssertionResult Verify();
private: private:
// Returns the location bar controller, or NULL if one does not exist.
LocationBarController* GetLocationBarController();
// Returns the active script controller, or NULL if one does not exist.
ActiveScriptController* GetActiveScriptController();
// Get the ExtensionAction for this extension, or NULL if one does not exist.
ExtensionAction* GetAction();
// The name of the extension, and also the message it sends. // The name of the extension, and also the message it sends.
std::string name_; std::string name_;
// The extension associated with this tester. // The extension associated with this tester.
const Extension* extension_; const Extension* extension_;
// Whether or not we expect the extension to have an action for script // The browser the tester is running in.
// injection. Browser* browser_;
bool expect_has_action_;
// Whether or not the extension has permission to run the script without
// asking the user.
RequiresConsent requires_consent_;
// The type of injection this tester uses.
InjectionType type_;
// All of these extensions should inject a script (either through content // All of these extensions should inject a script (either through content
// scripts or through chrome.tabs.executeScript()) that sends a message with // scripts or through chrome.tabs.executeScript()) that sends a message with
// their names (for verification). // the |kInjectSucceeded| message.
// Be prepared to wait for each extension to inject the script. linked_ptr<ExtensionTestMessageListener> inject_attempt_listener_;
linked_ptr<ExtensionTestMessageListener> listener_;
// After trying to inject the script, extensions sending the script via
// chrome.tabs.executeScript() send a |kInjectAttempted| message.
linked_ptr<ExtensionTestMessageListener> inject_success_listener_;
}; };
ActiveScriptTester::ActiveScriptTester(const std::string& name, ActiveScriptTester::ActiveScriptTester(const std::string& name,
const Extension* extension, const Extension* extension,
bool expect_has_action) Browser* browser,
RequiresConsent requires_consent,
InjectionType type)
: name_(name), : name_(name),
extension_(extension), extension_(extension),
expect_has_action_(expect_has_action), browser_(browser),
listener_( requires_consent_(requires_consent),
new ExtensionTestMessageListener(name, false /* won't reply */)) { type_(type),
inject_attempt_listener_(
new ExtensionTestMessageListener(kInjectAttempted,
false /* won't reply */)),
inject_success_listener_(
new ExtensionTestMessageListener(kInjectSucceeded,
false /* won't reply */)) {
inject_attempt_listener_->set_extension_id(extension->id());
inject_success_listener_->set_extension_id(extension->id());
} }
ActiveScriptTester::~ActiveScriptTester() { ActiveScriptTester::~ActiveScriptTester() {
} }
testing::AssertionResult ActiveScriptTester::Verify(Browser* browser) { testing::AssertionResult ActiveScriptTester::Verify() {
if (!extension_) if (!extension_)
return testing::AssertionFailure() << "Could not load extension: " << name_; return testing::AssertionFailure() << "Could not load extension: " << name_;
listener_->WaitUntilSatisfied(); // If it's not a content script, the Extension lets us know when it has
content::WebContents* web_contents = // attempted to inject the script.
browser ? browser->tab_strip_model()->GetActiveWebContents() : NULL; // This means there is a potential for a race condition with content scripts;
// however, since they are all injected at document_start, this shouldn't be
// a problem. If these tests start failing, though, that might be it.
if (type_ == EXECUTE_SCRIPT)
inject_attempt_listener_->WaitUntilSatisfied();
if (!web_contents) // Make sure all running tasks are complete.
return testing::AssertionFailure() << "Could not find web contents."; content::RunAllPendingInMessageLoop();
TabHelper* tab_helper = TabHelper::FromWebContents(web_contents); LocationBarController* location_bar_controller = GetLocationBarController();
if (!tab_helper) if (!location_bar_controller) {
return testing::AssertionFailure() << "Could not find tab helper."; return testing::AssertionFailure()
<< "Could not find location bar controller";
}
ActiveScriptController* controller = ActiveScriptController* controller =
tab_helper->location_bar_controller()->active_script_controller(); location_bar_controller->active_script_controller();
if (!controller) if (!controller)
return testing::AssertionFailure() << "Could not find controller."; return testing::AssertionFailure() << "Could not find controller.";
bool has_action = controller->GetActionForExtension(extension_) != NULL; ExtensionAction* action = GetAction();
if (expect_has_action_ != has_action) { bool has_action = action != NULL;
// An extension should have an action displayed iff it requires user consent.
if ((requires_consent_ == REQUIRES_CONSENT && !has_action) ||
(requires_consent_ == DOES_NOT_REQUIRE_CONSENT && has_action)) {
return testing::AssertionFailure()
<< "Improper action status for " << name_ << ": expected "
<< (requires_consent_ == REQUIRES_CONSENT) << ", found " << has_action;
}
// If the extension has permission, we should be able to simply wait for it
// to execute.
if (requires_consent_ == DOES_NOT_REQUIRE_CONSENT) {
inject_success_listener_->WaitUntilSatisfied();
return testing::AssertionSuccess();
}
// Otherwise, we don't have permission, and have to grant it. Ensure the
// script has *not* already executed.
// Currently, it's okay for content scripts to execute, because we don't
// block them.
// TODO(rdevlin.cronin): Fix this.
if (inject_success_listener_->was_satisfied() && type_ != CONTENT_SCRIPT) {
return testing::AssertionFailure() <<
name_ << "'s script ran without permission.";
}
// If we reach this point, we should always have an action.
DCHECK(action);
// Grant permission by clicking on the extension action.
location_bar_controller->OnClicked(action);
// Now, the extension should be able to inject the script.
inject_success_listener_->WaitUntilSatisfied();
// The Action should have disappeared.
has_action = GetAction() != NULL;
if (has_action) {
return testing::AssertionFailure() return testing::AssertionFailure()
<< "Improper action status: expected " << expect_has_action_ << "Extension " << name_ << " has lingering action.";
<< ", found " << has_action;
} }
return testing::AssertionSuccess(); return testing::AssertionSuccess();
} }
LocationBarController* ActiveScriptTester::GetLocationBarController() {
content::WebContents* web_contents =
browser_ ? browser_->tab_strip_model()->GetActiveWebContents() : NULL;
if (!web_contents)
return NULL;
TabHelper* tab_helper = TabHelper::FromWebContents(web_contents);
return tab_helper ? tab_helper->location_bar_controller() : NULL;
}
ActiveScriptController* ActiveScriptTester::GetActiveScriptController() {
LocationBarController* location_bar_controller = GetLocationBarController();
return location_bar_controller ?
location_bar_controller->active_script_controller() : NULL;
}
ExtensionAction* ActiveScriptTester::GetAction() {
ActiveScriptController* controller = GetActiveScriptController();
return controller ? controller->GetActionForExtension(extension_) : NULL;
}
IN_PROC_BROWSER_TEST_F(ActiveScriptControllerBrowserTest, IN_PROC_BROWSER_TEST_F(ActiveScriptControllerBrowserTest,
ActiveScriptsAreDisplayed) { ActiveScriptsAreDisplayed) {
base::FilePath active_script_path = base::FilePath active_script_path =
test_data_dir_.AppendASCII("active_script"); test_data_dir_.AppendASCII("active_script");
const char* kExtensionNames[] = { const char* kExtensionNames[] = {
"content_scripts_all_hosts",
"inject_scripts_all_hosts", "inject_scripts_all_hosts",
"inject_scripts_explicit_hosts",
"content_scripts_all_hosts",
"content_scripts_explicit_hosts" "content_scripts_explicit_hosts"
}; };
// First, we load up three extensions: // First, we load up three extensions:
// - An extension with a content script that runs on all hosts,
// - An extension that injects scripts into all hosts, // - An extension that injects scripts into all hosts,
// - An extension that injects scripts into explicit hosts,
// - An extension with a content script that runs on all hosts,
// - An extension with a content script that runs on explicit hosts. // - An extension with a content script that runs on explicit hosts.
// The extensions that operate on explicit hosts have permission; the ones
// that request all hosts require user consent.
ActiveScriptTester testers[] = { ActiveScriptTester testers[] = {
ActiveScriptTester( ActiveScriptTester(
kExtensionNames[0], kExtensionNames[0],
LoadExtension(active_script_path.AppendASCII(kExtensionNames[0])), GetOrCreateExtension(ALL_HOSTS, EXECUTE_SCRIPT),
true /* expect action */), browser(),
REQUIRES_CONSENT,
EXECUTE_SCRIPT),
ActiveScriptTester( ActiveScriptTester(
kExtensionNames[1], kExtensionNames[1],
LoadExtension(active_script_path.AppendASCII(kExtensionNames[1])), GetOrCreateExtension(EXPLICIT_HOSTS, EXECUTE_SCRIPT),
true /* expect action */), browser(),
DOES_NOT_REQUIRE_CONSENT,
EXECUTE_SCRIPT),
ActiveScriptTester( ActiveScriptTester(
kExtensionNames[2], kExtensionNames[2],
LoadExtension(active_script_path.AppendASCII(kExtensionNames[2])), GetOrCreateExtension(ALL_HOSTS, CONTENT_SCRIPT),
false /* expect action */) browser(),
REQUIRES_CONSENT,
CONTENT_SCRIPT),
ActiveScriptTester(
kExtensionNames[3],
GetOrCreateExtension(EXPLICIT_HOSTS, CONTENT_SCRIPT),
browser(),
DOES_NOT_REQUIRE_CONSENT,
CONTENT_SCRIPT),
}; };
// Navigate to an URL (which matches the explicit host specified in the // Navigate to an URL (which matches the explicit host specified in the
...@@ -135,7 +369,7 @@ IN_PROC_BROWSER_TEST_F(ActiveScriptControllerBrowserTest, ...@@ -135,7 +369,7 @@ IN_PROC_BROWSER_TEST_F(ActiveScriptControllerBrowserTest,
browser(), embedded_test_server()->GetURL("/extensions/test_file.html")); browser(), embedded_test_server()->GetURL("/extensions/test_file.html"));
for (size_t i = 0u; i < arraysize(testers); ++i) for (size_t i = 0u; i < arraysize(testers); ++i)
ASSERT_TRUE(testers[i].Verify(browser())) << kExtensionNames[i]; EXPECT_TRUE(testers[i].Verify()) << kExtensionNames[i];
} }
} // namespace extensions } // namespace extensions
...@@ -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