blob: 3ae34e1d1fc791a74a2430076b4ff2ab7607a1e5 [file] [log] [blame]
// 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