| // Copyright (c) 2012 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/api/web_request/web_request_permissions.h" |
| |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "content/public/browser/child_process_security_policy.h" |
| #include "content/public/browser/resource_request_info.h" |
| #include "extensions/browser/api/extensions_api_client.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/extensions_browser_client.h" |
| #include "extensions/browser/info_map.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_urls.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "url/gurl.h" |
| |
| #if defined(OS_CHROMEOS) |
| #include "chromeos/login/login_state.h" |
| #endif // defined(OS_CHROMEOS) |
| |
| using content::ResourceRequestInfo; |
| using extensions::PermissionsData; |
| |
| namespace { |
| |
| // Describes the different cases pertaining to permissions check for the |
| // initiator. |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class InitiatorAccess { |
| kAbsent = 0, |
| kOpaque = 1, |
| kNoAccess = 2, |
| kHasAccess = 3, |
| kMaxValue = kHasAccess, |
| }; |
| |
| void LogInitiatorAccess(InitiatorAccess access) { |
| UMA_HISTOGRAM_ENUMERATION("Extensions.WebRequest.InitiatorAccess2", access); |
| } |
| |
| // 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()); |
| } |
| |
| bool g_allow_all_extension_locations_in_public_session = false; |
| |
| 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*/); |
| } |
| |
| PermissionsData::PageAccess CanExtensionAccessURLInternal( |
| const extensions::InfoMap* extension_info_map, |
| const std::string& extension_id, |
| const GURL& url, |
| int tab_id, |
| bool crosses_incognito, |
| WebRequestPermissions::HostPermissionsCheck host_permissions_check, |
| const base::Optional<url::Origin>& initiator) { |
| // extension_info_map can be NULL in testing. |
| if (!extension_info_map) |
| return PermissionsData::PageAccess::kAllowed; |
| |
| const extensions::Extension* extension = |
| extension_info_map->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; |
| |
| // When restrictions are enabled in Public Session, allow all URLs for |
| // webRequests initiated by a regular extension (but don't allow chrome:// |
| // URLs). |
| #if defined(OS_CHROMEOS) |
| if (chromeos::LoginState::IsInitialized() && |
| chromeos::LoginState::Get()->ArePublicSessionRestrictionsEnabled() && |
| extension->is_extension() && !url.SchemeIs("chrome")) { |
| // Make sure that the extension is truly installed by policy (the assumption |
| // in Public Session is that all extensions are installed by policy). |
| CHECK(g_allow_all_extension_locations_in_public_session || |
| extensions::Manifest::IsPolicyLocation(extension->location())); |
| return PermissionsData::PageAccess::kAllowed; |
| } |
| #endif |
| |
| // Check if this event crosses incognito boundaries when it shouldn't. |
| if (crosses_incognito && !extension_info_map->CanCrossIncognito(extension)) |
| return PermissionsData::PageAccess::kDenied; |
| |
| PermissionsData::PageAccess access = PermissionsData::PageAccess::kDenied; |
| switch (host_permissions_check) { |
| case WebRequestPermissions::DO_NOT_CHECK_HOST: |
| access = PermissionsData::PageAccess::kAllowed; |
| break; |
| case WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL: |
| access = GetHostAccessForURL(*extension, url, tab_id); |
| // 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 (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; |
| } |
| break; |
| case WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL_AND_INITIATOR: { |
| PermissionsData::PageAccess request_access = |
| GetHostAccessForURL(*extension, url, tab_id); |
| if (request_access == PermissionsData::PageAccess::kDenied) |
| return request_access; |
| |
| // For cases, where an extension has (allowed or withheld) access to the |
| // request url, log if it has access to the request initiator. |
| if (!initiator) { |
| LogInitiatorAccess(InitiatorAccess::kAbsent); |
| return request_access; |
| } |
| |
| if (initiator->opaque()) { |
| LogInitiatorAccess(InitiatorAccess::kOpaque); |
| 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 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. |
| |
| PermissionsData::PageAccess initiator_access = |
| GetHostAccessForURL(*extension, initiator->GetURL(), tab_id); |
| LogInitiatorAccess(initiator_access == |
| PermissionsData::PageAccess::kDenied |
| ? InitiatorAccess::kNoAccess |
| : InitiatorAccess::kHasAccess); |
| return initiator_access; |
| break; |
| } |
| case WebRequestPermissions::REQUIRE_ALL_URLS: |
| if (extension->permissions_data()->HasEffectiveAccessToAllHosts()) |
| access = PermissionsData::PageAccess::kAllowed; |
| // else ACCESS_DENIED |
| break; |
| } |
| |
| return access; |
| } |
| |
| // 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 = base::size(kGoogleCom) - 1; |
| constexpr size_t kClientLength = base::size(kClient) - 1; |
| |
| if (!url.DomainIs(kGoogleCom)) |
| return false; |
| |
| base::StringPiece host = url.host_piece(); |
| |
| while (host.ends_with(".")) |
| 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( |
| const extensions::InfoMap* extension_info_map, |
| const extensions::WebRequestInfo& request) { |
| if (!HasWebRequestScheme(request.url)) |
| return true; |
| |
| // Requests from <webview> are never hidden. |
| if (request.is_web_view) |
| return false; |
| |
| // Requests from PAC scripts are always hidden. |
| // See https://crbug.com/794674 |
| if (request.is_pac_request) |
| return true; |
| |
| bool is_request_from_browser = request.render_process_id == -1; |
| |
| if (is_request_from_browser) { |
| // Hide all non-navigation requests made by the browser. crbug.com/884932. |
| if (!request.is_browser_side_navigation) |
| return true; |
| |
| DCHECK(request.type == content::RESOURCE_TYPE_MAIN_FRAME || |
| request.type == content::RESOURCE_TYPE_SUB_FRAME); |
| |
| // 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.type != content::RESOURCE_TYPE_MAIN_FRAME && |
| IsSensitiveGoogleClientUrl(request)) { |
| return true; |
| } |
| } |
| |
| // Hide requests from the Chrome WebStore App. |
| if (!is_request_from_browser && extension_info_map && |
| extension_info_map->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; |
| } |
| |
| // Allow the extension embedder to hide the request. |
| if (extensions::ExtensionsAPIClient::Get()->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::IsBlacklistUpdateUrl(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 |
| void WebRequestPermissions:: |
| AllowAllExtensionLocationsInPublicSessionForTesting(bool value) { |
| g_allow_all_extension_locations_in_public_session = value; |
| } |
| |
| // static |
| PermissionsData::PageAccess WebRequestPermissions::CanExtensionAccessURL( |
| const extensions::InfoMap* extension_info_map, |
| const std::string& extension_id, |
| const GURL& url, |
| int tab_id, |
| bool crosses_incognito, |
| HostPermissionsCheck host_permissions_check, |
| const base::Optional<url::Origin>& initiator) { |
| return CanExtensionAccessURLInternal(extension_info_map, extension_id, url, |
| tab_id, crosses_incognito, |
| host_permissions_check, initiator); |
| } |
| |
| // static |
| bool WebRequestPermissions::CanExtensionAccessInitiator( |
| const extensions::InfoMap* extension_info_map, |
| const extensions::ExtensionId extension_id, |
| const base::Optional<url::Origin>& initiator, |
| int tab_id, |
| bool crosses_incognito) { |
| if (!initiator) |
| return true; |
| |
| return CanExtensionAccessURLInternal( |
| extension_info_map, extension_id, initiator->GetURL(), tab_id, |
| crosses_incognito, |
| WebRequestPermissions::REQUIRE_HOST_PERMISSION_FOR_URL, |
| base::nullopt) == PermissionsData::PageAccess::kAllowed; |
| } |