blob: 3131d78498b23422cd100edd7a3b9e88080c3926 [file] [log] [blame]
// Copyright 2012 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/api/web_request/web_request_permissions.h"
#include <string_view>
#include "base/debug/crash_logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_util.h"
#include "build/chromeos_buildflags.h"
#include "content/public/browser/child_process_security_policy.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/api/web_request/permission_helper.h"
#include "extensions/browser/api/web_request/web_request_api_constants.h"
#include "extensions/browser/api/web_request/web_request_info.h"
#include "extensions/browser/extension_navigation_ui_data.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/process_map.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/manifest_handlers/incognito_info.h"
#include "extensions/common/permissions/permissions_data.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
#include "third_party/abseil-cpp/absl/strings/ascii.h"
#include "third_party/blink/public/common/loader/resource_type_util.h"
#include "url/gurl.h"
#include "url/origin.h"
static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));
using extensions::PermissionsData;
namespace {
// Returns true if the scheme is one we want to allow extensions to have access
// to. Extensions still need specific permissions for a given URL, which is
// covered by CanExtensionAccessURL.
// TODO(karandeepb): This allows more schemes than
// ExtensionWebRequestEventRouter::RequestFiler, which specifies the schemes
// allowed by web request event listeners. Consolidate the two.
bool HasWebRequestScheme(const GURL& url) {
return (url.SchemeIs(url::kAboutScheme) || url.SchemeIs(url::kFileScheme) ||
url.SchemeIs(url::kFileSystemScheme) ||
url.SchemeIs(url::kFtpScheme) || url.SchemeIsHTTPOrHTTPS() ||
url.SchemeIs(extensions::kExtensionScheme) || url.SchemeIsWSOrWSS() ||
url.SchemeIs(url::kUuidInPackageScheme));
}
PermissionsData::PageAccess GetHostAccessForURL(
const extensions::Extension& extension,
const GURL& url,
int tab_id) {
// about: URLs are not covered in host permissions, but are allowed
// anyway.
if (url.SchemeIs(url::kAboutScheme) ||
url::IsSameOriginWith(url, extension.url())) {
return PermissionsData::PageAccess::kAllowed;
}
return extension.permissions_data()->GetPageAccess(url, tab_id,
nullptr /*error*/);
}
bool IsWebRequestResourceTypeFrame(
extensions::WebRequestResourceType web_request_type) {
return web_request_type == extensions::WebRequestResourceType::MAIN_FRAME ||
web_request_type == extensions::WebRequestResourceType::SUB_FRAME;
}
PermissionsData::PageAccess CanExtensionAccessURLInternal(
extensions::PermissionHelper* permission_helper,
const extensions::ExtensionId& extension_id,
const GURL& url,
int tab_id,
bool crosses_incognito,
WebRequestPermissions::HostPermissionsCheck host_permissions_check,
const std::optional<url::Origin>& initiator,
const std::optional<extensions::WebRequestResourceType>& web_request_type) {
const extensions::Extension* extension =
permission_helper->extension_registry()->enabled_extensions().GetByID(
extension_id);
if (!extension) {
return PermissionsData::PageAccess::kDenied;
}
// Prevent viewing / modifying requests initiated by a host protected by
// policy.
if (initiator &&
extension->permissions_data()->IsPolicyBlockedHost(initiator->GetURL())) {
return PermissionsData::PageAccess::kDenied;
}
// Check if this event crosses incognito boundaries when it shouldn't.
if (crosses_incognito && !permission_helper->CanCrossIncognito(extension)) {
return PermissionsData::PageAccess::kDenied;
}
switch (host_permissions_check) {
case WebRequestPermissions::DO_NOT_CHECK_HOST:
return PermissionsData::PageAccess::kAllowed;
case WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL: {
PermissionsData::PageAccess access =
GetHostAccessForURL(*extension, url, tab_id);
bool is_navigation_request =
web_request_type && IsWebRequestResourceTypeFrame(*web_request_type);
// For sub-resource (non-navigation) requests, if access to the host was
// withheld, check if the extension has access to the initiator. If it
// does, allow the extension to see the request. This is important for
// extensions with webRequest to work well with runtime host permissions.
if (!is_navigation_request &&
access == PermissionsData::PageAccess::kWithheld) {
PermissionsData::PageAccess initiator_access =
initiator
? GetHostAccessForURL(*extension, initiator->GetURL(), tab_id)
: PermissionsData::PageAccess::kDenied;
if (initiator_access == PermissionsData::PageAccess::kAllowed) {
access = PermissionsData::PageAccess::kAllowed;
}
}
return access;
}
case WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL_AND_INITIATOR: {
PermissionsData::PageAccess request_access =
GetHostAccessForURL(*extension, url, tab_id);
bool is_navigation_request =
web_request_type && IsWebRequestResourceTypeFrame(*web_request_type);
// Only require access to the initiator for sub-resource (non-navigation)
// requests. See crbug.com/918137.
// TODO(karandeepb): Should service worker navigation preload requests be
// treated similarly?
if (is_navigation_request) {
return request_access;
}
if (request_access == PermissionsData::PageAccess::kDenied) {
return request_access;
}
if (!initiator || initiator->opaque()) {
return request_access;
}
DCHECK(request_access == PermissionsData::PageAccess::kWithheld ||
request_access == PermissionsData::PageAccess::kAllowed);
// Possible remaining states:
// ----------------------------------------------------
// | Initiator access| Request access| Expected access|
// ----------------------------------------------------
// | Withheld | Withheld | Withheld |
// | Withheld | Allowed | Withheld |
// | Allowed | Withheld | Allowed |
// | Allowed | Allowed | Allowed |
// | Denied | * | Denied |
// ----------------------------------------------------
// Note: The only interesting case is when the access to a sub-resource
// request is withheld but the access to initiator is allowed. In this
// case, we allow access to the request. This is important for extensions
// with webRequest to work well with runtime host permissions. See
// crbug.com/851722.
return GetHostAccessForURL(*extension, initiator->GetURL(), tab_id);
}
case WebRequestPermissions::REQUIRE_ALL_URLS:
return extension->permissions_data()
->active_permissions()
.HasEffectiveAccessToAllHosts()
? PermissionsData::PageAccess::kAllowed
: PermissionsData::PageAccess::kDenied;
}
NOTREACHED();
}
// Returns true if |request|.url is of the form clients[0-9]*.google.com.
bool IsSensitiveGoogleClientUrl(const extensions::WebRequestInfo& request) {
const GURL& url = request.url;
// TODO(battre) Merge this, CanExtensionAccessURL and
// PermissionsData::CanAccessPage into one function.
static constexpr char kGoogleCom[] = "google.com";
static constexpr char kClient[] = "clients";
constexpr size_t kGoogleComLength = std::size(kGoogleCom) - 1;
constexpr size_t kClientLength = std::size(kClient) - 1;
if (!url.DomainIs(kGoogleCom)) {
return false;
}
std::string_view host = url.host_piece();
while (base::EndsWith(host, ".")) {
host.remove_suffix(1u);
}
// Check for "clients[0-9]*.google.com" hosts.
// This protects requests to several internal services such as sync,
// extension update pings, captive portal detection, fraudulent certificate
// reporting, autofill and others.
//
// These URLs are only protected for requests from the browser, and not for
// requests from common renderers, because clients*.google.com are also used
// by websites.
std::string_view::size_type pos = host.rfind(kClient);
if (pos == std::string_view::npos) {
return false;
}
if (pos > 0 && host[pos - 1] != '.') {
return false;
}
for (std::string_view::const_iterator
i = host.begin() + pos + kClientLength,
end = host.end() - (kGoogleComLength + 1);
i != end; ++i) {
if (!absl::ascii_isdigit(static_cast<unsigned char>(*i))) {
return false;
}
}
return true;
}
bool IsMainFrameNavigationRequest(const extensions::WebRequestInfo& request) {
return request.is_navigation_request &&
request.web_request_type ==
extensions::WebRequestResourceType::MAIN_FRAME;
}
} // namespace
// static
bool WebRequestPermissions::HideRequest(
extensions::PermissionHelper* permission_helper,
const extensions::WebRequestInfo& request) {
if (!HasWebRequestScheme(request.url)) {
return true;
}
// Requests from <webview> are never hidden.
if (request.is_web_view) {
return false;
}
bool is_request_from_browser = request.render_process_id == -1;
if (is_request_from_browser) {
// Browser initiated service worker script requests (e.g., for update check)
// are not hidden.
if (request.is_service_worker_script) {
DCHECK(request.web_request_type ==
extensions::WebRequestResourceType::SCRIPT);
return false;
}
// Hide all non-navigation requests made by the browser. crbug.com/884932.
if (!request.is_navigation_request) {
return true;
}
DCHECK(request.web_request_type ==
extensions::WebRequestResourceType::MAIN_FRAME ||
request.web_request_type ==
extensions::WebRequestResourceType::SUB_FRAME ||
request.web_request_type ==
extensions::WebRequestResourceType::OBJECT);
// Hide sub-frame requests to clientsX.google.com.
// TODO(crbug.com/40595750): Determine if the code here can be cleaned up
// since browser initiated non-navigation requests are now hidden from
// extensions.
if (request.web_request_type !=
extensions::WebRequestResourceType::MAIN_FRAME &&
IsSensitiveGoogleClientUrl(request)) {
return true;
}
}
// Hide requests from the Chrome WebStore App.
if (!is_request_from_browser &&
permission_helper->process_map()->Contains(extensions::kWebStoreAppId,
request.render_process_id)) {
return true;
}
// Hide requests initiated by the new Webstore domain, including requests that
// may be on subframes which have an opaque origin.
if (request.initiator) {
const auto& request_tuple_or_precursor_tuple =
request.initiator->GetTupleOrPrecursorTupleIfOpaque();
if (request_tuple_or_precursor_tuple ==
url::SchemeHostPort(extension_urls::GetNewWebstoreLaunchURL())) {
return true;
}
}
const GURL& url = request.url;
bool is_request_from_webui_renderer =
!is_request_from_browser &&
content::ChildProcessSecurityPolicy::GetInstance()->HasWebUIBindings(
request.render_process_id);
if (is_request_from_webui_renderer) {
#if DCHECK_IS_ON()
const bool is_network_request =
url.SchemeIsHTTPOrHTTPS() || url.SchemeIsWSOrWSS();
if (is_network_request) {
// WebUI renderers should never be making network requests, but we may
// make some exceptions for now. See https://crbug.com/829412 for
// details.
//
// The DCHECK helps avoid proliferation of such behavior.
DCHECK(request.initiator.has_value());
DCHECK(extensions::ExtensionsBrowserClient::Get()
->IsWebUIAllowedToMakeNetworkRequests(*request.initiator))
<< "Unsupported network request from "
<< request.initiator->GetURL().spec() << " for " << url.spec();
}
#endif // DCHECK_IS_ON()
// In any case, we treat the requests as sensitive to ensure that the Web
// Request API doesn't see them.
return true;
}
// Requests from chrome-untrusted:// are generally sensitive (because they
// are considered part of browser UI).
//
// Main frame navigations from chrome-untrusted:// to non-WebUI origins are an
// exception: These requests are inspectable by the Web Request API (e.g. by a
// content filtering extension) and therefore allowlisted.
if (request.initiator.has_value() &&
request.initiator->scheme() == content::kChromeUIUntrustedScheme) {
// The call to `HasWebRequestScheme()` with an early exit at the top already
// ensures that request.url does not point to a chrome-untrusted:// URL.
// Therefore, it's not necessary to check the scheme of request.url again.
bool allowlist = IsMainFrameNavigationRequest(request);
if (!allowlist) {
return true;
}
}
// Allow the extension embedder to hide the request.
if (permission_helper->ShouldHideBrowserNetworkRequest(request)) {
return true;
}
// Safebrowsing and Chrome Webstore URLs are always protected, i.e. also
// for requests from common renderers.
// TODO(crbug.com/40235977): it would be nice to be able to just use
// extension_urls::IsWebstoreDomain for the last two checks here, but the old
// webstore check specifically requires the path to be checked, not just the
// domain. However once the old webstore is turned down we can change it over
// during that cleanup.
if (extension_urls::IsWebstoreUpdateUrl(url) ||
extension_urls::IsWebstoreApiUrl(url) ||
extension_urls::IsBlocklistUpdateUrl(url) ||
extension_urls::IsSafeBrowsingUrl(url) ||
(url.DomainIs("chrome.google.com") &&
base::StartsWith(url.path_piece(), "/webstore",
base::CompareCase::SENSITIVE)) ||
url.DomainIs(extension_urls::GetNewWebstoreLaunchURL().GetHost())) {
return true;
}
return false;
}
// static
PermissionsData::PageAccess WebRequestPermissions::CanExtensionAccessURL(
extensions::PermissionHelper* permission_helper,
const extensions::ExtensionId& extension_id,
const GURL& url,
int tab_id,
bool crosses_incognito,
HostPermissionsCheck host_permissions_check,
const std::optional<url::Origin>& initiator,
extensions::WebRequestResourceType web_request_type) {
return CanExtensionAccessURLInternal(
permission_helper, extension_id, url, tab_id, crosses_incognito,
host_permissions_check, initiator, web_request_type);
}
// static
bool WebRequestPermissions::CanExtensionAccessInitiator(
extensions::PermissionHelper* permission_helper,
const extensions::ExtensionId extension_id,
const std::optional<url::Origin>& initiator,
int tab_id,
bool crosses_incognito) {
if (!initiator) {
return true;
}
return CanExtensionAccessURLInternal(
permission_helper, extension_id, initiator->GetURL(), tab_id,
crosses_incognito,
WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL,
std::nullopt /* initiator */, std::nullopt /* resource_type */) ==
PermissionsData::PageAccess::kAllowed;
}