| // 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/trace_event/typed_macros.h" |
| #include "components/guest_view/browser/guest_view_base.h" |
| #include "content/public/browser/browser_thread.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); |
| } |
| |
| void AddFrame(content::RenderFrameHost* frame) { frames_.insert(frame); } |
| void RemoveFrame(content::RenderFrameHost* frame) { frames_.erase(frame); } |
| const std::set<content::RenderFrameHost*>& frames() const { return frames_; } |
| |
| 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_; |
| |
| // Set of frames that are *currently* hosted in this particular renderer |
| // process. This is mostly used just to get GetLastCommittedURL of these |
| // frames so that when a new extension is loaded, then ScriptInjectionTracker |
| // can know where content scripts may be injected. |
| std::set<content::RenderFrameHost*> frames_; |
| |
| // 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 `script` will inject 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 UserScript& script, |
| content::RenderFrameHost& frame, |
| const GURL& url) { |
| content::RenderProcessHost& process = *frame.GetProcess(); |
| const ExtensionId& extension_id = script.extension_id(); |
| |
| // ScriptInjectionTracker only needs to track Javascript content scripts (e.g. |
| // doesn't track CSS-only injections). |
| if (script.js_scripts().empty()) { |
| TRACE_EVENT_INSTANT("extensions", |
| "ScriptInjectionTracker/DoesScriptMatch=false(non-js)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension_id)); |
| return false; |
| } |
| |
| GURL effective_url = |
| GetEffectiveDocumentURL(&frame, url, script.match_origin_as_fallback()); |
| if (script.url_patterns().MatchesSecurityOrigin(effective_url)) { |
| TRACE_EVENT_INSTANT("extensions", |
| "ScriptInjectionTracker/DoesScriptMatch=true", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension_id)); |
| return true; |
| } |
| |
| TRACE_EVENT_INSTANT("extensions", |
| "ScriptInjectionTracker/DoesScriptMatch=false(mismatch)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension_id)); |
| return false; |
| } |
| |
| 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 any of `scripts` will inject JavaScript content into the |
| // `frame` / `url`. |
| bool DoScriptsMatch(const std::vector<const UserScript*>& scripts, |
| content::RenderFrameHost& frame, |
| const GURL& url) { |
| return base::ranges::any_of(scripts.begin(), scripts.end(), |
| [&frame, &url](const UserScript* script) { |
| return DoesScriptMatch(*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. |
| TRACE_EVENT_INSTANT( |
| "extensions", |
| "ScriptInjectionTracker/DoWebViewScripstMatch=false(non-guest)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| 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()) { |
| TRACE_EVENT_INSTANT("extensions", |
| "ScriptInjectionTracker/DoWebViewScripstMatch=true", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return true; |
| } |
| } |
| |
| TRACE_EVENT_INSTANT( |
| "extensions", |
| "ScriptInjectionTracker/DoWebViewScripstMatch=false(nomatch)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| 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)) { |
| TRACE_EVENT_INSTANT( |
| "extensions", |
| "ScriptInjectionTracker/DoStaticContentScriptMatch=false(webview)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return false; |
| } |
| |
| std::vector<const UserScript*> static_content_scripts = |
| GetVectorFromScriptList( |
| ContentScriptsInfo::GetContentScripts(&extension)); |
| if (DoScriptsMatch(static_content_scripts, frame, url)) { |
| TRACE_EVENT_INSTANT( |
| "extensions", "ScriptInjectionTracker/DoStaticContentScriptMatch=true", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return true; |
| } |
| |
| TRACE_EVENT_INSTANT( |
| "extensions", |
| "ScriptInjectionTracker/DoStaticContentScriptMatch=false(nomatch)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return false; |
| } |
| |
| // 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)) { |
| TRACE_EVENT_INSTANT( |
| "extensions", |
| "ScriptInjectionTracker/DoDynamicContentScriptsMatch=false(webview)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return false; |
| } |
| |
| std::vector<const UserScript*> dynamic_user_scripts = GetLoadedDynamicScripts( |
| extension.id(), UserScript::Source::kDynamicContentScript, process); |
| if (DoScriptsMatch(dynamic_user_scripts, frame, url)) { |
| TRACE_EVENT_INSTANT( |
| "extensions", |
| "ScriptInjectionTracker/DoDynamicContentScriptsMatch=true", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return true; |
| } |
| |
| TRACE_EVENT_INSTANT( |
| "extensions", |
| "ScriptInjectionTracker/DoDynamicContentScriptsMatch=false(nomatch)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return false; |
| } |
| |
| // 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)) { |
| TRACE_EVENT_INSTANT( |
| "extensions", |
| "ScriptInjectionTracker/DoUserScriptsMatch=false(webview)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return false; |
| } |
| |
| std::vector<const UserScript*> dynamic_user_scripts = GetLoadedDynamicScripts( |
| extension.id(), UserScript::Source::kDynamicUserScript, process); |
| if (DoScriptsMatch(dynamic_user_scripts, frame, url)) { |
| TRACE_EVENT_INSTANT( |
| "extensions", "ScriptInjectionTracker/DoUserScriptsMatch=true(dynamic)", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return true; |
| } |
| |
| TRACE_EVENT_INSTANT("extensions", |
| "ScriptInjectionTracker/DoUserScriptsMatch=false", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(extension.id())); |
| return false; |
| } |
| |
| // 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; |
| } |
| |
| // 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) { |
| 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 WillUpdateContentScriptsInRenderer |
| // 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::RenderFrameCreated( |
| base::PassKey<ExtensionWebContentsObserver> pass_key, |
| content::RenderFrameHost* frame) { |
| TRACE_EVENT("extensions", "ScriptInjectionTracker::RenderFrameCreated", |
| ChromeTrackEvent::kRenderProcessHost, *frame->GetProcess()); |
| |
| auto& process_data = |
| RenderProcessHostUserData::GetOrCreate(*frame->GetProcess()); |
| process_data.AddFrame(frame); |
| } |
| |
| // static |
| void ScriptInjectionTracker::RenderFrameDeleted( |
| base::PassKey<ExtensionWebContentsObserver> pass_key, |
| content::RenderFrameHost* frame) { |
| TRACE_EVENT("extensions", "ScriptInjectionTracker::RenderFrameDeleted", |
| ChromeTrackEvent::kRenderProcessHost, *frame->GetProcess()); |
| |
| auto& process_data = |
| RenderProcessHostUserData::GetOrCreate(*frame->GetProcess()); |
| process_data.RemoveFrame(frame); |
| } |
| |
| // 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::WillUpdateScriptsInRenderer( |
| base::PassKey<UserScriptLoader> pass_key, |
| const mojom::HostID& host_id, |
| content::RenderProcessHost& process) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| TRACE_EVENT("extensions", |
| "ScriptInjectionTracker::WillUpdateContentScriptsInRenderer", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(host_id.id)); |
| |
| const Extension* extension = |
| FindExtensionByHostId(process.GetBrowserContext(), host_id); |
| if (!extension) { |
| return; |
| } |
| |
| auto& process_data = RenderProcessHostUserData::GetOrCreate(process); |
| const std::set<content::RenderFrameHost*>& frames_in_process = |
| process_data.frames(); |
| bool any_frame_matches_scripts = base::ranges::any_of( |
| frames_in_process, [extension](content::RenderFrameHost* frame) { |
| auto url = frame->GetLastCommittedURL(); |
| return DoWebViewScripstMatch(*extension, *frame) || |
| DoStaticContentScriptsMatch(*extension, *frame, url) || |
| DoDynamicContentScriptsMatch(*extension, *frame, url) || |
| DoUserScriptsMatch(*extension, *frame, url); |
| }); |
| if (any_frame_matches_scripts) { |
| process_data.AddScript(ScriptType::kContentScript, extension->id()); |
| } else { |
| TRACE_EVENT_INSTANT("extensions", |
| "ScriptInjectionTracker::" |
| "WillUpdateContentScriptsInRenderer - no matches", |
| ChromeTrackEvent::kRenderProcessHost, process, |
| ChromeTrackEvent::kChromeExtensionId, |
| ExtensionIdForTracing(host_id.id)); |
| } |
| } |
| |
| // static |
| bool ScriptInjectionTracker::DoStaticContentScriptsMatchForTesting( |
| const Extension& extension, |
| content::RenderFrameHost* frame, |
| const GURL& url) { |
| return DoStaticContentScriptsMatch(extension, *frame, url); |
| } |
| |
| } // namespace extensions |