blob: 1e6219101ca16ed2337698883d43e05227030218 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/content_script_tracker.h"
#include <algorithm>
#include <map>
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/memory/ptr_util.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/extension_registry.h"
#include "extensions/browser/url_loader_factory_manager.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_handlers/content_scripts_handler.h"
#include "extensions/common/user_script.h"
namespace extensions {
namespace {
// Crrently we track content script injection per-RenderFrameHost (using
// GlobalFrameRoutingId as the key of the map below).
// RenderDocumentHostUserData is not used because of a race between content
// script injection, ReadyToCommit and DidCommit (see the
// ContentScriptTrackerBrowserTest.ProgrammaticInjectionRacingWithDidCommit test
// for more details).
//
// TODO(lukasza): Once RenderDocumentHost project ships, we should switch to
// per-document tracking.
using FrameToExtensionIdSet =
std::map<content::GlobalFrameRoutingId, ExtensionIdSet>;
FrameToExtensionIdSet& GetFrameToExtensionIdSet() {
static base::NoDestructor<FrameToExtensionIdSet> frame_to_extension_id_set;
return *frame_to_extension_id_set;
}
ExtensionIdSet& GetOrCreateExtensionIdSet(content::RenderFrameHost* frame) {
FrameToExtensionIdSet& frame_to_extension_id_set = GetFrameToExtensionIdSet();
return frame_to_extension_id_set[frame->GetGlobalFrameRoutingId()];
}
// If `match_about_blank` is true, then traverses parent/opener chain until the
// first non-about-scheme document and returns its url. Otherwise, simply
// returns `document_url`.
//
// This function approximates
// ScriptContext::GetEffectiveDocumentURLForInjection() from the renderer side.
// Unlike the renderer code, this just iterates up frame tree, and doesn't look
// at the effective or precursor origin of the frame. This is okay, because our
// only caller (DoesContentScriptMatch()) expects false positives.
GURL GetEffectiveDocumentURL(content::RenderFrameHost* frame,
const GURL& document_url,
bool match_about_blank) {
base::flat_set<content::RenderFrameHost*> already_visited_frames;
// Common scenario. If `match_about_blank` is false (as is the case in most
// extensions), or if the frame is not an about:-page, just return
// `document_url` (supposedly the URL of the frame).
if (!match_about_blank || !document_url.SchemeIs(url::kAboutScheme))
return document_url;
// Non-sandboxed about:blank and about:srcdoc pages inherit their security
// origin from their parent frame/window. So, traverse the frame/window
// hierarchy to find the closest non-about:-page and return its URL.
//
// TODO(https://crbug.com/1186321): The assumption above is incorrect -
// about:blank frames inherit their origin from the initiator of the
// navigation (which might not be the parent and/or the opener).
content::RenderFrameHost* found_frame = frame;
do {
DCHECK(found_frame);
already_visited_frames.insert(found_frame);
// The loop should only execute (and consider the parent chain) if the
// currently considered frame has about: scheme.
DCHECK(match_about_blank);
DCHECK(
((found_frame == frame) && document_url.SchemeIs(url::kAboutScheme)) ||
(found_frame->GetLastCommittedURL().SchemeIs(url::kAboutScheme)));
// Attempt to find `next_candidate` - either a parent of opener of
// `found_frame`.
content::RenderFrameHost* next_candidate = found_frame->GetParent();
if (!next_candidate) {
next_candidate =
content::WebContents::FromRenderFrameHost(found_frame)->GetOpener();
}
if (!next_candidate ||
base::Contains(already_visited_frames, next_candidate)) {
break;
}
found_frame = next_candidate;
} while (found_frame->GetLastCommittedURL().SchemeIs(url::kAboutScheme));
if (found_frame == frame)
return document_url; // Not committed yet at ReadyToCommitNavigation time.
return found_frame->GetLastCommittedURL();
}
// If `user_script` will inject JavaScript content script into the target of
// `navigation`, then DoesContentScriptMatch returns true. Otherwise it may
// return either true or false. 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 or the results of ScriptInjector::CanExecuteOnFrame.
// Additionally the `effective_url` calculations are also only an approximation.
// This is okay, because the top-level doc comment for ContentScriptTracker
// documents that false positives are expected and why they are okay.
bool DoesContentScriptMatch(const UserScript& user_script,
content::RenderFrameHost* frame,
const GURL& url) {
// ContentScriptTracker only needs to track Javascript content scripts (e.g.
// doesn't track CSS-only injections).
if (user_script.js_scripts().empty())
return false;
// TODO(devlin): Update GetEffectiveDocumentURL() to take a
// MatchOriginAsFallbackBehavior.
bool match_about_blank = false;
switch (user_script.match_origin_as_fallback()) {
case MatchOriginAsFallbackBehavior::kAlways:
case MatchOriginAsFallbackBehavior::kMatchForAboutSchemeAndClimbTree:
match_about_blank = true;
break;
case MatchOriginAsFallbackBehavior::kNever:
break; // `false` is correct for `match_about_blank`.
}
GURL effective_url = GetEffectiveDocumentURL(frame, url, match_about_blank);
bool is_subframe = frame->GetParent();
return user_script.MatchesDocument(effective_url, is_subframe);
}
void HandleProgrammaticContentScriptInjection(
base::PassKey<ContentScriptTracker> pass_key,
content::RenderFrameHost* frame,
const Extension& extension) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
ExtensionIdSet& content_scripts_for_frame = GetOrCreateExtensionIdSet(frame);
content_scripts_for_frame.insert(extension.id());
URLLoaderFactoryManager::WillProgrammaticallyInjectContentScript(
pass_key, frame, extension);
}
// If `extension`'s manifest declares that it may inject JavaScript content
// script into the `frame` / `url`, then DoContentScriptsMatch returns true.
// Otherwise it may return either true or false.
//
// Note that the `url` might be either 1) the last committed URL of `frame` or
// 2) the target of a ReadyToCommit navigation in `frame`.
//
// Note that this method ignores CSS content scripts.
bool DoContentScriptsMatch(const Extension& extension,
content::RenderFrameHost* frame,
const GURL& url) {
const UserScriptList& list =
ContentScriptsInfo::GetContentScripts(&extension);
return std::any_of(list.begin(), list.end(),
[frame, &url](const std::unique_ptr<UserScript>& script) {
return DoesContentScriptMatch(*script, frame, url);
});
}
std::vector<const Extension*> GetExtensionsInjectingContentScripts(
content::NavigationHandle* navigation) {
content::RenderFrameHost* frame = navigation->GetRenderFrameHost();
const GURL& url = navigation->GetURL();
std::vector<const Extension*> extensions_injecting_content_scripts;
const ExtensionRegistry* registry =
ExtensionRegistry::Get(frame->GetProcess()->GetBrowserContext());
DCHECK(registry); // This method shouldn't be called during shutdown.
for (const auto& it : registry->enabled_extensions()) {
const Extension& extension = *it;
if (!DoContentScriptsMatch(extension, frame, url))
continue;
extensions_injecting_content_scripts.push_back(&extension);
}
return extensions_injecting_content_scripts;
}
} // namespace
// static
bool ContentScriptTracker::DidFrameRunContentScriptFromExtension(
content::RenderFrameHost* frame,
const ExtensionId& extension_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!frame)
return false;
// Check the last committed URL - this is needed for URLs (like "about:blank")
// that might not go through ReadyToCommit. (Today "about:blank" should still
// go through DidCommit, but there are tentative ideas/plans to avoid this and
// therefore ContentScriptTracker looks at the last committed URL rather than
// monitoring DidCommit IPCs.)
const GURL& url = frame->GetLastCommittedURL();
if (url.SchemeIs(url::kAboutScheme)) {
const ExtensionRegistry* registry =
ExtensionRegistry::Get(frame->GetBrowserContext());
DCHECK(registry); // This method shouldn't be called during shutdown.
const Extension* extension =
registry->enabled_extensions().GetByID(extension_id);
if (extension && DoContentScriptsMatch(*extension, frame, url))
return true;
}
// Check if we've been notified about the content script injection via
// ReadyToCommitNavigation or WillExecuteCode methods.
FrameToExtensionIdSet& frame_to_extension_id_set = GetFrameToExtensionIdSet();
auto it = frame_to_extension_id_set.find(frame->GetGlobalFrameRoutingId());
if (it == frame_to_extension_id_set.end())
return false;
const ExtensionIdSet& extension_id_set = it->second;
return base::Contains(extension_id_set, extension_id);
}
// static
void ContentScriptTracker::ReadyToCommitNavigation(
base::PassKey<ExtensionWebContentsObserver> pass_key,
content::NavigationHandle* navigation) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::vector<const Extension*> extensions_injecting_content_scripts =
GetExtensionsInjectingContentScripts(navigation);
ExtensionIdSet& content_scripts_for_frame =
GetOrCreateExtensionIdSet(navigation->GetRenderFrameHost());
for (const Extension* extension : extensions_injecting_content_scripts)
content_scripts_for_frame.insert(extension->id());
URLLoaderFactoryManager::WillInjectContentScriptsWhenNavigationCommits(
base::PassKey<ContentScriptTracker>(), navigation,
extensions_injecting_content_scripts);
}
// static
void ContentScriptTracker::RenderFrameDeleted(
base::PassKey<ExtensionWebContentsObserver> pass_key,
content::RenderFrameHost* frame) {
FrameToExtensionIdSet& frame_to_extension_id_set = GetFrameToExtensionIdSet();
frame_to_extension_id_set.erase(frame->GetGlobalFrameRoutingId());
}
// static
void ContentScriptTracker::WillExecuteCode(
base::PassKey<ScriptExecutor> pass_key,
content::RenderFrameHost* frame,
const mojom::HostID& host_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
switch (host_id.type) {
case mojom::HostID::HostType::kWebUi:
// This class only tracks extensions.
return;
case mojom::HostID::HostType::kExtensions:
break;
}
const ExtensionRegistry* registry =
ExtensionRegistry::Get(frame->GetProcess()->GetBrowserContext());
DCHECK(registry); // WillExecuteCode shouldn't happen during shutdown.
const Extension* extension =
registry->enabled_extensions().GetByID(host_id.id);
DCHECK(extension); // Guaranteed by the caller - see the doc comment.
HandleProgrammaticContentScriptInjection(PassKey(), frame, *extension);
}
// static
void ContentScriptTracker::WillExecuteCode(
base::PassKey<RequestContentScript> pass_key,
content::RenderFrameHost* frame,
const Extension& extension) {
HandleProgrammaticContentScriptInjection(PassKey(), frame, extension);
}
// static
bool ContentScriptTracker::DoContentScriptsMatchForTesting(
const Extension& extension,
content::RenderFrameHost* frame,
const GURL& url) {
return DoContentScriptsMatch(extension, frame, url);
}
} // namespace extensions