| // 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 "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/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) { |
| // 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 |