blob: 94638063b0a2c2e5ffaff0c3568a151e501b97b1 [file] [log] [blame]
// Copyright 2015 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 "chrome/browser/extensions/scripting_permissions_modifier.h"
#include "base/bind_helpers.h"
#include "base/feature_list.h"
#include "chrome/browser/extensions/permissions_updater.h"
#include "chrome/common/webui_url_constants.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/manifest_handlers/permissions_parser.h"
#include "extensions/common/permissions/permission_set.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/user_script.h"
namespace extensions {
namespace {
// The entry into the ExtensionPrefs indicating that an extension should be
// granted all the requested host permissions without requiring explicit runtime
// permission from the user. The preference name is different for legacy
// reasons.
const char kGrantExtensionAllHostPermissionsPrefName[] =
"extension_can_script_all_urls";
// Returns true if Chrome can potentially withhold permissions from the
// extension.
bool CanWithholdFromExtension(const Extension& extension) {
// Some extensions must retain privilege to all requested host permissions.
// Specifically, extensions that don't show up in chrome:extensions (where
// withheld permissions couldn't be granted), extensions that are part of
// chrome or corporate policy, and extensions that are whitelisted to script
// everywhere must always have permission to run on a page.
return extension.ShouldDisplayInExtensionSettings() &&
!Manifest::IsPolicyLocation(extension.location()) &&
!Manifest::IsComponentLocation(extension.location()) &&
!PermissionsData::CanExecuteScriptEverywhere(extension.id(),
extension.location());
}
// Iterates over |requested_permissions| and adds any permissions that should
// be granted to |granted_permissions_out|. These include any non-host
// permissions or host permissions that are present in
// |runtime_granted_permissions|. |granted_permissions_out| may contain new
// patterns not found in either |requested_permissions| or
// |runtime_granted_permissions| in the case of overlapping host permissions
// (such as *://*.google.com/* and https://*/*, which would intersect with
// https://*.google.com/*).
void PartitionHostPermissions(
const PermissionSet& requested_permissions,
const PermissionSet& runtime_granted_permissions,
std::unique_ptr<const PermissionSet>* granted_permissions_out) {
auto segregate_url_permissions =
[](const URLPatternSet& requested_patterns,
const URLPatternSet& runtime_granted_patterns,
URLPatternSet* granted) {
*granted = URLPatternSet::CreateIntersection(
requested_patterns, runtime_granted_patterns,
URLPatternSet::IntersectionBehavior::kDetailed);
for (const URLPattern& pattern : requested_patterns) {
// The chrome://favicon permission is special. It is requested by
// extensions to access stored favicons, but is not a traditional
// host permission. Since it cannot be reasonably runtime-granted
// while the user is on the site (i.e., the user never visits
// chrome://favicon/), we auto-grant it and treat it like an API
// permission.
bool is_chrome_favicon =
pattern.scheme() == content::kChromeUIScheme &&
pattern.host() == chrome::kChromeUIFaviconHost;
if (is_chrome_favicon)
granted->AddPattern(pattern);
}
};
URLPatternSet granted_explicit_hosts;
URLPatternSet granted_scriptable_hosts;
segregate_url_permissions(requested_permissions.explicit_hosts(),
runtime_granted_permissions.explicit_hosts(),
&granted_explicit_hosts);
segregate_url_permissions(requested_permissions.scriptable_hosts(),
runtime_granted_permissions.scriptable_hosts(),
&granted_scriptable_hosts);
*granted_permissions_out = std::make_unique<PermissionSet>(
requested_permissions.apis().Clone(),
requested_permissions.manifest_permissions().Clone(),
std::move(granted_explicit_hosts), std::move(granted_scriptable_hosts));
}
// Returns true if the extension should even be considered for being affected
// by the runtime host permissions experiment.
bool ShouldConsiderExtension(const Extension& extension) {
// No extensions are affected if the experiment is disabled.
if (!base::FeatureList::IsEnabled(
extensions_features::kRuntimeHostPermissions))
return false;
// Certain extensions are always exempt from having permissions withheld.
if (!CanWithholdFromExtension(extension))
return false;
return true;
}
base::Optional<bool> GetWithholdPermissionsPrefValue(
const ExtensionPrefs& prefs,
const ExtensionId& id) {
bool permissions_allowed = false;
if (!prefs.ReadPrefAsBoolean(id, kGrantExtensionAllHostPermissionsPrefName,
&permissions_allowed)) {
return base::nullopt;
}
// NOTE: For legacy reasons, the preference stores whether the extension was
// allowed access to all its host permissions, rather than if Chrome should
// withhold permissions. Invert the boolean for backwards compatibility.
return !permissions_allowed;
}
void SetWithholdPermissionsPrefValue(ExtensionPrefs* prefs,
const ExtensionId& id,
bool should_withhold) {
// NOTE: For legacy reasons, the preference stores whether the extension was
// allowed access to all its host permissions, rather than if Chrome should
// withhold permissions. Invert the boolean for backwards compatibility.
bool permissions_allowed = !should_withhold;
prefs->UpdateExtensionPref(
id, kGrantExtensionAllHostPermissionsPrefName,
std::make_unique<base::Value>(permissions_allowed));
}
// Retrieves the effective list of runtime-granted permissions for a given
// |extension| from the |prefs|. ExtensionPrefs doesn't store the valid schemes
// for URLPatterns, which results in the chrome:-scheme being included for
// <all_urls> when retrieving it directly from the prefs; this then causes
// CHECKs to fail when validating that permissions being revoked are present
// (see https://crbug.com/930062).
// Returns null if there are no stored runtime-granted permissions.
// TODO(https://crbug.com/931881): ExtensionPrefs should return properly-bounded
// permissions.
std::unique_ptr<const PermissionSet> GetRuntimePermissionsFromPrefs(
const Extension& extension,
const ExtensionPrefs& prefs) {
std::unique_ptr<const PermissionSet> permissions =
prefs.GetRuntimeGrantedPermissions(extension.id());
// If there are no stored permissions, there's nothing to adjust.
if (!permissions)
return nullptr;
// If the extension is allowed to run on chrome:// URLs, then we don't have
// to adjust anything.
if (PermissionsData::AllUrlsIncludesChromeUrls(extension.id()))
return permissions;
// We need to adjust a pattern if it matches all URLs and includes the
// chrome:-scheme. These patterns would otherwise match hosts like
// chrome://settings, which should not be allowed.
// NOTE: We don't need to adjust for the file scheme, because
// ExtensionPrefs properly does that based on the extension's file access.
auto needs_chrome_scheme_adjustment = [](const URLPattern& pattern) {
return pattern.match_all_urls() &&
((pattern.valid_schemes() & URLPattern::SCHEME_CHROMEUI) != 0);
};
// NOTE: We don't need to check scriptable_hosts, because the default
// scriptable_hosts scheme mask omits the chrome:-scheme in normal
// circumstances (whereas the default explicit scheme does not, in order to
// allow for patterns like chrome://favicon).
bool needs_adjustment = std::any_of(permissions->explicit_hosts().begin(),
permissions->explicit_hosts().end(),
needs_chrome_scheme_adjustment);
// If no patterns need adjustment, return the original set.
if (!needs_adjustment)
return permissions;
// Otherwise, iterate over the explicit hosts, and modify any that need to be
// tweaked, adding back in permitted chrome:-scheme hosts. This logic mirrors
// that in PermissionsParser, and is also similar to logic in
// permissions_api_helpers::UnpackOriginPermissions(), and has some overlap
// to URLPatternSet::Populate().
// TODO(devlin): ^^ Ouch. Refactor so that this isn't duplicated.
URLPatternSet new_explicit_hosts;
for (const auto& pattern : permissions->explicit_hosts()) {
if (!needs_chrome_scheme_adjustment(pattern)) {
new_explicit_hosts.AddPattern(pattern);
continue;
}
URLPattern new_pattern(pattern);
int new_valid_schemes =
pattern.valid_schemes() & ~URLPattern::SCHEME_CHROMEUI;
new_pattern.SetValidSchemes(new_valid_schemes);
new_explicit_hosts.AddPattern(std::move(new_pattern));
}
return std::make_unique<PermissionSet>(
permissions->apis().Clone(), permissions->manifest_permissions().Clone(),
std::move(new_explicit_hosts), permissions->scriptable_hosts().Clone());
}
} // namespace
ScriptingPermissionsModifier::ScriptingPermissionsModifier(
content::BrowserContext* browser_context,
const scoped_refptr<const Extension>& extension)
: browser_context_(browser_context),
extension_(extension),
extension_prefs_(ExtensionPrefs::Get(browser_context_)) {
DCHECK(extension_);
}
ScriptingPermissionsModifier::~ScriptingPermissionsModifier() {}
void ScriptingPermissionsModifier::SetWithholdHostPermissions(
bool should_withhold) {
DCHECK(CanAffectExtension());
if (HasWithheldHostPermissions() == should_withhold)
return;
// Set the pref first, so that listeners for permission changes get the proper
// value if they query HasWithheldHostPermissions().
SetWithholdPermissionsPrefValue(extension_prefs_, extension_->id(),
should_withhold);
if (should_withhold)
WithholdHostPermissions();
else
GrantWithheldHostPermissions();
}
bool ScriptingPermissionsModifier::HasWithheldHostPermissions() const {
DCHECK(CanAffectExtension());
base::Optional<bool> pref_value =
GetWithholdPermissionsPrefValue(*extension_prefs_, extension_->id());
if (!pref_value.has_value()) {
// If there is no value present, default to false.
return false;
}
return *pref_value;
}
bool ScriptingPermissionsModifier::CanAffectExtension() const {
if (!ShouldConsiderExtension(*extension_))
return false;
// The extension can be affected if it currently has host permissions, or if
// it did and they are actively withheld.
return !extension_->permissions_data()
->active_permissions()
.effective_hosts()
.is_empty() ||
!extension_->permissions_data()
->withheld_permissions()
.effective_hosts()
.is_empty();
}
ScriptingPermissionsModifier::SiteAccess
ScriptingPermissionsModifier::GetSiteAccess(const GURL& url) const {
SiteAccess access;
ExtensionPrefs* prefs = ExtensionPrefs::Get(browser_context_);
// Awkward holder object because permission sets are immutable, and when
// return from prefs, ownership is passed.
std::unique_ptr<const PermissionSet> permission_holder;
const PermissionSet* granted_permissions = nullptr;
if (!HasWithheldHostPermissions()) {
// If the extension doesn't have any withheld permissions, we look at the
// current active permissions.
// TODO(devlin): This is clunky. It would be nice to have runtime-granted
// permissions be correctly populated in all cases, rather than looking at
// two different sets.
// TODO(devlin): This won't account for granted permissions that aren't
// currently active, even though the extension may re-request them (and be
// silently granted them) at any time.
granted_permissions = &extension_->permissions_data()->active_permissions();
} else {
permission_holder = GetRuntimePermissionsFromPrefs(*extension_, *prefs);
granted_permissions = permission_holder.get();
}
DCHECK(granted_permissions);
const bool is_restricted_site =
extension_->permissions_data()->IsRestrictedUrl(url, /*error=*/nullptr);
// For indicating whether an extension has access to a site, we look at the
// granted permissions, which could include patterns that weren't explicitly
// requested. However, we should still indicate they are granted, so that the
// user can revoke them (and because if the extension does request them and
// they are already granted, they are silently added).
// The extension should never have access to restricted sites (even if a
// pattern matches, as it may for e.g. the webstore).
if (!is_restricted_site &&
granted_permissions->effective_hosts().MatchesSecurityOrigin(url)) {
access.has_site_access = true;
}
const PermissionSet& withheld_permissions =
extension_->permissions_data()->withheld_permissions();
// Be sure to check |access.has_site_access| in addition to withheld
// permissions, so that we don't indicate we've withheld permission if an
// extension is granted https://a.com/*, but has *://*/* withheld.
// We similarly don't show access as withheld for restricted sites, since
// withheld permissions should only include those that are conceivably
// grantable.
if (!is_restricted_site && !access.has_site_access &&
withheld_permissions.effective_hosts().MatchesSecurityOrigin(url)) {
access.withheld_site_access = true;
}
constexpr bool include_api_permissions = false;
if (granted_permissions->ShouldWarnAllHosts(include_api_permissions))
access.has_all_sites_access = true;
if (withheld_permissions.ShouldWarnAllHosts(include_api_permissions) &&
!access.has_all_sites_access) {
access.withheld_all_sites_access = true;
}
return access;
}
void ScriptingPermissionsModifier::GrantHostPermission(const GURL& url) {
DCHECK(CanAffectExtension());
// Check that we don't grant host permission to a restricted URL.
DCHECK(
!extension_->permissions_data()->IsRestrictedUrl(url, /*error=*/nullptr))
<< "Cannot grant access to a restricted URL.";
URLPatternSet explicit_hosts;
explicit_hosts.AddOrigin(Extension::kValidHostPermissionSchemes, url);
URLPatternSet scriptable_hosts;
scriptable_hosts.AddOrigin(UserScript::ValidUserScriptSchemes(), url);
PermissionsUpdater(browser_context_)
.GrantRuntimePermissions(
*extension_,
PermissionSet(APIPermissionSet(), ManifestPermissionSet(),
std::move(explicit_hosts), std::move(scriptable_hosts)),
base::DoNothing::Once());
}
bool ScriptingPermissionsModifier::HasGrantedHostPermission(
const GURL& url) const {
DCHECK(CanAffectExtension());
return GetRuntimePermissionsFromPrefs(*extension_, *extension_prefs_)
->effective_hosts()
.MatchesSecurityOrigin(url);
}
void ScriptingPermissionsModifier::RemoveGrantedHostPermission(
const GURL& url) {
DCHECK(CanAffectExtension());
DCHECK(HasGrantedHostPermission(url));
ExtensionPrefs* prefs = ExtensionPrefs::Get(browser_context_);
std::unique_ptr<const PermissionSet> runtime_permissions =
GetRuntimePermissionsFromPrefs(*extension_, *prefs);
URLPatternSet explicit_hosts;
for (const auto& pattern : runtime_permissions->explicit_hosts()) {
if (pattern.MatchesSecurityOrigin(url))
explicit_hosts.AddPattern(pattern);
}
URLPatternSet scriptable_hosts;
for (const auto& pattern : runtime_permissions->scriptable_hosts()) {
if (pattern.MatchesSecurityOrigin(url))
scriptable_hosts.AddPattern(pattern);
}
PermissionsUpdater(browser_context_)
.RevokeRuntimePermissions(
*extension_,
PermissionSet(APIPermissionSet(), ManifestPermissionSet(),
std::move(explicit_hosts), std::move(scriptable_hosts)),
base::DoNothing::Once());
}
void ScriptingPermissionsModifier::RemoveAllGrantedHostPermissions() {
DCHECK(CanAffectExtension());
WithholdHostPermissions();
}
// static
void ScriptingPermissionsModifier::WithholdPermissionsIfNecessary(
const Extension& extension,
const ExtensionPrefs& extension_prefs,
const PermissionSet& permissions,
std::unique_ptr<const PermissionSet>* granted_permissions_out) {
bool should_withhold = false;
if (ShouldConsiderExtension(extension)) {
base::Optional<bool> pref_value =
GetWithholdPermissionsPrefValue(extension_prefs, extension.id());
should_withhold = pref_value.has_value() && pref_value.value() == true;
}
should_withhold &= !permissions.effective_hosts().is_empty();
if (!should_withhold) {
*granted_permissions_out = permissions.Clone();
return;
}
// Only grant host permissions that the user has explicitly granted at
// runtime through the runtime host permissions feature or the optional
// permissions API.
std::unique_ptr<const PermissionSet> runtime_granted_permissions =
GetRuntimePermissionsFromPrefs(extension, extension_prefs);
PartitionHostPermissions(permissions, *runtime_granted_permissions,
granted_permissions_out);
}
std::unique_ptr<const PermissionSet>
ScriptingPermissionsModifier::GetRevokablePermissions() const {
// No extra revokable permissions if the extension couldn't ever be affected.
if (!ShouldConsiderExtension(*extension_))
return nullptr;
// If we aren't withholding host permissions, then there may be some
// permissions active on the extension that should be revokable. Otherwise,
// all granted permissions should be stored in the preferences (and these
// can be a superset of permissions on the extension, as in the case of e.g.
// granting origins when only a subset is requested by the extension).
// TODO(devlin): This is confusing and subtle. We should instead perhaps just
// add all requested hosts as runtime-granted hosts if we aren't withholding
// host permissions.
const PermissionSet* current_granted_permissions = nullptr;
std::unique_ptr<const PermissionSet> runtime_granted_permissions =
GetRuntimePermissionsFromPrefs(*extension_, *extension_prefs_);
std::unique_ptr<const PermissionSet> union_set;
if (runtime_granted_permissions) {
union_set = PermissionSet::CreateUnion(
*runtime_granted_permissions,
extension_->permissions_data()->active_permissions());
current_granted_permissions = union_set.get();
} else {
current_granted_permissions =
&extension_->permissions_data()->active_permissions();
}
// Revokable permissions are those that would be withheld if there were no
// runtime-granted permissions.
PermissionSet empty_runtime_granted_permissions;
std::unique_ptr<const PermissionSet> granted_permissions;
PartitionHostPermissions(*current_granted_permissions,
empty_runtime_granted_permissions,
&granted_permissions);
return PermissionSet::CreateDifference(*current_granted_permissions,
*granted_permissions);
}
void ScriptingPermissionsModifier::GrantWithheldHostPermissions() {
const PermissionSet& withheld =
extension_->permissions_data()->withheld_permissions();
PermissionSet permissions(APIPermissionSet(), ManifestPermissionSet(),
withheld.explicit_hosts().Clone(),
withheld.scriptable_hosts().Clone());
PermissionsUpdater(browser_context_)
.GrantRuntimePermissions(*extension_, permissions,
base::DoNothing::Once());
}
void ScriptingPermissionsModifier::WithholdHostPermissions() {
PermissionsUpdater(browser_context_)
.RevokeRuntimePermissions(*extension_, *GetRevokablePermissions(),
base::DoNothing::Once());
}
} // namespace extensions