blob: fb6ec044522e116a39bbc15980ada9062171732f [file] [log] [blame]
// 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.
#include <stddef.h>
#include <map>
#include <utility>
#include "base/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/values.h"
#include "chrome/browser/extensions/active_tab_permission_granter.h"
#include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/permissions_updater.h"
#include "chrome/browser/extensions/scripting_permissions_modifier.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "components/crx_file/id_util.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/navigation_simulator.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/manifest.h"
#include "extensions/common/mojom/injection_type.mojom-shared.h"
#include "extensions/common/mojom/run_location.mojom-shared.h"
#include "extensions/common/user_script.h"
#include "extensions/common/value_builder.h"
namespace extensions {
namespace {
const char kAllHostsPermission[] = "*://*/*";
} // namespace
// Unittests for the ExtensionActionRunner mostly test the internal logic
// of the runner itself (when to allow/deny extension script injection).
// Testing real injection is allowed/denied as expected (i.e., that the
// ExtensionActionRunner correctly interfaces in the system) is done in the
// ExtensionActionRunnerBrowserTests.
class ExtensionActionRunnerUnitTest : public ChromeRenderViewHostTestHarness {
public:
ExtensionActionRunnerUnitTest(const ExtensionActionRunnerUnitTest&) = delete;
ExtensionActionRunnerUnitTest& operator=(
const ExtensionActionRunnerUnitTest&) = delete;
protected:
ExtensionActionRunnerUnitTest();
~ExtensionActionRunnerUnitTest() override;
// Creates an extension with all hosts permission and adds it to the registry.
const Extension* AddExtension();
// Reloads |extension_| by removing it from the registry and recreating it.
const Extension* ReloadExtension();
// Returns true if the |extension| requires user consent before injecting
// a script.
bool RequiresUserConsent(const Extension* extension) const;
// Request an injection for the given |extension|.
void RequestInjection(const Extension* extension);
void RequestInjection(const Extension* extension,
mojom::RunLocation run_location);
// Returns the number of times a given extension has had a script execute.
size_t GetExecutionCountForExtension(const std::string& extension_id) const;
ExtensionActionRunner* runner() const { return extension_action_runner_; }
private:
// Returns a closure to use as a script execution for a given extension.
ExtensionActionRunner::ScriptInjectionCallback
GetExecutionCallbackForExtension(const std::string& extension_id);
// Increment the number of executions for the given |extension_id|.
void IncrementExecutionCount(const std::string& extension_id, bool granted);
void SetUp() override;
// The associated ExtensionActionRunner.
raw_ptr<ExtensionActionRunner> extension_action_runner_ = nullptr;
// The map of observed executions, keyed by extension id.
std::map<std::string, int> extension_executions_;
scoped_refptr<const Extension> extension_;
};
ExtensionActionRunnerUnitTest::ExtensionActionRunnerUnitTest() = default;
ExtensionActionRunnerUnitTest::~ExtensionActionRunnerUnitTest() = default;
const Extension* ExtensionActionRunnerUnitTest::AddExtension() {
const std::string kId = crx_file::id_util::GenerateId("all_hosts_extension");
extension_ =
ExtensionBuilder()
.SetManifest(
DictionaryBuilder()
.Set("name", "all_hosts_extension")
.Set("description", "an extension")
.Set("manifest_version", 2)
.Set("version", "1.0.0")
.Set("permissions",
ListBuilder().Append(kAllHostsPermission).Build())
.Build())
.SetLocation(mojom::ManifestLocation::kInternal)
.SetID(kId)
.Build();
ExtensionRegistry::Get(profile())->AddEnabled(extension_);
PermissionsUpdater(profile()).InitializePermissions(extension_.get());
ScriptingPermissionsModifier(profile(), extension_.get())
.SetWithholdHostPermissions(true);
return extension_.get();
}
const Extension* ExtensionActionRunnerUnitTest::ReloadExtension() {
ExtensionRegistry::Get(profile())->RemoveEnabled(extension_->id());
return AddExtension();
}
bool ExtensionActionRunnerUnitTest::RequiresUserConsent(
const Extension* extension) const {
PermissionsData::PageAccess access_type =
runner()->RequiresUserConsentForScriptInjectionForTesting(
extension, mojom::InjectionType::kProgrammaticScript);
// We should never downright refuse access in these tests.
DCHECK_NE(PermissionsData::PageAccess::kDenied, access_type);
return access_type == PermissionsData::PageAccess::kWithheld;
}
void ExtensionActionRunnerUnitTest::RequestInjection(
const Extension* extension) {
RequestInjection(extension, mojom::RunLocation::kDocumentIdle);
}
void ExtensionActionRunnerUnitTest::RequestInjection(
const Extension* extension,
mojom::RunLocation run_location) {
runner()->RequestScriptInjectionForTesting(
extension, run_location,
GetExecutionCallbackForExtension(extension->id()));
}
size_t ExtensionActionRunnerUnitTest::GetExecutionCountForExtension(
const std::string& extension_id) const {
auto iter = extension_executions_.find(extension_id);
if (iter != extension_executions_.end())
return iter->second;
return 0u;
}
ExtensionActionRunner::ScriptInjectionCallback
ExtensionActionRunnerUnitTest::GetExecutionCallbackForExtension(
const std::string& extension_id) {
// We use base unretained here, but if this ever gets executed outside of
// this test's lifetime, we have a major problem anyway.
return base::BindOnce(&ExtensionActionRunnerUnitTest::IncrementExecutionCount,
base::Unretained(this), extension_id);
}
void ExtensionActionRunnerUnitTest::IncrementExecutionCount(
const std::string& extension_id,
bool granted) {
if (!granted)
return;
++extension_executions_[extension_id];
}
void ExtensionActionRunnerUnitTest::SetUp() {
ChromeRenderViewHostTestHarness::SetUp();
TabHelper::CreateForWebContents(web_contents());
TabHelper* tab_helper = TabHelper::FromWebContents(web_contents());
// These should never be null.
DCHECK(tab_helper);
extension_action_runner_ = tab_helper->extension_action_runner();
DCHECK(extension_action_runner_);
}
// Test that extensions with all_hosts require permission to execute, and, once
// that permission is granted, do execute.
TEST_F(ExtensionActionRunnerUnitTest, RequestPermissionAndExecute) {
const Extension* extension = AddExtension();
ASSERT_TRUE(extension);
NavigateAndCommit(GURL("https://www.google.com"));
// Ensure that there aren't any executions pending.
ASSERT_EQ(0u, GetExecutionCountForExtension(extension->id()));
ASSERT_FALSE(runner()->WantsToRun(extension));
// Since the extension requests all_hosts, we should require user consent.
EXPECT_TRUE(RequiresUserConsent(extension));
// Request an injection. The extension should want to run, but should not have
// executed.
RequestInjection(extension);
EXPECT_TRUE(runner()->WantsToRun(extension));
EXPECT_EQ(0u, GetExecutionCountForExtension(extension->id()));
// Click to accept the extension executing.
runner()->RunForTesting(extension);
// The extension should execute, and the extension shouldn't want to run.
EXPECT_EQ(1u, GetExecutionCountForExtension(extension->id()));
EXPECT_FALSE(runner()->WantsToRun(extension));
// Since we already executed on the given page, we shouldn't need permission
// for a second time.
EXPECT_FALSE(RequiresUserConsent(extension));
// Reloading and same-origin navigations shouldn't clear those permissions,
// and we shouldn't require user constent again.
content::NavigationSimulator::Reload(web_contents());
EXPECT_FALSE(RequiresUserConsent(extension));
NavigateAndCommit(GURL("https://www.google.com/foo"));
EXPECT_FALSE(RequiresUserConsent(extension));
NavigateAndCommit(GURL("https://www.google.com/bar"));
EXPECT_FALSE(RequiresUserConsent(extension));
// Cross-origin navigations should clear permissions.
NavigateAndCommit(GURL("https://otherdomain.google.com"));
EXPECT_TRUE(RequiresUserConsent(extension));
// Grant access.
RequestInjection(extension);
runner()->RunForTesting(extension);
EXPECT_EQ(2u, GetExecutionCountForExtension(extension->id()));
EXPECT_FALSE(runner()->WantsToRun(extension));
// Navigating to another site should also clear the permissions.
NavigateAndCommit(GURL("https://www.foo.com"));
EXPECT_TRUE(RequiresUserConsent(extension));
}
// Test that injections that are not executed by the time the user navigates are
// ignored and never execute.
TEST_F(ExtensionActionRunnerUnitTest, PendingInjectionsRemovedAtNavigation) {
const Extension* extension = AddExtension();
ASSERT_TRUE(extension);
NavigateAndCommit(GURL("https://www.google.com"));
ASSERT_EQ(0u, GetExecutionCountForExtension(extension->id()));
// Request an injection. The extension should want to run, but not execute.
RequestInjection(extension);
EXPECT_TRUE(runner()->WantsToRun(extension));
EXPECT_EQ(0u, GetExecutionCountForExtension(extension->id()));
// Reload. This should remove the pending injection, and we should not
// execute anything.
content::NavigationSimulator::Reload(web_contents());
EXPECT_FALSE(runner()->WantsToRun(extension));
EXPECT_EQ(0u, GetExecutionCountForExtension(extension->id()));
// Request and accept a new injection.
RequestInjection(extension);
runner()->RunForTesting(extension);
// The extension should only have executed once, even though a grand total
// of two executions were requested.
EXPECT_EQ(1u, GetExecutionCountForExtension(extension->id()));
EXPECT_FALSE(runner()->WantsToRun(extension));
}
// Test that queueing multiple pending injections, and then accepting, triggers
// them all.
TEST_F(ExtensionActionRunnerUnitTest, MultiplePendingInjection) {
const Extension* extension = AddExtension();
ASSERT_TRUE(extension);
NavigateAndCommit(GURL("https://www.google.com"));
ASSERT_EQ(0u, GetExecutionCountForExtension(extension->id()));
const size_t kNumInjections = 3u;
// Queue multiple pending injections.
for (size_t i = 0u; i < kNumInjections; ++i)
RequestInjection(extension);
EXPECT_EQ(0u, GetExecutionCountForExtension(extension->id()));
runner()->RunForTesting(extension);
// All pending injections should have executed.
EXPECT_EQ(kNumInjections, GetExecutionCountForExtension(extension->id()));
EXPECT_FALSE(runner()->WantsToRun(extension));
}
TEST_F(ExtensionActionRunnerUnitTest, ActiveScriptsUseActiveTabPermissions) {
const Extension* extension = AddExtension();
NavigateAndCommit(GURL("https://www.google.com"));
ActiveTabPermissionGranter* active_tab_permission_granter =
TabHelper::FromWebContents(web_contents())
->active_tab_permission_granter();
ASSERT_TRUE(active_tab_permission_granter);
// Grant the extension active tab permissions. This normally happens, e.g.,
// if the user clicks on a browser action.
active_tab_permission_granter->GrantIfRequested(extension);
// Since we have active tab permissions, we shouldn't need user consent
// anymore.
EXPECT_FALSE(RequiresUserConsent(extension));
// Reloading and other same-origin navigations maintain the permission to
// execute.
content::NavigationSimulator::Reload(web_contents());
EXPECT_FALSE(RequiresUserConsent(extension));
NavigateAndCommit(GURL("https://www.google.com/foo"));
EXPECT_FALSE(RequiresUserConsent(extension));
NavigateAndCommit(GURL("https://www.google.com/bar"));
EXPECT_FALSE(RequiresUserConsent(extension));
// Navigating to a different origin will require user consent again.
NavigateAndCommit(GURL("https://yahoo.com"));
EXPECT_TRUE(RequiresUserConsent(extension));
// Back to the original origin should also re-require constent.
NavigateAndCommit(GURL("https://www.google.com"));
EXPECT_TRUE(RequiresUserConsent(extension));
RequestInjection(extension);
EXPECT_TRUE(runner()->WantsToRun(extension));
EXPECT_EQ(0u, GetExecutionCountForExtension(extension->id()));
// Grant active tab.
active_tab_permission_granter->GrantIfRequested(extension);
// The pending injections should have run since active tab permission was
// granted.
EXPECT_EQ(1u, GetExecutionCountForExtension(extension->id()));
EXPECT_FALSE(runner()->WantsToRun(extension));
}
TEST_F(ExtensionActionRunnerUnitTest, ActiveScriptsCanHaveAllUrlsPref) {
const Extension* extension = AddExtension();
ASSERT_TRUE(extension);
NavigateAndCommit(GURL("https://www.google.com"));
EXPECT_TRUE(RequiresUserConsent(extension));
// Enable the extension on all urls.
ScriptingPermissionsModifier permissions_modifier(profile(), extension);
permissions_modifier.SetWithholdHostPermissions(false);
EXPECT_FALSE(RequiresUserConsent(extension));
// This should carry across navigations, and websites.
NavigateAndCommit(GURL("http://www.foo.com"));
EXPECT_FALSE(RequiresUserConsent(extension));
// Turning off the preference should have instant effect.
permissions_modifier.SetWithholdHostPermissions(true);
EXPECT_TRUE(RequiresUserConsent(extension));
// And should also persist across navigations and websites.
NavigateAndCommit(GURL("http://www.bar.com"));
EXPECT_TRUE(RequiresUserConsent(extension));
}
TEST_F(ExtensionActionRunnerUnitTest, TestAlwaysRun) {
const Extension* extension = AddExtension();
ASSERT_TRUE(extension);
NavigateAndCommit(GURL("https://www.google.com/?gws_rd=ssl"));
// Ensure that there aren't any executions pending.
ASSERT_EQ(0u, GetExecutionCountForExtension(extension->id()));
ASSERT_FALSE(runner()->WantsToRun(extension));
// Since the extension requests all_hosts, we should require user consent.
EXPECT_TRUE(RequiresUserConsent(extension));
// Request an injection. The extension should want to run, but not execute.
RequestInjection(extension);
EXPECT_TRUE(runner()->WantsToRun(extension));
EXPECT_EQ(0u, GetExecutionCountForExtension(extension->id()));
// Allow the extension to always run on this origin.
ScriptingPermissionsModifier modifier(profile(), extension);
modifier.GrantHostPermission(web_contents()->GetLastCommittedURL());
runner()->RunForTesting(extension);
// The extension should execute, and the extension shouldn't want to run.
EXPECT_EQ(1u, GetExecutionCountForExtension(extension->id()));
EXPECT_FALSE(runner()->WantsToRun(extension));
// Since we already executed on the given page, we shouldn't need permission
// for a second time.
EXPECT_FALSE(RequiresUserConsent(extension));
// Navigating to another site that hasn't been granted a persisted permission
// should necessitate user consent.
NavigateAndCommit(GURL("https://www.foo.com/bar"));
EXPECT_TRUE(RequiresUserConsent(extension));
// We shouldn't need user permission upon returning to the original origin.
NavigateAndCommit(GURL("https://www.google.com/foo/bar"));
EXPECT_FALSE(RequiresUserConsent(extension));
// Reloading the extension should not clear any granted host permissions.
extension = ReloadExtension();
content::NavigationSimulator::Reload(web_contents());
EXPECT_FALSE(RequiresUserConsent(extension));
// Different host...
NavigateAndCommit(GURL("https://www.foo.com/bar"));
EXPECT_TRUE(RequiresUserConsent(extension));
// Different scheme...
NavigateAndCommit(GURL("http://www.google.com/foo/bar"));
EXPECT_TRUE(RequiresUserConsent(extension));
// Different subdomain...
NavigateAndCommit(GURL("https://en.google.com/foo/bar"));
EXPECT_TRUE(RequiresUserConsent(extension));
// Only the "always run" origin should be allowed to run without user consent.
NavigateAndCommit(GURL("https://www.google.com/foo/bar"));
EXPECT_FALSE(RequiresUserConsent(extension));
}
TEST_F(ExtensionActionRunnerUnitTest, TestDifferentScriptRunLocations) {
const Extension* extension = AddExtension();
ASSERT_TRUE(extension);
NavigateAndCommit(GURL("https://www.foo.com"));
EXPECT_EQ(BLOCKED_ACTION_NONE, runner()->GetBlockedActions(extension->id()));
RequestInjection(extension, mojom::RunLocation::kDocumentEnd);
EXPECT_EQ(BLOCKED_ACTION_SCRIPT_OTHER,
runner()->GetBlockedActions(extension->id()));
RequestInjection(extension, mojom::RunLocation::kDocumentIdle);
EXPECT_EQ(BLOCKED_ACTION_SCRIPT_OTHER,
runner()->GetBlockedActions(extension->id()));
RequestInjection(extension, mojom::RunLocation::kDocumentStart);
EXPECT_EQ(BLOCKED_ACTION_SCRIPT_AT_START | BLOCKED_ACTION_SCRIPT_OTHER,
runner()->GetBlockedActions(extension->id()));
runner()->RunForTesting(extension);
EXPECT_EQ(BLOCKED_ACTION_NONE, runner()->GetBlockedActions(extension->id()));
}
TEST_F(ExtensionActionRunnerUnitTest, TestWebRequestBlocked) {
const Extension* extension = AddExtension();
ASSERT_TRUE(extension);
NavigateAndCommit(GURL("https://www.foo.com"));
EXPECT_EQ(BLOCKED_ACTION_NONE, runner()->GetBlockedActions(extension->id()));
EXPECT_FALSE(runner()->WantsToRun(extension));
runner()->OnWebRequestBlocked(extension);
EXPECT_EQ(BLOCKED_ACTION_WEB_REQUEST,
runner()->GetBlockedActions(extension->id()));
EXPECT_TRUE(runner()->WantsToRun(extension));
RequestInjection(extension);
EXPECT_EQ(BLOCKED_ACTION_WEB_REQUEST | BLOCKED_ACTION_SCRIPT_OTHER,
runner()->GetBlockedActions(extension->id()));
EXPECT_TRUE(runner()->WantsToRun(extension));
NavigateAndCommit(GURL("https://www.bar.com"));
EXPECT_EQ(BLOCKED_ACTION_NONE, runner()->GetBlockedActions(extension->id()));
EXPECT_FALSE(runner()->WantsToRun(extension));
}
} // namespace extensions