blob: b0a5d8ac739996798592aca1b2332322a8f1d630 [file] [log] [blame]
// Copyright 2018 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/url_loader_factory_manager.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/hash/sha1.h"
#include "base/metrics/field_trial_params.h"
#include "base/no_destructor.h"
#include "base/stl_util.h"
#include "base/strings/string_split.h"
#include "content/public/browser/browser_context.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/process_map.h"
#include "extensions/common/constants.h"
#include "extensions/common/cors_util.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/manifest_handlers/content_scripts_handler.h"
#include "extensions/common/script_constants.h"
#include "extensions/common/switches.h"
#include "extensions/common/user_script.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/scheme_host_port.h"
#include "url/url_constants.h"
namespace extensions {
namespace {
enum class FactoryUser {
kContentScript,
kExtensionProcess,
};
constexpr size_t kHashedExtensionIdLength = base::kSHA1Length * 2;
bool IsValidHashedExtensionId(const std::string& hash) {
bool correct_chars = std::all_of(hash.begin(), hash.end(), [](char c) {
return ('A' <= c && c <= 'F') || ('0' <= c && c <= '9');
});
bool correct_length = (kHashedExtensionIdLength == hash.length());
return correct_chars && correct_length;
}
std::vector<std::string> CreateExtensionAllowlist() {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kForceEmptyCorbAllowlist)) {
return std::vector<std::string>();
}
// Append extensions from the field trial param.
std::string field_trial_arg = base::GetFieldTrialParamValueByFeature(
extensions_features::kCorbCorsAllowlist,
extensions_features::kCorbCorsAllowlistParamName);
field_trial_arg = base::ToUpperASCII(field_trial_arg);
std::vector<std::string> field_trial_allowlist = base::SplitString(
field_trial_arg, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
base::EraseIf(field_trial_allowlist, [](const std::string& hash) {
// Filter out invalid data from |field_trial_allowlist|.
if (IsValidHashedExtensionId(hash))
return false; // Don't remove.
LOG(ERROR) << "Invalid extension hash: " << hash;
return true; // Remove.
});
return field_trial_allowlist;
}
// Returns a set of HashedExtensionId of extensions that depend on relaxed CORB
// or CORS behavior in their content scripts.
base::flat_set<std::string>& GetExtensionsAllowlist() {
static base::NoDestructor<base::flat_set<std::string>> s_allowlist([] {
base::flat_set<std::string> result(CreateExtensionAllowlist());
result.shrink_to_fit();
return result;
}());
return *s_allowlist;
}
bool DoContentScriptsDependOnRelaxedCorbOrCors(const Extension& extension) {
// Content scripts injected by Chrome Apps (e.g. into <webview> tag) need to
// run with relaxed CORB.
if (extension.is_platform_app())
return true;
// Content scripts in manifest v2 might be allowlisted to depend on relaxed
// CORB and/or CORS.
if (extension.manifest_version() <= 2) {
const std::string& hash = extension.hashed_id().value();
DCHECK(IsValidHashedExtensionId(hash));
return base::Contains(GetExtensionsAllowlist(), hash);
}
// Safe fallback for future extension manifest versions.
return false;
}
bool DoExtensionPermissionsCoverHttpOrHttpsOrigins(const Extension& extension) {
// TODO(lukasza): https://crbug.com/1016904: Return false if the |extension|'s
// permissions do not actually cover http or https origins. For now we
// conservatively return true so that *all* extensions get relaxed CORS/CORB
// treatment.
return true;
}
// Returns whether the default URLLoaderFactoryParams::is_corb_enabled should be
// overridden and changed to false.
bool ShouldDisableCorb(const Extension& extension, FactoryUser factory_user) {
if (!DoExtensionPermissionsCoverHttpOrHttpsOrigins(extension))
return false;
switch (factory_user) {
case FactoryUser::kContentScript:
return DoContentScriptsDependOnRelaxedCorbOrCors(extension);
case FactoryUser::kExtensionProcess:
return true;
}
}
// Returns whether URLLoaderFactoryParams::ignore_isolated_world_origin should
// be overridden and changed to false.
bool ShouldInspectIsolatedWorldOrigin(const Extension& extension,
FactoryUser factory_user) {
if (!DoExtensionPermissionsCoverHttpOrHttpsOrigins(extension))
return false;
switch (factory_user) {
case FactoryUser::kContentScript:
return DoContentScriptsDependOnRelaxedCorbOrCors(extension);
case FactoryUser::kExtensionProcess:
return false;
}
}
bool ShouldCreateSeparateFactoryForContentScripts(const Extension& extension) {
return ShouldDisableCorb(extension, FactoryUser::kContentScript) ||
ShouldInspectIsolatedWorldOrigin(extension,
FactoryUser::kContentScript);
}
void OverrideFactoryParams(const Extension& extension,
FactoryUser factory_user,
network::mojom::URLLoaderFactoryParams* params) {
if (ShouldDisableCorb(extension, factory_user)) {
// TODO(lukasza): https://crbug.com/1016904: Use more granular CORB
// enforcement based on the specific |extension|'s permissions reflected
// in the |factory_bound_access_patterns| above.
params->is_corb_enabled = false;
// Setup factory bound allow list that overwrites per-profile common list to
// allow tab specific permissions only for this newly created factory.
//
// TODO(lukasza): Setting |factory_bound_access_patterns| together with
// |is_corb_enabled| seems accidental.
params->factory_bound_access_patterns =
network::mojom::CorsOriginAccessPatterns::New();
params->factory_bound_access_patterns->source_origin =
url::Origin::Create(extension.url());
params->factory_bound_access_patterns->allow_patterns =
CreateCorsOriginAccessAllowList(
extension,
PermissionsData::EffectiveHostPermissionsMode::kIncludeTabSpecific);
params->factory_bound_access_patterns->block_patterns =
CreateCorsOriginAccessBlockList(extension);
}
if (ShouldInspectIsolatedWorldOrigin(extension, factory_user))
params->ignore_isolated_world_origin = false;
// TODO(lukasza): Do not override |unsafe_non_webby_initiator| unless
// DoExtensionPermissionsCoverHttpOrHttpsOrigins(extension).
if (factory_user == FactoryUser::kExtensionProcess)
params->unsafe_non_webby_initiator = true;
}
void MarkIsolatedWorldsAsRequiringSeparateURLLoaderFactory(
content::RenderFrameHost* frame,
std::vector<url::Origin> request_initiators,
bool push_to_renderer_now) {
DCHECK(!request_initiators.empty());
frame->MarkIsolatedWorldsAsRequiringSeparateURLLoaderFactory(
std::move(request_initiators), push_to_renderer_now);
}
// 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 (DoesContentScriptMatchNavigatingFrame()) 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.
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 DoesContentScriptMatchNavigatingFrame 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 we may return either true even if no content scripts
// would be injected (i.e. it is okay to create a special URLLoaderFactory when
// in reality the content script won't be injected and won't need the factory).
bool DoesContentScriptMatchNavigatingFrame(
const UserScript& user_script,
content::RenderFrameHost* navigating_frame,
const GURL& navigation_target) {
// A special URLLoaderFactory is only needed for Javascript content scripts
// (and is never needed for 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(
navigating_frame, navigation_target, match_about_blank);
bool is_subframe = navigating_frame->GetParent();
return user_script.MatchesDocument(effective_url, is_subframe);
}
} // namespace
// static
bool URLLoaderFactoryManager::DoContentScriptsMatchNavigatingFrame(
const Extension& extension,
content::RenderFrameHost* navigating_frame,
const GURL& navigation_target) {
const UserScriptList& list =
ContentScriptsInfo::GetContentScripts(&extension);
return std::any_of(list.begin(), list.end(),
[navigating_frame, navigation_target](
const std::unique_ptr<UserScript>& script) {
return DoesContentScriptMatchNavigatingFrame(
*script, navigating_frame, navigation_target);
});
}
// static
void URLLoaderFactoryManager::ReadyToCommitNavigation(
content::NavigationHandle* navigation) {
content::RenderFrameHost* frame = navigation->GetRenderFrameHost();
const GURL& url = navigation->GetURL();
std::vector<url::Origin> initiators_requiring_separate_factory;
const ExtensionRegistry* registry =
ExtensionRegistry::Get(frame->GetProcess()->GetBrowserContext());
DCHECK(registry); // ReadyToCommitNavigation shouldn't run during shutdown.
for (const auto& it : registry->enabled_extensions()) {
const Extension& extension = *it;
if (!DoContentScriptsMatchNavigatingFrame(extension, frame, url))
continue;
if (!ShouldCreateSeparateFactoryForContentScripts(extension))
continue;
initiators_requiring_separate_factory.push_back(
url::Origin::Create(extension.url()));
}
if (!initiators_requiring_separate_factory.empty()) {
// At ReadyToCommitNavigation time there is no need to trigger an explicit
// push of URLLoaderFactoryBundle to the renderer - it is sufficient if the
// factories are pushed slightly later - during the commit.
constexpr bool kPushToRendererNow = false;
MarkIsolatedWorldsAsRequiringSeparateURLLoaderFactory(
frame, std::move(initiators_requiring_separate_factory),
kPushToRendererNow);
}
}
// static
void URLLoaderFactoryManager::WillExecuteCode(content::RenderFrameHost* frame,
const HostID& host_id) {
if (host_id.type() != HostID::EXTENSIONS)
return;
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.
if (!ShouldCreateSeparateFactoryForContentScripts(*extension))
return;
// When WillExecuteCode runs, the frame already received the initial
// URLLoaderFactoryBundle - therefore we need to request a separate push
// below. This doesn't race with the ExtensionMsg_ExecuteCode message,
// because the URLLoaderFactoryBundle is sent to the renderer over
// content.mojom.FrameNavigationControl interface which is associated with the
// legacy IPC pipe (raciness will be introduced if that ever changes).
constexpr bool kPushToRendererNow = true;
MarkIsolatedWorldsAsRequiringSeparateURLLoaderFactory(
frame, {url::Origin::Create(extension->url())}, kPushToRendererNow);
}
// static
void URLLoaderFactoryManager::OverrideURLLoaderFactoryParams(
content::BrowserContext* browser_context,
const url::Origin& origin,
bool is_for_isolated_world,
network::mojom::URLLoaderFactoryParams* factory_params) {
const ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context);
DCHECK(registry); // CreateFactory shouldn't happen during shutdown.
// Opaque origins normally don't inherit security properties of their
// precursor origins, but here opaque origins (e.g. think data: URIs) created
// by an extension should inherit CORS/CORB treatment of the extension.
url::SchemeHostPort precursor_origin =
origin.GetTupleOrPrecursorTupleIfOpaque();
// Don't change factory params for something that is not an extension.
if (precursor_origin.scheme() != kExtensionScheme)
return;
// Find the |extension| associated with |initiator_origin|.
const Extension* extension =
registry->enabled_extensions().GetByID(precursor_origin.host());
if (!extension) {
// This may happen if an extension gets disabled between the time
// RenderFrameHost::MarkIsolatedWorldAsRequiringSeparateURLLoaderFactory is
// called and the time
// ContentBrowserClient::OverrideURLLoaderFactoryParams is called.
return;
}
// Identify and set |factory_params| that need to be overridden.
FactoryUser factory_user = is_for_isolated_world
? FactoryUser::kContentScript
: FactoryUser::kExtensionProcess;
OverrideFactoryParams(*extension, factory_user, factory_params);
}
} // namespace extensions