| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/extension_util.h" |
| |
| #include "base/barrier_closure.h" |
| #include "base/command_line.h" |
| #include "base/no_destructor.h" |
| #include "build/chromeos_buildflags.h" |
| #include "components/crx_file/id_util.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/child_process_security_policy.h" |
| #include "content/public/browser/site_instance.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/storage_partition_config.h" |
| #include "extensions/browser/extension_host.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/browser/extensions_browser_client.h" |
| #include "extensions/browser/process_manager.h" |
| #include "extensions/browser/process_map.h" |
| #include "extensions/browser/script_injection_tracker.h" |
| #include "extensions/browser/ui_util.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_id.h" |
| #include "extensions/common/feature_switch.h" |
| #include "extensions/common/features/feature.h" |
| #include "extensions/common/manifest.h" |
| #include "extensions/common/manifest_handlers/incognito_info.h" |
| #include "extensions/common/manifest_handlers/shared_module_info.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/switches.h" |
| #include "extensions/grit/extensions_browser_resources.h" |
| #include "mojo/public/cpp/bindings/clone_traits.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "url/gurl.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chromeos/constants/chromeos_features.h" |
| #include "chromeos/constants/pref_names.h" |
| #include "components/prefs/pref_service.h" |
| #endif |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "base/system/sys_info.h" |
| #endif |
| |
| namespace extensions { |
| namespace util { |
| |
| namespace { |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| bool IsSigninProfileTestExtensionOnTestImage(const Extension* extension) { |
| if (extension->id() != extension_misc::kSigninProfileTestExtensionId) { |
| return false; |
| } |
| base::SysInfo::CrashIfChromeOSNonTestImage(); |
| return true; |
| } |
| #endif |
| |
| } // namespace |
| |
| bool CanBeIncognitoEnabled(const Extension* extension) { |
| return IncognitoInfo::IsIncognitoAllowed(extension) && |
| (!extension->is_platform_app() || |
| extension->location() == mojom::ManifestLocation::kComponent); |
| } |
| |
| bool IsIncognitoEnabled(const ExtensionId& extension_id, |
| content::BrowserContext* context) { |
| const Extension* extension = |
| ExtensionRegistry::Get(context)->enabled_extensions().GetByID( |
| extension_id); |
| if (extension) { |
| if (!CanBeIncognitoEnabled(extension)) { |
| return false; |
| } |
| // If this is an existing component extension we always allow it to |
| // work in incognito mode. |
| if (Manifest::IsComponentLocation(extension->location())) { |
| return true; |
| } |
| if (extension->is_login_screen_extension()) { |
| return true; |
| } |
| #if BUILDFLAG(IS_CHROMEOS) |
| if (IsSigninProfileTestExtensionOnTestImage(extension)) { |
| return true; |
| } |
| #endif |
| } |
| #if BUILDFLAG(IS_CHROMEOS) |
| // An OTR Profile is used for captive portal signin to hide PII from |
| // captive portals (which require HTTP redirects to function). |
| // However, for captive portal signin we do not want want to disable |
| // extensions by default. (Proxies are explicitly disabled elsewhere). |
| // See b/261727502 for details. |
| PrefService* prefs = |
| ExtensionsBrowserClient::Get()->GetPrefServiceForContext(context); |
| if (prefs) { |
| const PrefService::Preference* captive_portal_pref = |
| prefs->FindPreference(chromeos::prefs::kCaptivePortalSignin); |
| if (captive_portal_pref && captive_portal_pref->GetValue()->GetBool()) { |
| return true; |
| } |
| } |
| #endif |
| return ExtensionPrefs::Get(context)->IsIncognitoEnabled(extension_id); |
| } |
| |
| bool CanCrossIncognito(const Extension* extension, |
| content::BrowserContext* context) { |
| // We allow the extension to see events and data from another profile iff it |
| // uses "spanning" behavior and it has incognito access. "split" mode |
| // extensions only see events for a matching profile. |
| CHECK(extension); |
| return IsIncognitoEnabled(extension->id(), context) && |
| !IncognitoInfo::IsSplitMode(extension); |
| } |
| |
| bool IsExtensionIdle(const std::string& extension_id, |
| content::BrowserContext* context) { |
| std::vector<std::string> ids_to_check; |
| ids_to_check.push_back(extension_id); |
| |
| const Extension* extension = |
| ExtensionRegistry::Get(context)->enabled_extensions().GetByID( |
| extension_id); |
| if (extension && extension->is_shared_module()) { |
| // We have to check all the extensions that use this shared module for idle |
| // to tell whether it is really 'idle'. |
| std::unique_ptr<ExtensionSet> dependents = |
| ExtensionSystem::Get(context)->GetDependentExtensions(extension); |
| for (const auto& dependent : *dependents) { |
| ids_to_check.push_back(dependent->id()); |
| } |
| } |
| |
| ProcessManager* process_manager = ProcessManager::Get(context); |
| ProcessMap* process_map = ProcessMap::Get(context); |
| for (const auto& id : ids_to_check) { |
| ExtensionHost* host = process_manager->GetBackgroundHostForExtension(id); |
| if (host) { |
| return false; |
| } |
| |
| if (!process_manager->GetRenderFrameHostsForExtension(id).empty()) { |
| return false; |
| } |
| |
| // TODO(devlin): We can probably remove the checks above (for background |
| // hosts and frame hosts). If an extension has any active frames, it should |
| // have a dedicated process. |
| if (process_map->ExtensionHasProcess(id)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool IsPromptingEnabled() { |
| return FeatureSwitch::prompt_for_external_extensions()->IsEnabled(); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| void InitExtensionSystemForIncognitoSplit( |
| content::BrowserContext* incognito_context) { |
| ExtensionSystem* extension_system = ExtensionSystem::Get(incognito_context); |
| if (!extension_system->is_ready()) { |
| extension_system->InitForRegularProfile(/*extensions_enabled=*/true); |
| } |
| } |
| #endif |
| |
| bool AllowFileAccess(const ExtensionId& extension_id, |
| content::BrowserContext* context) { |
| return base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kDisableExtensionsFileAccessCheck) || |
| ExtensionPrefs::Get(context)->AllowFileAccess(extension_id); |
| } |
| |
| const std::string& GetPartitionDomainForExtension(const Extension* extension) { |
| // Extensions use their own ID for a partition domain. |
| return extension->id(); |
| } |
| |
| content::StoragePartitionConfig GetStoragePartitionConfigForExtensionId( |
| const ExtensionId& extension_id, |
| content::BrowserContext* browser_context) { |
| if (ExtensionsBrowserClient::Get()->HasIsolatedStorage(extension_id, |
| browser_context)) { |
| // For extensions with isolated storage, the |extension_id| is |
| // the |partition_domain|. The |in_memory| and |partition_name| are only |
| // used in guest schemes so they are cleared here. |
| return content::StoragePartitionConfig::Create( |
| browser_context, extension_id, std::string() /* partition_name */, |
| false /*in_memory */); |
| } |
| |
| return content::StoragePartitionConfig::CreateDefault(browser_context); |
| } |
| |
| content::StoragePartition* GetStoragePartitionForExtensionId( |
| const ExtensionId& extension_id, |
| content::BrowserContext* browser_context, |
| bool can_create) { |
| auto storage_partition_config = |
| GetStoragePartitionConfigForExtensionId(extension_id, browser_context); |
| content::StoragePartition* storage_partition = |
| browser_context->GetStoragePartition(storage_partition_config, |
| can_create); |
| return storage_partition; |
| } |
| |
| content::ServiceWorkerContext* GetServiceWorkerContextForExtensionId( |
| const ExtensionId& extension_id, |
| content::BrowserContext* browser_context) { |
| return GetStoragePartitionForExtensionId(extension_id, browser_context) |
| ->GetServiceWorkerContext(); |
| } |
| |
| // This function is security sensitive. Bugs could cause problems that break |
| // restrictions on local file access or NaCl's validation caching. If you modify |
| // this function, please get a security review from a NaCl person. |
| bool MapUrlToLocalFilePath(const ExtensionSet* extensions, |
| const GURL& file_url, |
| bool use_blocking_api, |
| base::FilePath* file_path) { |
| // Check that the URL is recognized by the extension system. |
| const Extension* extension = extensions->GetExtensionOrAppByURL(file_url); |
| if (!extension) { |
| return false; |
| } |
| |
| // This is a short-cut which avoids calling a blocking file operation |
| // (GetFilePath()), so that this can be called on the non blocking threads. It |
| // only handles a subset of the urls. |
| if (!use_blocking_api) { |
| if (file_url.SchemeIs(kExtensionScheme)) { |
| std::string path = file_url.path(); |
| base::TrimString(path, "/", &path); // Remove first slash |
| *file_path = extension->path().AppendASCII(path); |
| return true; |
| } |
| return false; |
| } |
| |
| std::string path = file_url.path(); |
| ExtensionResource resource; |
| |
| if (SharedModuleInfo::IsImportedPath(path)) { |
| // Check if this is a valid path that is imported for this extension. |
| ExtensionId new_extension_id; |
| std::string new_relative_path; |
| SharedModuleInfo::ParseImportedPath(path, &new_extension_id, |
| &new_relative_path); |
| const Extension* new_extension = extensions->GetByID(new_extension_id); |
| if (!new_extension) { |
| return false; |
| } |
| |
| if (!SharedModuleInfo::ImportsExtensionById(extension, new_extension_id)) { |
| return false; |
| } |
| |
| resource = new_extension->GetResource(new_relative_path); |
| } else { |
| // Check that the URL references a resource in the extension. |
| resource = extension->GetResource(path); |
| } |
| |
| if (resource.empty()) { |
| return false; |
| } |
| |
| // GetFilePath is a blocking function call. |
| const base::FilePath resource_file_path = resource.GetFilePath(); |
| if (resource_file_path.empty()) { |
| return false; |
| } |
| |
| *file_path = resource_file_path; |
| return true; |
| } |
| |
| bool CanWithholdPermissionsFromExtension(const Extension& extension) { |
| return CanWithholdPermissionsFromExtension( |
| extension.id(), extension.GetType(), extension.location()); |
| } |
| |
| bool CanWithholdPermissionsFromExtension(const ExtensionId& extension_id, |
| Manifest::Type type, |
| mojom::ManifestLocation location) { |
| // 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 allowlisted to script |
| // everywhere must always have permission to run on a page. |
| return ui_util::ShouldDisplayInExtensionSettings(type, location) && |
| !Manifest::IsPolicyLocation(location) && |
| !Manifest::IsComponentLocation(location) && |
| !PermissionsData::CanExecuteScriptEverywhere(extension_id, location); |
| } |
| |
| int GetBrowserContextId(content::BrowserContext* context) { |
| using ContextIdMap = std::map<std::string, int>; |
| |
| static int next_id = 0; |
| static base::NoDestructor<ContextIdMap> context_map; |
| |
| // we need to get the original context to make sure we take the right context. |
| content::BrowserContext* original_context = |
| ExtensionsBrowserClient::Get()->GetOriginalContext(context); |
| const std::string& context_id = original_context->UniqueId(); |
| auto iter = context_map->find(context_id); |
| if (iter == context_map->end()) { |
| iter = context_map->insert(std::make_pair(context_id, next_id++)).first; |
| } |
| DCHECK(iter->second != kUnspecifiedContextId); |
| return iter->second; |
| } |
| |
| bool IsExtensionVisibleToContext(const Extension& extension, |
| content::BrowserContext* browser_context) { |
| // Renderers don't need to know about themes. |
| if (extension.is_theme()) { |
| return false; |
| } |
| |
| // Only extensions enabled in incognito mode should be loaded in an incognito |
| // renderer. However extensions which can't be enabled in the incognito mode |
| // (e.g. platform apps) should also be loaded in an incognito renderer to |
| // ensure connections from incognito tabs to such extensions work. |
| return !browser_context->IsOffTheRecord() || |
| !CanBeIncognitoEnabled(&extension) || |
| IsIncognitoEnabled(extension.id(), browser_context); |
| } |
| |
| void InitializeFileSchemeAccessForExtension( |
| int render_process_id, |
| const ExtensionId& extension_id, |
| content::BrowserContext* browser_context) { |
| ExtensionPrefs* prefs = ExtensionPrefs::Get(browser_context); |
| // TODO(karandeepb): This should probably use |
| // extensions::util::AllowFileAccess. |
| if (prefs->AllowFileAccess(extension_id)) { |
| content::ChildProcessSecurityPolicy::GetInstance()->GrantRequestScheme( |
| render_process_id, url::kFileScheme); |
| } |
| } |
| |
| const gfx::ImageSkia& GetDefaultAppIcon() { |
| return *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| IDR_APP_DEFAULT_ICON); |
| } |
| |
| const gfx::ImageSkia& GetDefaultExtensionIcon() { |
| return *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| IDR_EXTENSION_DEFAULT_ICON); |
| } |
| |
| ExtensionId GetExtensionIdForSiteInstance( |
| content::SiteInstance& site_instance) { |
| // <webview> guests always store the ExtensionId in the partition domain. |
| if (site_instance.IsGuest()) { |
| return site_instance.GetStoragePartitionConfig().partition_domain(); |
| } |
| |
| // This works for both apps and extensions because the site has been |
| // normalized to the extension URL for hosted apps. |
| const GURL& site_url = site_instance.GetSiteURL(); |
| if (!site_url.SchemeIs(kExtensionScheme)) { |
| return ExtensionId(); |
| } |
| |
| // Navigating to a disabled (or uninstalled or not-yet-installed) extension |
| // will set the site URL to chrome-extension://invalid. |
| ExtensionId maybe_extension_id = site_url.host(); |
| if (maybe_extension_id == "invalid") { |
| return ExtensionId(); |
| } |
| |
| // Otherwise,`site_url.host()` should always be a valid extension id. In |
| // particular, navigations should never commit a URL that uses a dynamic, |
| // GUID-based hostname (such navigations should redirect to the statically |
| // known, extension-id-based hostname). |
| DCHECK(crx_file::id_util::IdIsValid(maybe_extension_id)) |
| << "; maybe_extension_id = " << maybe_extension_id; |
| return maybe_extension_id; |
| } |
| |
| std::string GetExtensionIdFromFrame( |
| content::RenderFrameHost* render_frame_host) { |
| const GURL& site = render_frame_host->GetSiteInstance()->GetSiteURL(); |
| if (!site.SchemeIs(kExtensionScheme)) { |
| return std::string(); |
| } |
| |
| return site.host(); |
| } |
| |
| bool CanRendererHostExtensionOrigin(int render_process_id, |
| const ExtensionId& extension_id, |
| bool is_sandboxed) { |
| url::Origin extension_origin = |
| Extension::CreateOriginFromExtensionId(extension_id); |
| if (is_sandboxed) { |
| // If the extension frame is sandboxed, the corresponding process is only |
| // allowed to host opaque origins, per crbug.com/325410297. Therefore, |
| // convert the origin into an opaque origin, and note that HostsOrigin() |
| // will still validate the extension ID in the origin's precursor. |
| extension_origin = extension_origin.DeriveNewOpaqueOrigin(); |
| } |
| auto* policy = content::ChildProcessSecurityPolicy::GetInstance(); |
| return policy->HostsOrigin(render_process_id, extension_origin); |
| } |
| |
| bool CanRendererActOnBehalfOfExtension( |
| const ExtensionId& extension_id, |
| content::RenderFrameHost* render_frame_host, |
| content::RenderProcessHost& render_process_host, |
| bool include_user_scripts) { |
| // TODO(lukasza): Some of the checks below can be restricted to specific |
| // context types (e.g. an empty `extension_id` should not happen in an |
| // extension context; and the SiteInstance-based check should only be needed |
| // for hosted apps). Consider leveraging ProcessMap::GetMostLikelyContextType |
| // to implement this kind of restrictions. Note that |
| // ExtensionFunctionDispatcher::CreateExtensionFunction already calls |
| // GetMostLikelyContextType - some refactoring might be needed to avoid |
| // duplicating the work. |
| |
| // Allow empty extension id (it seems okay to assume that no |
| // extension-specific special powers will be granted without an extension id). |
| // For instance, WebUI pages may call private APIs like developerPrivate, |
| // settingsPrivate, metricsPrivate, and others. In these cases, there is no |
| // associated extension ID. |
| // |
| // TODO(lukasza): Investigate if the exception below can be avoided if |
| // `render_process_host` hosts HTTP origins (i.e. if the exception can be |
| // restricted to NTP, and/or chrome://... cases. |
| if (extension_id.empty()) { |
| return true; |
| } |
| |
| // Did `render_process_id` run a content script or user script from |
| // `extension_id`? |
| // TODO(crbug.com/40055126): Ideally, we'd only check content script/ |
| // user script status if the renderer claimed to be acting on behalf of the |
| // corresponding type (e.g. mojom::ContextType::kContentScript). We evaluate |
| // this later in ProcessMap::CanProcessHostContextType(), but we could be |
| // stricter by including it here. |
| if (ScriptInjectionTracker::DidProcessRunContentScriptFromExtension( |
| render_process_host, extension_id) || |
| (ScriptInjectionTracker::DidProcessRunUserScriptFromExtension( |
| render_process_host, extension_id) && |
| include_user_scripts)) { |
| return true; |
| } |
| |
| // CanRendererHostExtensionOrigin() needs to know if the extension is |
| // sandboxed, so check the sandbox flags if this request is for an extension |
| // frame. Note that extension workers cannot be sandboxed since workers aren't |
| // supported in opaque origins. |
| bool is_sandboxed = |
| render_frame_host && |
| render_frame_host->IsSandboxed(network::mojom::WebSandboxFlags::kOrigin); |
| |
| // Can `render_process_id` host a chrome-extension:// origin (frame, worker, |
| // etc.)? |
| if (CanRendererHostExtensionOrigin(render_process_host.GetDeprecatedID(), |
| extension_id, is_sandboxed)) { |
| return true; |
| } |
| |
| if (render_frame_host) { |
| DCHECK_EQ(render_process_host.GetDeprecatedID(), |
| render_frame_host->GetProcess()->GetDeprecatedID()); |
| content::SiteInstance& site_instance = |
| *render_frame_host->GetSiteInstance(); |
| |
| // Chrome Extension APIs can be accessed from some hosted apps. |
| // |
| // Today this is mostly needed by the Chrome Web Store's hosted app, but the |
| // code below doesn't make this assumption and allows *all* hosted apps |
| // based on the trustworthy, Browser-side information from the SiteInstance |
| // / SiteURL. This way the code is resilient to future changes + there are |
| // concerns that `chrome.test.sendMessage` might already be exposed to |
| // hosted apps (but maybe not covered by tests). |
| // |
| // Note that the condition below allows all extensions (i.e. not just hosted |
| // apps), but hosted apps aren't covered by the |
| // `CanRendererHostExtensionOrigin` call above (because the process lock of |
| // hosted apps is based on a https://, rather than chrome-extension:// url). |
| // |
| // GuestView is explicitly excluded, because we don't want to allow |
| // GuestViews to spoof the extension id of their host. |
| if (!site_instance.IsGuest() && |
| extension_id == util::GetExtensionIdForSiteInstance(site_instance)) { |
| return true; |
| } |
| } |
| |
| // Disallow any other cases. |
| return false; |
| } |
| |
| bool IsChromeApp(const ExtensionId& extension_id, |
| content::BrowserContext* context) { |
| const Extension* extension = |
| ExtensionRegistry::Get(context)->enabled_extensions().GetByID( |
| extension_id); |
| return extension->is_platform_app(); |
| } |
| |
| bool IsAppLaunchable(const ExtensionId& extension_id, |
| content::BrowserContext* context) { |
| DisableReasonSet reason = |
| ExtensionPrefs::Get(context)->GetDisableReasons(extension_id); |
| return !reason.contains(disable_reason::DISABLE_UNSUPPORTED_REQUIREMENT) && |
| !reason.contains(disable_reason::DISABLE_CORRUPTED); |
| } |
| |
| bool IsAppLaunchableWithoutEnabling(const ExtensionId& extension_id, |
| content::BrowserContext* context) { |
| return ExtensionRegistry::Get(context)->enabled_extensions().Contains( |
| extension_id); |
| } |
| |
| } // namespace util |
| } // namespace extensions |