| // Copyright 2021 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/script_injection_tracker.h" |
| |
| #include "base/check_is_test.h" |
| #include "base/containers/contains.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/raw_ref.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/trace_event/typed_macros.h" |
| #include "components/guest_view/browser/guest_view_base.h" |
| #include "components/sessions/content/session_tab_helper.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/browser/browser_frame_context_data.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/guest_view/web_view/web_view_content_script_manager.h" |
| #include "extensions/browser/url_loader_factory_manager.h" |
| #include "extensions/browser/user_script_manager.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/content_script_injection_url_getter.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/manifest_handlers/content_scripts_handler.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/trace_util.h" |
| #include "extensions/common/user_script.h" |
| #include "services/metrics/public/cpp/metrics_utils.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| |
| using perfetto::protos::pbzero::ChromeTrackEvent; |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // Helper for lazily attaching ExtensionIdSet to a RenderProcessHost. Used to |
| // track the set of extensions which have injected a JS script into a |
| // RenderProcessHost. |
| // |
| // We track script injection per-RenderProcessHost: |
| // 1. This matches the real security boundary that Site Isolation uses (the |
| // boundary of OS processes) and follows the precedent of |
| // content::ChildProcessSecurityPolicy. |
| // 2. This robustly handles initial empty documents (see the *InitialEmptyDoc* |
| // tests in //script_injection_tracker_browsertest.cc) and isn't impacted |
| // by ReadyToCommit races associated with DocumentUserData. |
| // For more information see: |
| // https://docs.google.com/document/d/1MFprp2ss2r9RNamJ7Jxva1bvRZvec3rzGceDGoJ6vW0/edit# |
| class RenderProcessHostUserData : public base::SupportsUserData::Data { |
| public: |
| static const RenderProcessHostUserData* Get( |
| const content::RenderProcessHost& process) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| return static_cast<RenderProcessHostUserData*>( |
| process.GetUserData(kUserDataKey)); |
| } |
| |
| static RenderProcessHostUserData& GetOrCreate( |
| content::RenderProcessHost& process) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| auto* self = static_cast<RenderProcessHostUserData*>( |
| process.GetUserData(kUserDataKey)); |
| |
| if (!self) { |
| // Create a new RenderProcessHostUserData if needed. The ownership is |
| // passed to the `process` (i.e. the new RenderProcessHostUserData will be |
| // destroyed at the same time as the `process` - this is why we don't need |
| // to purge or destroy the set from within ScriptInjectionTracker). |
| auto owned_self = |
| base::WrapUnique(new RenderProcessHostUserData(process)); |
| self = owned_self.get(); |
| process.SetUserData(kUserDataKey, std::move(owned_self)); |
| } |
| |
| DCHECK(self); |
| return *self; |
| } |
| |
| // base::SupportsUserData::Data override: |
| ~RenderProcessHostUserData() override { |
| TRACE_EVENT_END("extensions", perfetto::Track::FromPointer(this), |
| ChromeTrackEvent::kRenderProcessHost, *process_); |
| } |
| |
| bool HasScript(ScriptInjectionTracker::ScriptType script_type, |
| const ExtensionId& extension_id) const { |
| return base::Contains(GetScripts(script_type), extension_id); |
| } |
| |
| void AddScript(ScriptInjectionTracker::ScriptType script_type, |
| const ExtensionId& extension_id) { |
| TRACE_EVENT_INSTANT( |
| "extensions", |
| "ScriptInjectionTracker::RenderProcessHostUserData::AddScript", |
| ChromeTrackEvent::kRenderProcessHost, *process_, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension_id)); |
| GetScripts(script_type).insert(extension_id); |
| } |
| |
| const ExtensionIdSet& content_scripts() const { return content_scripts_; } |
| const ExtensionIdSet& user_scripts() const { return user_scripts_; } |
| |
| private: |
| explicit RenderProcessHostUserData(content::RenderProcessHost& process) |
| : process_(process) { |
| TRACE_EVENT_BEGIN("extensions", |
| "ScriptInjectionTracker::RenderProcessHostUserData", |
| perfetto::Track::FromPointer(this), |
| ChromeTrackEvent::kRenderProcessHost, *process_); |
| } |
| |
| const ExtensionIdSet& GetScripts( |
| ScriptInjectionTracker::ScriptType script_type) const { |
| switch (script_type) { |
| case ScriptInjectionTracker::ScriptType::kContentScript: |
| return content_scripts_; |
| case ScriptInjectionTracker::ScriptType::kUserScript: |
| return user_scripts_; |
| } |
| } |
| ExtensionIdSet& GetScripts(ScriptInjectionTracker::ScriptType script_type) { |
| return const_cast<ExtensionIdSet&>( |
| const_cast<const RenderProcessHostUserData*>(this)->GetScripts( |
| script_type)); |
| } |
| |
| static const char* kUserDataKey; |
| |
| // The sets of extension ids that have *ever* injected a content script or |
| // user script into this particular renderer process. This is the core data |
| // maintained by the ScriptInjectionTracker. |
| ExtensionIdSet content_scripts_; |
| ExtensionIdSet user_scripts_; |
| |
| // Only used for tracing. |
| const raw_ref<content::RenderProcessHost> process_; |
| }; |
| |
| const char* RenderProcessHostUserData::kUserDataKey = |
| "ScriptInjectionTracker's data"; |
| |
| std::vector<const UserScript*> GetVectorFromScriptList( |
| const UserScriptList& scripts) { |
| std::vector<const UserScript*> result; |
| result.reserve(scripts.size()); |
| for (const auto& script : scripts) { |
| result.push_back(script.get()); |
| } |
| return result; |
| } |
| |
| // Returns all the loaded dynamic scripts with `source` of `extension_id` on |
| // `frame`. |
| std::vector<const UserScript*> GetLoadedDynamicScripts( |
| const ExtensionId& extension_id, |
| UserScript::Source source, |
| content::RenderProcessHost& process) { |
| // `manager` can be null for some unit tests which do not initialize the |
| // ExtensionSystem. |
| UserScriptManager* manager = |
| ExtensionSystem::Get(process.GetBrowserContext())->user_script_manager(); |
| if (!manager) { |
| CHECK_IS_TEST(); |
| return std::vector<const UserScript*>(); |
| } |
| |
| const UserScriptList& loaded_dynamic_scripts = |
| manager->GetUserScriptLoaderForExtension(extension_id) |
| ->GetLoadedDynamicScripts(); |
| |
| std::vector<const UserScript*> scripts; |
| for (auto& loaded_script : loaded_dynamic_scripts) { |
| if (loaded_script->GetSource() == source) { |
| scripts.push_back(loaded_script.get()); |
| } |
| } |
| return scripts; |
| } |
| |
| // This function approximates ScriptContext::GetEffectiveDocumentURLForInjection |
| // from the renderer side. |
| GURL GetEffectiveDocumentURL( |
| content::RenderFrameHost* frame, |
| const GURL& document_url, |
| MatchOriginAsFallbackBehavior match_origin_as_fallback) { |
| // This is a simplification to avoid calling |
| // `BrowserFrameContextData::CanAccess` which is unable to replicate all of |
| // WebSecurityOrigin::CanAccess checks (e.g. universal access or file |
| // exceptions tracked on the renderer side). This is okay, because our only |
| // caller (DoesContentScriptMatch()) expects false positives. |
| constexpr bool kAllowInaccessibleParents = true; |
| |
| return ContentScriptInjectionUrlGetter::Get( |
| BrowserFrameContextData(frame), document_url, match_origin_as_fallback, |
| kAllowInaccessibleParents); |
| } |
| |
| // Returns whether the extension's scripts can run on `frame`. |
| bool CanExtensionScriptsAffectFrame(content::RenderFrameHost& frame, |
| const Extension& extension) { |
| // Most extension's scripts won't run on webviews. The only ones that may are |
| // those from extensions that can execute script everywhere. |
| auto* guest = guest_view::GuestViewBase::FromRenderFrameHost(&frame); |
| return !guest || PermissionsData::CanExecuteScriptEverywhere( |
| extension.id(), extension.location()); |
| } |
| |
| // Returns whether `extension` will inject any of `scripts` JavaScript content |
| // into the `frame` / `url`. Note that this function ignores CSS content |
| // scripts. This function approximates a subset of checks from |
| // UserScriptSet::GetInjectionForScript (which runs in the renderer process). |
| // Unlike the renderer version, the code below doesn't consider ability to |
| // create an injection host, nor the results of |
| // ScriptInjector::CanExecuteOnFrame, nor the path of `url_patterns`. |
| // Additionally the `effective_url` calculations are also only an approximation. |
| // This is okay, because the top-level doc comment for ScriptInjectionTracker |
| // documents that false positives are expected and why they are okay. |
| bool DoesScriptMatch(const Extension& extension, |
| const UserScript& script, |
| content::RenderFrameHost& frame, |
| const GURL& url) { |
| // ScriptInjectionTracker only needs to track Javascript content scripts (e.g. |
| // doesn't track CSS-only injections). |
| if (script.js_scripts().empty()) { |
| return false; |
| } |
| |
| GURL effective_url = |
| GetEffectiveDocumentURL(&frame, url, script.match_origin_as_fallback()); |
| auto* web_contents = content::WebContents::FromRenderFrameHost(&frame); |
| int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id(); |
| |
| // Script can inject if the extension has tab permissions for the url. |
| if (extension.permissions_data()->HasTabPermissionsForSecurityOrigin( |
| tab_id, effective_url)) { |
| return true; |
| } |
| |
| // Dynamic scripts can only inject when the extension has host permissions for |
| // the url. |
| auto script_source = script.GetSource(); |
| if ((script_source == UserScript::Source::kDynamicContentScript || |
| script_source == UserScript::Source::kDynamicUserScript) && |
| !extension.permissions_data()->HasHostPermission(effective_url)) { |
| return false; |
| } |
| |
| return script.url_patterns().MatchesSecurityOrigin(effective_url); |
| } |
| |
| void HandleProgrammaticScriptInjection( |
| base::PassKey<ScriptInjectionTracker> pass_key, |
| ScriptInjectionTracker::ScriptType script_type, |
| content::RenderFrameHost* frame, |
| const Extension& extension) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // Store `extension.id()` in `process_data`. ScriptInjectionTracker never |
| // removes entries from this set - once a renderer process gains an ability to |
| // talk on behalf of a content script, it retains this ability forever. Note |
| // that the `process_data` will be destroyed together with the |
| // RenderProcessHost (see also a comment inside |
| // RenderProcessHostUserData::GetOrCreate). |
| auto& process_data = |
| RenderProcessHostUserData::GetOrCreate(*frame->GetProcess()); |
| process_data.AddScript(script_type, extension.id()); |
| |
| URLLoaderFactoryManager::WillProgrammaticallyInjectContentScript( |
| pass_key, frame, extension); |
| } |
| |
| // Returns whether ``extension` will inject any of `scripts` JavaScript content |
| // into the `frame` / `url`. |
| bool DoScriptsMatch(const Extension& extension, |
| const std::vector<const UserScript*>& scripts, |
| content::RenderFrameHost& frame, |
| const GURL& url) { |
| return base::ranges::any_of( |
| scripts.begin(), scripts.end(), |
| [&extension, &frame, &url](const UserScript* script) { |
| return DoesScriptMatch(extension, *script, frame, url); |
| }); |
| } |
| |
| // Returns whether an `extension` can inject JavaScript web view scripts into |
| // the `frame` / `url`. |
| bool DoWebViewScripstMatch(const Extension& extension, |
| content::RenderFrameHost& frame) { |
| content::RenderProcessHost& process = *frame.GetProcess(); |
| TRACE_EVENT("extensions", "ScriptInjectionTracker/DoWebViewScripstMatch", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| |
| auto* guest = guest_view::GuestViewBase::FromRenderFrameHost(&frame); |
| if (!guest) { |
| // Not a guest; no webview scripts. |
| return false; |
| } |
| |
| // Return true if `extension` is an owner of `guest` and it registered |
| // content scripts using the `webview.addContentScripts` API. |
| GURL owner_site_url = guest->GetOwnerSiteURL(); |
| if (owner_site_url.SchemeIs(kExtensionScheme) && |
| owner_site_url.host_piece() == extension.id()) { |
| WebViewContentScriptManager* script_manager = |
| WebViewContentScriptManager::Get(frame.GetBrowserContext()); |
| int embedder_process_id = guest->owner_rfh()->GetProcess()->GetID(); |
| std::set<std::string> script_ids = script_manager->GetContentScriptIDSet( |
| embedder_process_id, guest->view_instance_id()); |
| |
| // Note - more granular checks (e.g. against URL patterns) are desirable |
| // for performance (to avoid creating unnecessary URLLoaderFactory via |
| // URLLoaderFactoryManager), but not necessarily for security (because |
| // there are anyway no OOPIFs inside the webView process - |
| // https://crbug.com/614463). At the same time, more granular checks are |
| // difficult to achieve, because the UserScript objects are not retained |
| // (i.e. only UserScriptIDs are available) by WebViewContentScriptManager. |
| if (!script_ids.empty()) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| // Returns whether an `extension` can inject JavaScript static content scripts |
| // into the `frame` / `url`. The `url` might be either the last committed URL |
| // of `frame` or the target of a ReadyToCommit navigation in `frame`. |
| bool DoStaticContentScriptsMatch(const Extension& extension, |
| content::RenderFrameHost& frame, |
| const GURL& url) { |
| content::RenderProcessHost& process = *frame.GetProcess(); |
| TRACE_EVENT("extensions", "ScriptInjectionTracker/DoStaticContentScriptMatch", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| |
| if (!CanExtensionScriptsAffectFrame(frame, extension)) { |
| return false; |
| } |
| |
| std::vector<const UserScript*> static_content_scripts = |
| GetVectorFromScriptList( |
| ContentScriptsInfo::GetContentScripts(&extension)); |
| return DoScriptsMatch(extension, static_content_scripts, frame, url); |
| } |
| |
| // Returns whether an `extension` can inject JavaScript dynamic content scripts |
| // into the `frame` / `url`. The `url` might be either the last committed |
| // URL of `frame` or the target of a ReadyToCommit navigation in `frame`. |
| bool DoDynamicContentScriptsMatch(const Extension& extension, |
| content::RenderFrameHost& frame, |
| const GURL& url) { |
| content::RenderProcessHost& process = *frame.GetProcess(); |
| TRACE_EVENT("extensions", |
| "ScriptInjectionTracker/DoDynamicContentScriptsMatch", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| |
| if (!CanExtensionScriptsAffectFrame(frame, extension)) { |
| return false; |
| } |
| |
| std::vector<const UserScript*> dynamic_user_scripts = GetLoadedDynamicScripts( |
| extension.id(), UserScript::Source::kDynamicContentScript, process); |
| return DoScriptsMatch(extension, dynamic_user_scripts, frame, url); |
| } |
| |
| // Returns whether an `extension` can inject JavaScript dynamic user scripts |
| // into the `frame` / `url`. The `url` might be either the last committed URL |
| // of `frame` or the target of a ReadyToCommit navigation in `frame`. |
| bool DoUserScriptsMatch(const Extension& extension, |
| content::RenderFrameHost& frame, |
| const GURL& url) { |
| content::RenderProcessHost& process = *frame.GetProcess(); |
| TRACE_EVENT("extensions", "ScriptInjectionTracker/DoUserScriptsMatch", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| |
| if (!CanExtensionScriptsAffectFrame(frame, extension)) { |
| return false; |
| } |
| |
| std::vector<const UserScript*> dynamic_user_scripts = GetLoadedDynamicScripts( |
| extension.id(), UserScript::Source::kDynamicUserScript, process); |
| return DoScriptsMatch(extension, dynamic_user_scripts, frame, url); |
| } |
| |
| // Returns all the extensions injecting content scripts into the `frame` / |
| // `url`. |
| std::vector<const Extension*> GetExtensionsInjectingContentScripts( |
| const ExtensionSet& extensions, |
| content::RenderFrameHost& frame, |
| const GURL& url) { |
| std::vector<const Extension*> extensions_injecting_scripts; |
| for (const auto& it : extensions) { |
| const Extension& extension = *it; |
| if (DoWebViewScripstMatch(extension, frame) || |
| DoStaticContentScriptsMatch(extension, frame, url) || |
| DoDynamicContentScriptsMatch(extension, frame, url)) { |
| extensions_injecting_scripts.push_back(&extension); |
| } |
| } |
| |
| return extensions_injecting_scripts; |
| } |
| |
| // Adds all scripts from `extension` that matches the `process` renderers to the |
| // process data. |
| void AddMatchingScriptsToProcess(const Extension& extension, |
| content::RenderProcessHost& process) { |
| bool any_frame_matches_content_scripts = false; |
| bool any_frame_matches_user_scripts = false; |
| process.ForEachRenderFrameHost([&any_frame_matches_content_scripts, |
| &any_frame_matches_user_scripts, |
| &extension](content::RenderFrameHost* frame) { |
| const GURL& url = frame->GetLastCommittedURL(); |
| if (!any_frame_matches_content_scripts) { |
| any_frame_matches_content_scripts = |
| DoWebViewScripstMatch(extension, *frame) || |
| DoStaticContentScriptsMatch(extension, *frame, url) || |
| DoDynamicContentScriptsMatch(extension, *frame, url); |
| } |
| if (!any_frame_matches_user_scripts) { |
| any_frame_matches_user_scripts = |
| DoUserScriptsMatch(extension, *frame, url); |
| } |
| }); |
| |
| if (any_frame_matches_content_scripts || any_frame_matches_user_scripts) { |
| auto& process_data = RenderProcessHostUserData::GetOrCreate(process); |
| if (any_frame_matches_content_scripts) { |
| process_data.AddScript(ScriptInjectionTracker::ScriptType::kContentScript, |
| extension.id()); |
| } |
| if (any_frame_matches_user_scripts) { |
| process_data.AddScript(ScriptInjectionTracker::ScriptType::kUserScript, |
| extension.id()); |
| } |
| } |
| } |
| |
| // Returns all the extensions injecting user scripts into the `frame` / `url`. |
| std::vector<const Extension*> GetExtensionsInjectingUserScripts( |
| const ExtensionSet& extensions, |
| content::RenderFrameHost& frame, |
| const GURL& url) { |
| std::vector<const Extension*> extensions_injecting_scripts; |
| for (const auto& it : extensions) { |
| const Extension& extension = *it; |
| if (DoUserScriptsMatch(extension, frame, url)) { |
| extensions_injecting_scripts.push_back(&extension); |
| } |
| } |
| |
| return extensions_injecting_scripts; |
| } |
| |
| void RecordUkm(content::NavigationHandle* navigation, |
| int extensions_injecting_content_script_count) { |
| using PermissionID = extensions::mojom::APIPermissionID; |
| const ExtensionSet& enabled_extensions = |
| ExtensionRegistry::Get( |
| navigation->GetRenderFrameHost()->GetProcess()->GetBrowserContext()) |
| ->enabled_extensions(); |
| int enabled_extension_count = 0; |
| int enabled_extension_count_has_host_permissions = 0; |
| int web_request_permission_count = 0; |
| int web_request_auth_provider_permission_count = 0; |
| int web_request_blocking_permission_count = 0; |
| int declarative_net_request_permission_count = 0; |
| int declarative_net_request_feedback_permission_count = 0; |
| int declarative_net_request_with_host_access_permission_count = 0; |
| int declarative_web_request_permission_count = 0; |
| for (const scoped_refptr<const Extension>& extension : enabled_extensions) { |
| if (!extension->is_extension()) { |
| continue; |
| } |
| // Ignore component extensions. |
| if (Manifest::IsComponentLocation(extension->location())) { |
| continue; |
| } |
| enabled_extension_count++; |
| const PermissionsData* permissions = extension->permissions_data(); |
| if (!permissions) { |
| continue; |
| } |
| if (!permissions->HasHostPermission(navigation->GetURL())) { |
| continue; |
| } |
| enabled_extension_count_has_host_permissions++; |
| if (permissions->HasAPIPermission(PermissionID::kWebRequest)) { |
| web_request_permission_count++; |
| } |
| if (permissions->HasAPIPermission(PermissionID::kWebRequestAuthProvider)) { |
| web_request_auth_provider_permission_count++; |
| } |
| if (permissions->HasAPIPermission(PermissionID::kWebRequestBlocking)) { |
| web_request_blocking_permission_count++; |
| } |
| if (permissions->HasAPIPermission(PermissionID::kDeclarativeNetRequest)) { |
| declarative_net_request_permission_count++; |
| } |
| if (permissions->HasAPIPermission( |
| PermissionID::kDeclarativeNetRequestFeedback)) { |
| declarative_net_request_feedback_permission_count++; |
| } |
| if (permissions->HasAPIPermission( |
| PermissionID::kDeclarativeNetRequestWithHostAccess)) { |
| declarative_net_request_with_host_access_permission_count++; |
| } |
| if (permissions->HasAPIPermission(PermissionID::kDeclarativeWebRequest)) { |
| declarative_web_request_permission_count++; |
| } |
| } |
| |
| const double kBucketSpacing = 2; |
| ukm::builders::Extensions_OnNavigation(navigation->GetNextPageUkmSourceId()) |
| .SetEnabledExtensionCount( |
| ukm::GetExponentialBucketMin(enabled_extension_count, kBucketSpacing)) |
| .SetEnabledExtensionCount_InjectContentScript( |
| ukm::GetExponentialBucketMin( |
| extensions_injecting_content_script_count, kBucketSpacing)) |
| .SetEnabledExtensionCount_HaveHostPermissions( |
| ukm::GetExponentialBucketMin( |
| enabled_extension_count_has_host_permissions, kBucketSpacing)) |
| .SetWebRequestPermissionCount(ukm::GetExponentialBucketMin( |
| web_request_permission_count, kBucketSpacing)) |
| .SetWebRequestAuthProviderPermissionCount(ukm::GetExponentialBucketMin( |
| web_request_auth_provider_permission_count, kBucketSpacing)) |
| .SetWebRequestBlockingPermissionCount(ukm::GetExponentialBucketMin( |
| web_request_blocking_permission_count, kBucketSpacing)) |
| .SetDeclarativeNetRequestPermissionCount(ukm::GetExponentialBucketMin( |
| declarative_net_request_permission_count, kBucketSpacing)) |
| .SetDeclarativeNetRequestFeedbackPermissionCount( |
| ukm::GetExponentialBucketMin( |
| declarative_net_request_feedback_permission_count, |
| kBucketSpacing)) |
| .SetDeclarativeNetRequestWithHostAccessPermissionCount( |
| ukm::GetExponentialBucketMin( |
| declarative_net_request_with_host_access_permission_count, |
| kBucketSpacing)) |
| .SetDeclarativeWebRequestPermissionCount(ukm::GetExponentialBucketMin( |
| declarative_web_request_permission_count, kBucketSpacing)) |
| .Record(ukm::UkmRecorder::Get()); |
| } |
| |
| const Extension* FindExtensionByHostId(content::BrowserContext* browser_context, |
| const mojom::HostID& host_id) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| switch (host_id.type) { |
| // TODO(cmp): Investigate whether Controlled Frame support is needed in |
| // ScriptInjectionTracker. |
| case mojom::HostID::HostType::kControlledFrameEmbedder: |
| case mojom::HostID::HostType::kWebUi: |
| // ScriptInjectionTracker only tracks extensions. |
| return nullptr; |
| case mojom::HostID::HostType::kExtensions: |
| break; |
| } |
| |
| const ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context); |
| DCHECK(registry); // WillExecuteCode and DidUpdateScriptsInRenderer |
| // shouldn't happen during shutdown. |
| |
| const Extension* extension = |
| registry->enabled_extensions().GetByID(host_id.id); |
| |
| return extension; |
| } |
| |
| // Stores extensions injecting scripts with `script_type` in `process` data. |
| void StoreExtensionsInjectingScripts( |
| const std::vector<const Extension*>& extensions, |
| ScriptInjectionTracker::ScriptType script_type, |
| content::RenderProcessHost& process) { |
| // ContentScriptTracker never removes entries from this set - once a |
| // renderer process gains an ability to talk on behalf of a content script, |
| // it retains this ability forever. Note that the `process_data` will be |
| // destroyed together with the RenderProcessHost (see also a comment inside |
| // RenderProcessHostUserData::GetOrCreate). |
| auto& process_data = RenderProcessHostUserData::GetOrCreate(process); |
| for (const Extension* extension : extensions) { |
| process_data.AddScript(script_type, extension->id()); |
| } |
| } |
| |
| bool DidProcessRunScriptFromExtension( |
| ScriptInjectionTracker::ScriptType script_type, |
| const content::RenderProcessHost& process, |
| const ExtensionId& extension_id) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(!extension_id.empty()); |
| |
| // Check if we've been notified about the content script injection via |
| // ReadyToCommitNavigation or WillExecuteCode methods. |
| const auto* process_data = RenderProcessHostUserData::Get(process); |
| if (!process_data) { |
| return false; |
| } |
| |
| return process_data->HasScript(script_type, extension_id); |
| } |
| |
| } // namespace |
| |
| // static |
| ExtensionIdSet |
| ScriptInjectionTracker::GetExtensionsThatRanContentScriptsInProcess( |
| const content::RenderProcessHost& process) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| const auto* process_data = RenderProcessHostUserData::Get(process); |
| if (!process_data) { |
| return {}; |
| } |
| |
| return process_data->content_scripts(); |
| } |
| |
| // static |
| bool ScriptInjectionTracker::DidProcessRunContentScriptFromExtension( |
| const content::RenderProcessHost& process, |
| const ExtensionId& extension_id) { |
| return DidProcessRunScriptFromExtension(ScriptType::kContentScript, process, |
| extension_id); |
| } |
| |
| // static |
| bool ScriptInjectionTracker::DidProcessRunUserScriptFromExtension( |
| const content::RenderProcessHost& process, |
| const ExtensionId& extension_id) { |
| return DidProcessRunScriptFromExtension(ScriptType::kUserScript, process, |
| extension_id); |
| } |
| |
| // static |
| void ScriptInjectionTracker::ReadyToCommitNavigation( |
| base::PassKey<ExtensionWebContentsObserver> pass_key, |
| content::NavigationHandle* navigation) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| content::RenderFrameHost& frame = *navigation->GetRenderFrameHost(); |
| content::RenderProcessHost& process = *frame.GetProcess(); |
| TRACE_EVENT("extensions", "ScriptInjectionTracker::ReadyToCommitNavigation", |
| ChromeTrackEvent::kRenderProcessHost, process); |
| |
| const GURL& url = navigation->GetURL(); |
| const ExtensionRegistry* registry = |
| ExtensionRegistry::Get(process.GetBrowserContext()); |
| DCHECK(registry); // This method shouldn't be called during shutdown. |
| const ExtensionSet& extensions = registry->enabled_extensions(); |
| |
| // Need to call StoreExtensionsInjectingScripts at ReadyToCommitNavigation |
| // time to deal with a (hypothetical, not confirmed by tests) race condition |
| // where Browser process sends Commit IPC and then immediately disables the |
| // extension. In this scenario, the renderer may run some content scripts, |
| // even though at DidCommit time the Browser will see that the extension has |
| // been disabled. |
| std::vector<const Extension*> extensions_injecting_content_scripts = |
| GetExtensionsInjectingContentScripts(extensions, frame, url); |
| std::vector<const Extension*> extensions_injecting_user_scripts = |
| GetExtensionsInjectingUserScripts(extensions, frame, url); |
| StoreExtensionsInjectingScripts( |
| extensions_injecting_content_scripts, |
| ScriptInjectionTracker::ScriptType::kContentScript, process); |
| StoreExtensionsInjectingScripts( |
| extensions_injecting_user_scripts, |
| ScriptInjectionTracker::ScriptType::kUserScript, process); |
| |
| // Notify URLLoaderFactoryManager for both user and content scripts. This |
| // needs to happen at ReadyToCommitNavigation time (i.e. before constructing a |
| // URLLoaderFactory that will be sent to the Renderer in a Commit IPC). |
| // TODO(crbug.com/1495177): This should only use webview scripts, since it's |
| // not needed for all extensions. |
| extensions_injecting_content_scripts.reserve( |
| extensions_injecting_content_scripts.size() + |
| extensions_injecting_user_scripts.size()); |
| extensions_injecting_content_scripts.insert( |
| extensions_injecting_content_scripts.end(), |
| extensions_injecting_user_scripts.begin(), |
| extensions_injecting_user_scripts.end()); |
| URLLoaderFactoryManager::WillInjectContentScriptsWhenNavigationCommits( |
| base::PassKey<ScriptInjectionTracker>(), navigation, |
| extensions_injecting_content_scripts); |
| } |
| |
| // static |
| void ScriptInjectionTracker::DidFinishNavigation( |
| base::PassKey<ExtensionWebContentsObserver> pass_key, |
| content::NavigationHandle* navigation) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // Only consider cross-document navigations that actually commit. (Documents |
| // associated with same-document navigations should have already been |
| // processed by an earlier DidFinishNavigation. Navigations that don't |
| // commit/load won't inject content scripts. Content script injections are |
| // primarily driven by URL matching and therefore failed navigations may still |
| // end up injecting content scripts into the error page. Pre-rendered pages |
| // already ran content scripts at the initial navigation and don't need to |
| // run them again on activation.) |
| if (!navigation->HasCommitted() || navigation->IsSameDocument() || |
| navigation->IsPrerenderedPageActivation()) { |
| return; |
| } |
| |
| content::RenderFrameHost& frame = *navigation->GetRenderFrameHost(); |
| content::RenderProcessHost& process = *frame.GetProcess(); |
| TRACE_EVENT("extensions", "ScriptInjectionTracker::DidFinishNavigation", |
| ChromeTrackEvent::kRenderProcessHost, process); |
| |
| const GURL& url = navigation->GetURL(); |
| const ExtensionRegistry* registry = |
| ExtensionRegistry::Get(process.GetBrowserContext()); |
| DCHECK(registry); // This method shouldn't be called during shutdown. |
| const ExtensionSet& extensions = registry->enabled_extensions(); |
| |
| // Calling StoreExtensionsInjectingScripts in response to DidCommit IPC is |
| // required for correct handling of the race condition from |
| // https://crbug.com/1312125. |
| std::vector<const Extension*> extensions_injecting_content_scripts = |
| GetExtensionsInjectingContentScripts(extensions, frame, url); |
| std::vector<const Extension*> extensions_injecting_user_scripts = |
| GetExtensionsInjectingUserScripts(extensions, frame, url); |
| StoreExtensionsInjectingScripts( |
| extensions_injecting_content_scripts, |
| ScriptInjectionTracker::ScriptType::kContentScript, process); |
| StoreExtensionsInjectingScripts( |
| extensions_injecting_user_scripts, |
| ScriptInjectionTracker::ScriptType::kUserScript, process); |
| |
| int num_extensions_injecting_scripts = |
| extensions_injecting_content_scripts.size() + |
| extensions_injecting_user_scripts.size(); |
| RecordUkm(navigation, num_extensions_injecting_scripts); |
| } |
| |
| // static |
| void ScriptInjectionTracker::WillExecuteCode( |
| base::PassKey<ScriptExecutor> pass_key, |
| ScriptType script_type, |
| content::RenderFrameHost* frame, |
| const mojom::HostID& host_id) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| content::RenderProcessHost& process = *frame->GetProcess(); |
| TRACE_EVENT("extensions", "ScriptInjectionTracker::WillExecuteCode/1", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(host_id.id)); |
| |
| const Extension* extension = |
| FindExtensionByHostId(process.GetBrowserContext(), host_id); |
| if (!extension) { |
| return; |
| } |
| |
| HandleProgrammaticScriptInjection(PassKey(), script_type, frame, *extension); |
| } |
| |
| // static |
| void ScriptInjectionTracker::WillExecuteCode( |
| base::PassKey<RequestContentScript> pass_key, |
| content::RenderFrameHost* frame, |
| const Extension& extension) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| TRACE_EVENT("extensions", "ScriptInjectionTracker::WillExecuteCode/2", |
| ChromeTrackEvent::kRenderProcessHost, *frame->GetProcess(), |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| |
| // Declarative content scripts are only ever of a kContentScript type and |
| // never handle user scripts. |
| HandleProgrammaticScriptInjection(PassKey(), ScriptType::kContentScript, |
| frame, extension); |
| } |
| |
| // static |
| void ScriptInjectionTracker::WillGrantActiveTab( |
| base::PassKey<ActiveTabPermissionGranter> pass_key, |
| const Extension& extension, |
| content::RenderProcessHost& process) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| AddMatchingScriptsToProcess(extension, process); |
| } |
| |
| // static |
| void ScriptInjectionTracker::DidUpdateScriptsInRenderer( |
| base::PassKey<UserScriptLoader> pass_key, |
| const mojom::HostID& host_id, |
| content::RenderProcessHost& process) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| TRACE_EVENT( |
| "extensions", "ScriptInjectionTracker::DidUpdateScriptsInRenderer", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, ExtensionIdForTracing(host_id.id)); |
| |
| scoped_refptr<const Extension> extension = |
| FindExtensionByHostId(process.GetBrowserContext(), host_id); |
| if (!extension) { |
| return; |
| } |
| |
| AddMatchingScriptsToProcess(*extension, process); |
| } |
| |
| // static |
| void ScriptInjectionTracker::DidUpdatePermissionsInRenderer( |
| base::PassKey<PermissionsUpdater> pass_key, |
| const Extension& extension, |
| content::RenderProcessHost& process) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| AddMatchingScriptsToProcess(extension, process); |
| } |
| |
| // static |
| bool ScriptInjectionTracker::DoStaticContentScriptsMatchForTesting( |
| const Extension& extension, |
| content::RenderFrameHost* frame, |
| const GURL& url) { |
| return DoStaticContentScriptsMatch(extension, *frame, url); |
| } |
| |
| namespace debug { |
| |
| namespace { |
| |
| base::debug::CrashKeyString* GetRegistryStatusCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "extension_registry_status", base::debug::CrashKeySize::Size256); |
| return crash_key; |
| } |
| |
| std::string GetRegistryStatusValue(const ExtensionId& extension_id, |
| content::BrowserContext& browser_context) { |
| std::string result = "status="; |
| ExtensionRegistry* registry = ExtensionRegistry::Get(&browser_context); |
| if (registry->enabled_extensions().Contains(extension_id)) { |
| result += "enabled,"; |
| } |
| if (registry->disabled_extensions().Contains(extension_id)) { |
| result += "disabled,"; |
| } |
| if (registry->terminated_extensions().Contains(extension_id)) { |
| result += "terminated,"; |
| } |
| if (registry->blocklisted_extensions().Contains(extension_id)) { |
| result += "blocklisted,"; |
| } |
| if (registry->blocked_extensions().Contains(extension_id)) { |
| result += "blocked,"; |
| } |
| if (registry->ready_extensions().Contains(extension_id)) { |
| result += "ready,"; |
| } |
| return result; |
| } |
| |
| base::debug::CrashKeyString* GetIsIncognitoCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "is_incognito", base::debug::CrashKeySize::Size32); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetLastCommittedOriginCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "script_frame_last_committed_origin", base::debug::CrashKeySize::Size256); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetLastCommittedUrlCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "script_frame_last_committed_url", base::debug::CrashKeySize::Size256); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetLifecycleStateCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "lifecycle_state", base::debug::CrashKeySize::Size32); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetIsGuestCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "is_guest", base::debug::CrashKeySize::Size32); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetDoWebViewScriptsMatchCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "do_web_view_scripts_match", base::debug::CrashKeySize::Size32); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetDoStaticContentScriptsMatchCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "do_static_content_scripts_match", base::debug::CrashKeySize::Size32); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetDoDynamicContentScriptsMatchCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "do_dynamic_content_scripts_match", base::debug::CrashKeySize::Size32); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetDoUserScriptsMatchCrashKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "do_user_scripts_match", base::debug::CrashKeySize::Size32); |
| return crash_key; |
| } |
| |
| const char* BoolToCrashKeyValue(bool value) { |
| return value ? "yes" : "no"; |
| } |
| |
| } // namespace |
| |
| ScopedScriptInjectionTrackerFailureCrashKeys:: |
| ScopedScriptInjectionTrackerFailureCrashKeys( |
| content::BrowserContext& browser_context, |
| const ExtensionId& extension_id) |
| : registry_status_crash_key_( |
| GetRegistryStatusCrashKey(), |
| GetRegistryStatusValue(extension_id, browser_context)), |
| is_incognito_crash_key_( |
| GetIsIncognitoCrashKey(), |
| BoolToCrashKeyValue(browser_context.IsOffTheRecord())) {} |
| |
| ScopedScriptInjectionTrackerFailureCrashKeys:: |
| ScopedScriptInjectionTrackerFailureCrashKeys( |
| content::RenderFrameHost& frame, |
| const ExtensionId& extension_id) |
| : ScopedScriptInjectionTrackerFailureCrashKeys(*frame.GetBrowserContext(), |
| extension_id) { |
| const GURL& frame_url = frame.GetLastCommittedURL(); |
| last_committed_origin_crash_key_.emplace( |
| GetLastCommittedOriginCrashKey(), |
| frame.GetLastCommittedOrigin().GetDebugString()); |
| last_committed_url_crash_key_.emplace(GetLastCommittedUrlCrashKey(), |
| frame_url.possibly_invalid_spec()); |
| lifecycle_state_crash_key_.emplace( |
| GetLifecycleStateCrashKey(), |
| base::NumberToString(static_cast<int>(frame.GetLifecycleState()))); |
| |
| auto* guest = guest_view::GuestViewBase::FromRenderFrameHost(&frame); |
| is_guest_crash_key_.emplace(GetIsGuestCrashKey(), |
| BoolToCrashKeyValue(!!guest)); |
| |
| const ExtensionRegistry* registry = |
| ExtensionRegistry::Get(frame.GetBrowserContext()); |
| CHECK(registry); |
| |
| const Extension* extension = |
| registry->enabled_extensions().GetByID(extension_id); |
| if (extension) { |
| do_web_view_scripts_match_crash_key_.emplace( |
| GetDoWebViewScriptsMatchCrashKey(), |
| BoolToCrashKeyValue(DoWebViewScripstMatch(*extension, frame))); |
| do_static_content_scripts_match_crash_key_.emplace( |
| GetDoStaticContentScriptsMatchCrashKey(), |
| BoolToCrashKeyValue( |
| DoStaticContentScriptsMatch(*extension, frame, frame_url))); |
| do_dynamic_content_scripts_match_crash_key_.emplace( |
| GetDoDynamicContentScriptsMatchCrashKey(), |
| BoolToCrashKeyValue( |
| DoDynamicContentScriptsMatch(*extension, frame, frame_url))); |
| do_user_scripts_match_crash_key_.emplace( |
| GetDoUserScriptsMatchCrashKey(), |
| BoolToCrashKeyValue(DoUserScriptsMatch(*extension, frame, frame_url))); |
| } |
| } |
| |
| ScopedScriptInjectionTrackerFailureCrashKeys:: |
| ~ScopedScriptInjectionTrackerFailureCrashKeys() = default; |
| |
| } // namespace debug |
| } // namespace extensions |