| // 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 "base/debug/crash_logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_piece.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/common/constants.h" |
| #include "extensions/common/extension.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/blink/public/common/loader/resource_type_util.h" |
| #include "url/gurl.h" |
| |
| 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 std::string& extension_id, |
| const GURL& url, |
| int tab_id, |
| bool crosses_incognito, |
| WebRequestPermissions::HostPermissionsCheck host_permissions_check, |
| const absl::optional<url::Origin>& initiator, |
| const absl::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()->HasEffectiveAccessToAllHosts() |
| ? PermissionsData::PageAccess::kAllowed |
| : PermissionsData::PageAccess::kDenied; |
| } |
| |
| NOTREACHED(); |
| return PermissionsData::PageAccess::kDenied; |
| } |
| |
| // 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; |
| |
| base::StringPiece 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. |
| base::StringPiece::size_type pos = host.rfind(kClient); |
| if (pos == base::StringPiece::npos) |
| return false; |
| |
| if (pos > 0 && host[pos - 1] != '.') |
| return false; |
| |
| for (base::StringPiece::const_iterator |
| i = host.begin() + pos + kClientLength, |
| end = host.end() - (kGoogleComLength + 1); |
| i != end; ++i) { |
| if (!isdigit(*i)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // 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/890006): 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; |
| } |
| |
| 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; |
| } |
| |
| // Treat requests from chrome-untrusted:// as sensitive to ensure that the |
| // Web Request API doesn't see them. Note that Extensions are never allowed to |
| // request permission for chrome-untrusted:// URLs so this is check is here |
| // just in case. |
| if (request.initiator.has_value() && |
| request.initiator->scheme() == content::kChromeUIUntrustedScheme) { |
| 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. |
| if (extension_urls::IsWebstoreUpdateUrl(url) || |
| extension_urls::IsBlocklistUpdateUrl(url) || |
| extension_urls::IsSafeBrowsingUrl(url::Origin::Create(url), |
| url.path_piece()) || |
| (url.DomainIs("chrome.google.com") && |
| base::StartsWith(url.path_piece(), "/webstore", |
| base::CompareCase::SENSITIVE))) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // static |
| PermissionsData::PageAccess WebRequestPermissions::CanExtensionAccessURL( |
| extensions::PermissionHelper* permission_helper, |
| const std::string& extension_id, |
| const GURL& url, |
| int tab_id, |
| bool crosses_incognito, |
| HostPermissionsCheck host_permissions_check, |
| const absl::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 absl::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, |
| absl::nullopt /* initiator */, |
| absl::nullopt /* resource_type */) == |
| PermissionsData::PageAccess::kAllowed; |
| } |