| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ssl/https_upgrades_interceptor.h" |
| |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "build/build_config.h" |
| #include "build/buildflag.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/renderer_host/chrome_navigation_ui_data.h" |
| #include "chrome/browser/ssl/https_first_mode_settings_tracker.h" |
| #include "chrome/browser/ssl/https_only_mode_tab_helper.h" |
| #include "chrome/browser/ssl/https_upgrades_util.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/captive_portal/core/buildflags.h" |
| #include "components/content_settings/core/browser/content_settings_registry.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/url_loader_request_interceptor.h" |
| #include "content/public/browser/web_contents.h" |
| #include "extensions/buildflags/buildflags.h" |
| #include "mojo/public/cpp/bindings/callback_helpers.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/system/data_pipe.h" |
| #include "net/base/url_util.h" |
| #include "net/http/http_status_code.h" |
| #include "net/http/http_util.h" |
| #include "net/url_request/redirect_info.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "services/network/public/mojom/url_loader.mojom.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "ui/base/page_transition_types.h" |
| #include "url/gurl.h" |
| #include "url/url_constants.h" |
| |
| #if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION) |
| #include "components/captive_portal/content/captive_portal_tab_helper.h" |
| #endif |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| #include "components/guest_view/browser/guest_view_base.h" |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| using security_interstitials::https_only_mode::RecordHttpsFirstModeNavigation; |
| using security_interstitials::https_only_mode:: |
| RecordNavigationRequestSecurityLevel; |
| |
| namespace { |
| |
| // Used to handle upgrading/fallback for tests using EmbeddedTestServer which |
| // uses random ports. |
| int g_https_port_for_testing = 0; |
| int g_http_port_for_testing = 0; |
| |
| // Updates a URL to HTTPS. URLs with the default port will result in the HTTPS |
| // URL using the default port 443. URLs with non-default ports won't have the |
| // port changed. For tests, the HTTPS port used can be overridden with |
| // HttpsUpgradesInterceptor::SetHttpsPortForTesting(). |
| GURL UpgradeUrlToHttps(const GURL& url) { |
| DCHECK(!url.SchemeIsCryptographic()); |
| |
| // Replace scheme with HTTPS. |
| GURL::Replacements upgrade_url; |
| upgrade_url.SetSchemeStr(url::kHttpsScheme); |
| |
| // For tests that use the EmbeddedTestServer, the server's port needs to be |
| // specified as it can't use the default ports. |
| int https_port_for_testing = |
| HttpsUpgradesInterceptor::GetHttpsPortForTesting(); |
| // `port_str` must be in scope for the call to ReplaceComponents() below. |
| const std::string port_str = base::NumberToString(https_port_for_testing); |
| if (https_port_for_testing) { |
| // Only reached in testing, where the original URL will always have a |
| // non-default port. |
| DCHECK(!url.port().empty()); |
| upgrade_url.SetPortStr(port_str); |
| } |
| |
| return url.ReplaceComponents(upgrade_url); |
| } |
| |
| // Helper to configure an artificial redirect to `new_url`. This configures |
| // `response_head` and returns a computed RedirectInfo so both can be passed to |
| // URLLoaderClient::OnReceiveRedirect() to trigger the redirect. |
| net::RedirectInfo SetupRedirect( |
| const network::ResourceRequest& request, |
| const GURL& new_url, |
| network::mojom::URLResponseHead* response_head) { |
| response_head->encoded_data_length = 0; |
| response_head->request_start = base::TimeTicks::Now(); |
| response_head->response_start = response_head->request_start; |
| std::string header_string = base::StringPrintf( |
| "HTTP/1.1 %i Temporary Redirect\n" |
| "Location: %s\n" |
| "Non-Authoritative-Reason: HttpsUpgrades\n", |
| net::HTTP_TEMPORARY_REDIRECT, new_url.spec().c_str()); |
| response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>( |
| net::HttpUtil::AssembleRawHeaders(header_string)); |
| net::RedirectInfo redirect_info = net::RedirectInfo::ComputeRedirectInfo( |
| request.method, request.url, request.site_for_cookies, |
| request.update_first_party_url_on_redirect |
| ? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT |
| : net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL, |
| request.referrer_policy, request.referrer.spec(), |
| request.request_initiator, net::HTTP_TEMPORARY_REDIRECT, new_url, |
| /*referrer_policy_header=*/std::nullopt, |
| /*insecure_scheme_was_upgraded=*/false); |
| return redirect_info; |
| } |
| |
| // Check whether the HTTP or HTTPS versions of the URL has "Insecure |
| // Content" allowed in content settings. A user can manually specify hosts |
| // or hostname patterns (e.g., [*.]example.com) in site settings. |
| bool DoesInsecureContentSettingDisableUpgrading(const GURL& url, |
| Profile* profile) { |
| // Mixed content isn't an overridable content setting on Android. |
| #if BUILDFLAG(IS_ANDROID) |
| return false; |
| #else |
| HostContentSettingsMap* content_settings = |
| HostContentSettingsMapFactory::GetForProfile(profile); |
| |
| if (!content_settings) { |
| return false; |
| } |
| |
| if (content_settings->GetContentSetting(url, GURL(), |
| ContentSettingsType::MIXEDSCRIPT) == |
| CONTENT_SETTING_ALLOW) { |
| return true; |
| } |
| |
| // Also check for the HTTPS version of the URL -- if an upgraded page is |
| // broken and the user goes through Page Info -> Site Settings and sets |
| // "Insecure Content" to be allowed, this will store a site setting only for |
| // the HTTPS version of the site. |
| GURL https_url = url.SchemeIsCryptographic() ? url : UpgradeUrlToHttps(url); |
| if (content_settings->GetContentSetting(https_url, GURL(), |
| ContentSettingsType::MIXEDSCRIPT) == |
| CONTENT_SETTING_ALLOW) { |
| return true; |
| } |
| return false; |
| #endif |
| } |
| |
| // Check for net errors that should not result in an HTTPS-First Mode |
| // interstitial. These cover cases where the error is most likely related to the |
| // local network conditions or a transient error, where it is more useful to |
| // show the user a network error page than the HTTPS-First Mode interstitial. |
| // (Or, in the case of HTTPS Upgrades, we don't want to fallback and allowlist |
| // the hostname yet -- instead we want to show the network error page and then |
| // retry the HTTPS upgrade again later.) |
| bool IsHttpsFirstModeExemptedError(int error) { |
| return net::IsHostnameResolutionError(error) || |
| error == net::ERR_NETWORK_CHANGED || |
| error == net::ERR_INTERNET_DISCONNECTED || |
| error == net::ERR_ADDRESS_UNREACHABLE; |
| } |
| |
| } // namespace |
| |
| using RequestHandler = HttpsUpgradesInterceptor::RequestHandler; |
| using security_interstitials::https_only_mode::Event; |
| using security_interstitials::https_only_mode::NavigationRequestSecurityLevel; |
| |
| // static |
| std::unique_ptr<HttpsUpgradesInterceptor> |
| HttpsUpgradesInterceptor::MaybeCreateInterceptor( |
| content::FrameTreeNodeId frame_tree_node_id, |
| content::NavigationUIData* navigation_ui_data) { |
| auto* web_contents = |
| content::WebContents::FromFrameTreeNodeId(frame_tree_node_id); |
| // Could be null if the FrameTreeNode's RenderFrameHost is shutting down. |
| if (!web_contents) { |
| return nullptr; |
| } |
| // If there isn't a BrowserContext/Profile for this, then just allow it. |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| if (!profile || |
| !g_browser_process->profile_manager()->IsValidProfile(profile)) { |
| return nullptr; |
| } |
| |
| PrefService* prefs = profile->GetPrefs(); |
| bool https_first_mode_enabled = |
| prefs && prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled); |
| |
| return std::make_unique<HttpsUpgradesInterceptor>( |
| frame_tree_node_id, https_first_mode_enabled, navigation_ui_data); |
| } |
| |
| HttpsUpgradesInterceptor::HttpsUpgradesInterceptor( |
| content::FrameTreeNodeId frame_tree_node_id, |
| bool http_interstitial_enabled_by_pref, |
| content::NavigationUIData* navigation_ui_data) |
| : frame_tree_node_id_(frame_tree_node_id), |
| http_interstitial_enabled_by_pref_(http_interstitial_enabled_by_pref), |
| navigation_ui_data_(navigation_ui_data) {} |
| |
| HttpsUpgradesInterceptor::~HttpsUpgradesInterceptor() = default; |
| |
| bool ShouldExcludeNavigationFromUpgrades( |
| content::NavigationUIData* navigation_ui_data, |
| content::WebContents* contents) { |
| // If the URL was typed with an explicit http:// URL or is captive portal |
| // login URL, it is opted-out from upgrades. |
| ChromeNavigationUIData* chrome_navigation_ui_data = |
| static_cast<ChromeNavigationUIData*>(navigation_ui_data); |
| if (!chrome_navigation_ui_data) { |
| return false; |
| } |
| if (!chrome_navigation_ui_data->force_no_https_upgrade()) { |
| return false; |
| } |
| NavigationRequestSecurityLevel level = |
| NavigationRequestSecurityLevel::kExplicitHttpScheme; |
| #if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION) |
| captive_portal::CaptivePortalTabHelper* captive_portal_tab_helper = |
| captive_portal::CaptivePortalTabHelper::FromWebContents(contents); |
| if (captive_portal_tab_helper->is_captive_portal_tab() || |
| captive_portal_tab_helper->is_captive_portal_window()) { |
| level = NavigationRequestSecurityLevel::kCaptivePortalLogin; |
| } |
| #endif |
| |
| RecordNavigationRequestSecurityLevel(level); |
| return true; |
| } |
| |
| void HttpsUpgradesInterceptor::MaybeCreateLoader( |
| const network::ResourceRequest& tentative_resource_request, |
| content::BrowserContext* browser_context, |
| content::URLLoaderRequestInterceptor::LoaderCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Note: Redirects cause a restarted request with a new call to |
| // MaybeCreateLoader(). |
| |
| // If there isn't a BrowserContext/Profile for this, then just allow it. |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| if (!profile || |
| !g_browser_process->profile_manager()->IsValidProfile(profile)) { |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| auto* web_contents = |
| content::WebContents::FromFrameTreeNodeId(frame_tree_node_id_); |
| // Could be null if the FrameTreeNode's RenderFrameHost is shutting down. |
| if (!web_contents) { |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| #if BUILDFLAG(ENABLE_EXTENSIONS) |
| // If this is a GuestView (e.g., Chrome Apps <webview>) then HTTPS-First Mode |
| // should not apply. See crbug.com/1233889 for more details. |
| if (guest_view::GuestViewBase::IsGuest(frame_tree_node_id_)) { |
| std::move(callback).Run({}); |
| return; |
| } |
| #endif // BUILDFLAG(ENABLE_EXTENSIONS) |
| |
| auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(web_contents); |
| if (!tab_helper) { |
| HttpsOnlyModeTabHelper::CreateForWebContents(web_contents); |
| tab_helper = HttpsOnlyModeTabHelper::FromWebContents(web_contents); |
| } |
| |
| StatefulSSLHostStateDelegate* state = |
| static_cast<StatefulSSLHostStateDelegate*>( |
| profile->GetSSLHostStateDelegate()); |
| auto* storage_partition = |
| web_contents->GetPrimaryMainFrame()->GetStoragePartition(); |
| |
| // Set up the interstitial state before checking any exclusions to upgrades, |
| // as some may depend on this being configured. |
| interstitial_state_ = std::make_unique< |
| security_interstitials::https_only_mode::HttpInterstitialState>(); |
| interstitial_state_->enabled_by_pref = http_interstitial_enabled_by_pref_; |
| auto* prefs = profile->GetPrefs(); |
| if (base::FeatureList::IsEnabled(features::kHttpsFirstModeIncognito)) { |
| if (prefs && prefs->GetBoolean(prefs::kHttpsFirstModeIncognito) && |
| profile->IsIncognitoProfile()) { |
| interstitial_state_->enabled_by_incognito = true; |
| } |
| } |
| // StatefulSSLHostStateDelegate can be null during tests. |
| if (state && |
| state->IsHttpsEnforcedForUrl(tentative_resource_request.url, |
| storage_partition) && |
| !MustDisableSiteEngagementHeuristic(profile)) { |
| interstitial_state_->enabled_by_engagement_heuristic = true; |
| } |
| if (IsBalancedModeEnabled(prefs) && state && |
| !state->HttpsFirstBalancedModeSuppressedForTesting()) { |
| interstitial_state_->enabled_in_balanced_mode = true; |
| } |
| |
| // Exclude HTTPS URLs. |
| if (tentative_resource_request.url.SchemeIs(url::kHttpsScheme)) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kSecure); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // Exclude all other schemes other than HTTP. |
| if (!tentative_resource_request.url.SchemeIs(url::kHttpScheme)) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kOtherScheme); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // Exclude "localhost" (and loopback addresses) as they do not expose traffic |
| // over the network. |
| if (net::IsLocalhost(tentative_resource_request.url)) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kLocalhost); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // For non-strict modes (and Incognito), skip attempting to upgrade non-unique |
| // hostnames as they can't get publicly-trusted certificates. |
| // |
| // HTTPS-First Strict Mode does not exempt these hosts in order to ensure that |
| // Chrome shows the HTTP interstitial before navigation to them. Potentially, |
| // these could fast-fail instead and skip directly to the interstitial. |
| if (net::IsHostnameNonUnique(tentative_resource_request.url.host())) { |
| if (ShouldExemptNonUniqueHostnames(*interstitial_state_)) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kNonUniqueHostname); |
| std::move(callback).Run({}); |
| return; |
| } |
| } |
| |
| // For non-strict modes (and Incognito), skip attempting to upgrade |
| // single-label hostnames. After crrev.com/c/5507613, single-label hostname |
| // are not guaranteed to be considered non-unique, but they are very unlikely |
| // to have publicly-trusted certificates. Similarly to non-unique hostnames, |
| // strict mode does not exempt these in order to ensure that Chrome shows the |
| // HTTP interstitial before navigation to them. |
| if (net::GetSuperdomain(tentative_resource_request.url.host()).empty()) { |
| // Record this as a fallback event so that we don't auto-enable HFM due to |
| // the typically secure user heuristic and start showing interstitials on |
| // it. |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| if (hfm_service) { |
| hfm_service->RecordHttpsUpgradeFallbackEvent(); |
| } |
| |
| if (ShouldExemptNonUniqueHostnames(*interstitial_state_)) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kSingleLabelHostname); |
| std::move(callback).Run({}); |
| return; |
| } |
| } |
| |
| // Captive portals and manually-entered http:// navigations are excluded from |
| // upgrades and we shouldn't warn on them when strict mode isn't enabled, so |
| // allowlist those http:// connections instead. |
| // TODO(crbug.com/363205521): Consider whether we want to allowlist captive |
| // portal hostnames. |
| if (!IsStrictInterstitialEnabled(*interstitial_state_) && |
| ShouldExcludeNavigationFromUpgrades(navigation_ui_data_, web_contents)) { |
| if (state) { |
| state->AllowHttpForHost(tentative_resource_request.url.host(), |
| storage_partition); |
| } |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // Check whether this host would be upgraded to HTTPS by HSTS. This requires a |
| // Mojo call to the network service, so set up a callback to continue the rest |
| // of the MaybeCreateLoader() logic (passing along the necessary state). The |
| // HSTS status will be passed as a boolean to |
| // MaybeCreateLoaderOnHstsQueryCompleted(). If the Mojo call fails, this will |
| // default to passing `false` and continuing as though the host does not have |
| // HSTS (i.e., it will proceed with the HTTPS-First Mode logic). |
| // TODO(crbug.com/40248833): Consider caching this result, at least within the |
| // same navigation. |
| auto query_complete_callback = base::BindOnce( |
| &HttpsUpgradesInterceptor::MaybeCreateLoaderOnHstsQueryCompleted, |
| weak_factory_.GetWeakPtr(), tentative_resource_request, |
| std::move(callback)); |
| network::mojom::NetworkContext* network_context = |
| profile->GetDefaultStoragePartition()->GetNetworkContext(); |
| |
| CHECK(tentative_resource_request.trusted_params); |
| network_context->IsHSTSActiveForHost( |
| tentative_resource_request.url.host(), |
| tentative_resource_request.trusted_params->isolation_info |
| .IsOutermostMainFrameRequest(), |
| mojo::WrapCallbackWithDefaultInvokeIfNotRun( |
| std::move(query_complete_callback), |
| /*is_hsts_active_for_host=*/false)); |
| } |
| |
| void HttpsUpgradesInterceptor::MaybeCreateLoaderOnHstsQueryCompleted( |
| const network::ResourceRequest& tentative_resource_request, |
| content::URLLoaderRequestInterceptor::LoaderCallback callback, |
| bool is_hsts_active_for_host) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Reconstruct objects here instead of binding them as parameters to this |
| // callback method. |
| // |
| // It's possible for the WebContents to be destroyed during the |
| // asynchronous HSTS query call, before this callback is run. If it no longer |
| // exists, don't upgrade and return. (See crbug.com/1499515.) |
| content::WebContents* web_contents = |
| content::WebContents::FromFrameTreeNodeId(frame_tree_node_id_); |
| if (!web_contents) { |
| std::move(callback).Run({}); |
| return; |
| } |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| HttpsOnlyModeTabHelper* tab_helper = |
| HttpsOnlyModeTabHelper::FromWebContents(web_contents); |
| CHECK(profile); |
| CHECK(tab_helper); |
| |
| // Don't upgrade this request if HSTS is active for this host. |
| if (is_hsts_active_for_host) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kHstsUpgraded); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // Only serve upgrade redirects for main frame, GET requests. |
| if (!tentative_resource_request.is_outermost_main_frame || |
| tentative_resource_request.method != "GET") { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kInsecure); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // For non-strict modes, skip attempting to upgrade URLs with non-default |
| // ports, as these are unlikely to succeed (the server needs to support HTTP |
| // and HTTPS on the same port, or the URL needs to be incorrectly have an |
| // HTTP scheme but with the server's HTTPS port). |
| // |
| // For non-default ports, we explicitly don't allowlist the hostname, as that |
| // would prevent upgrades/warnings on the entire hostname (even if the user |
| // later tried to visit using default ports). This also prevents this from |
| // being useful for downgrade attacks (e.g., a malicious site first calling |
| // `window.open("http://example.com:5678")` before calling |
| // `window.open("http://example.com")` will still result in the second |
| // navigation getting upgraded/warning-on-fallback). |
| // |
| // For testing, treat the "HTTP port for testing" (if set) as the default. |
| // |
| // TODO(crbug.com/349860796): If this check is placed in `MaybeCreateLoader()` |
| // above (before the async HSTS check), a few tests fail. Once the underlying |
| // test issues are determined we can freely re-order this exemption check. |
| if (tentative_resource_request.url.has_port() && |
| tentative_resource_request.url.IntPort() != GetHttpPortForTesting()) { |
| // Record this as a fallback event so that we don't auto-enable HFM due to |
| // the typically secure user heuristic and start showing interstitials on |
| // it. |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| if (hfm_service) { |
| hfm_service->RecordHttpsUpgradeFallbackEvent(); |
| } |
| |
| // Strict is true for HFM+SE if it applies to the hostname. |
| if (!IsStrictInterstitialEnabled(*interstitial_state_)) { |
| // All feature variations should record the navigation metric. |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kNonDefaultPorts); |
| std::move(callback).Run({}); |
| return; |
| } |
| } |
| |
| // Don't upgrade navigation if it is allowlisted. |
| // First, check the enterprise policy HTTP allowlist. |
| PrefService* prefs = profile->GetPrefs(); |
| if (IsHostnameInHttpAllowlist(tentative_resource_request.url, |
| profile->GetPrefs())) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kAllowlisted); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // Check if the origin is specified in the |
| // `--unsafely-treat-insecure-origin-as-secure` command-line flag or |
| // `OverrideSecurityRestrictionsOnInsecureOrigin` policy. |
| if (network::SecureOriginAllowlist::GetInstance().IsOriginAllowlisted( |
| url::Origin::Create(tentative_resource_request.url))) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kAllowlisted); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // Next check whether the HTTP or HTTPS versions of the URL has "Insecure |
| // Content" allowed in content settings. We treat this as a sign to not do |
| // silent HTTPS Upgrades for the site overall and not show an HTTPS-First Mode |
| // interstitial for Engaged Sites. Strict HTTPS-First Mode ignores this |
| // setting. |
| if (!interstitial_state_->enabled_by_pref && |
| DoesInsecureContentSettingDisableUpgrading(tentative_resource_request.url, |
| profile)) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kAllowlisted); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // Then check whether the host has been allowlisted by the user (or by a |
| // previous upgrade attempt failing). |
| // TODO(crbug.com/40248833): Distinguish HTTPS-First Mode and HTTPS-Upgrades |
| // allowlist entries. |
| // TODO(crbug.com/40248833): Move this to a helper function `IsAllowlisted()`, |
| // especially once this gets more complicated for HFM vs. Upgrades. |
| StatefulSSLHostStateDelegate* state = |
| static_cast<StatefulSSLHostStateDelegate*>( |
| profile->GetSSLHostStateDelegate()); |
| // StatefulSSLHostStateDelegate can be null during tests. |
| auto* storage_partition = |
| web_contents->GetPrimaryMainFrame()->GetStoragePartition(); |
| if (state && state->IsHttpAllowedForHost( |
| tentative_resource_request.url.host(), storage_partition)) { |
| // Renew the allowlist expiration for this host as the user is still |
| // actively using it. This means that the allowlist entry will stay |
| // valid until the user stops visiting this host for the entire |
| // expiration period (one week). |
| state->AllowHttpForHost(tentative_resource_request.url.host(), |
| storage_partition); |
| |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kInsecure); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // If this is a back/forward navigation to a failed upgrade, then don't |
| // intercept to upgrade the navigation. Other forms of re-visiting a URL |
| // that previously failed to be upgraded to HTTPS *should* be intercepted so |
| // the upgrade can be attempted again (e.g., the user reloading the tab, the |
| // user navigating around and ending back on this URL in the same tab, etc.). |
| // |
| // This effectively "caches" the HTTPS-First Mode interstitial for the |
| // history entry of a failed upgrade for the lifetime of the tab. This means |
| // that it is possible for a user to come back much later (say, a week later), |
| // after a site has fixed its HTTPS configuration, and still see the |
| // interstitial for that URL. |
| // |
| // Without this check, resetting the HTTPS-Upgrades flags in |
| // HttpsOnlyModeTabHelper::DidStartNavigation() means the Interceptor would |
| // fire on back/forward navigation to the interstitial, which causes an |
| // "extra" interstitial entry to be added to the history list and lose other |
| // entries. |
| auto* entry = web_contents->GetController().GetPendingEntry(); |
| if (entry && entry->GetTransitionType() & ui::PAGE_TRANSITION_FORWARD_BACK && |
| tab_helper->has_failed_upgrade(tentative_resource_request.url)) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kInsecure); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // The `HttpsUpgradesEnabled` enterprise policy can be set to `false` to |
| // disable HTTPS-Upgrades entirely. Abort if HFM is disabled and the |
| // enterprise policy is set. |
| if (!prefs->GetBoolean(prefs::kHttpsUpgradesEnabled) && |
| !IsInterstitialEnabled(*interstitial_state_)) { |
| RecordHttpsFirstModeNavigation(Event::kUpgradeNotAttempted, |
| *interstitial_state_); |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kAllowlisted); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| // Both HTTPS-First Mode and HTTPS-Upgrades are forms of upgrading all HTTP |
| // navigations to HTTPS, with HTTPS-First Mode additionally enabling the |
| // HTTP interstitial on fallback. |
| if (!base::FeatureList::IsEnabled(features::kHttpsUpgrades) && |
| !IsInterstitialEnabled(*interstitial_state_)) { |
| // Don't upgrade the request and let the default loader continue, but record |
| // that the request *would have* upgraded, had upgrading been enabled. |
| RecordHttpsFirstModeNavigation(Event::kUpgradeNotAttempted, |
| *interstitial_state_); |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kInsecure); |
| std::move(callback).Run({}); |
| return; |
| } |
| |
| if (state && |
| state->IsHttpsEnforcedForUrl(tentative_resource_request.url, |
| storage_partition) && |
| !MustDisableSiteEngagementHeuristic(profile)) { |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kHttpsEnforcedOnHostname); |
| } |
| |
| // If the request URL is in the set of URLs that HttpsUpgradesInterceptor has |
| // already processed, skip upgrading and trigger fallback to HTTP to avoid a |
| // redirect loop. |
| if (base::Contains(urls_seen_, tentative_resource_request.url)) { |
| // Record failure type metrics for upgraded navigations. |
| RecordHttpsFirstModeNavigation(Event::kUpgradeFailed, *interstitial_state_); |
| RecordHttpsFirstModeNavigation(Event::kUpgradeRedirectLoop, |
| *interstitial_state_); |
| |
| // If HTTPS-First Mode is not enabled (so no interstitial will be shown), |
| // add the fallback hostname to the allowlist now before triggering |
| // fallback. HTTPS-First Mode handles this on the user proceeding through |
| // the interstitial only. |
| // TODO(crbug.com/40912859): Distinguish HTTPS-First Mode and HTTPS-Upgrades |
| // allowlist entries, and ensure that HTTPS-Upgrades allowlist entries don't |
| // downgrade Page Info. |
| // TODO(crbug.com/40248833): Move this to a helper function |
| // `AddUrlToAllowlist()`, especially once this gets more complicated for |
| // HFM vs. Upgrades. |
| if (!IsInterstitialEnabled(*interstitial_state_)) { |
| // StatefulSSLHostStateDelegate can be null during tests. |
| if (state) { |
| state->AllowHttpForHost( |
| tab_helper->fallback_url().host(), |
| web_contents->GetPrimaryMainFrame()->GetStoragePartition()); |
| } |
| // Also record this fallback event so that we can auto-enable HFM based on |
| // browsing patterns later on. |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| // HttpsFirstModeService can be null in tests. |
| if (hfm_service) { |
| hfm_service->RecordHttpsUpgradeFallbackEvent(); |
| } |
| } |
| |
| tab_helper->set_is_navigation_upgraded(false); |
| tab_helper->set_is_navigation_fallback(true); |
| tab_helper->add_failed_upgrade(tab_helper->fallback_url()); |
| |
| // Note: If `fallback_url` is the same as the request URL, this |
| // could skip doing an additional redirect, but then the NavigationThrottle |
| // doesn't have the ability to act on this navigation request and apply |
| // the HTTPS-First Mode interstitial. If we add a better way to "fast fail" |
| // navigations directly to the interstitial, then we could probably use that |
| // here as well as an optimization. |
| std::move(callback).Run(CreateRedirectHandler(tab_helper->fallback_url())); |
| return; |
| } |
| // Not a redirect loop. Add the current request URL to the set of URLs seen. |
| urls_seen_.insert(tentative_resource_request.url); |
| |
| RecordNavigationRequestSecurityLevel( |
| NavigationRequestSecurityLevel::kUpgraded); |
| |
| // Mark navigation as upgraded. |
| tab_helper->set_is_navigation_upgraded(true); |
| tab_helper->set_fallback_url(tentative_resource_request.url); |
| |
| GURL https_url = UpgradeUrlToHttps(tentative_resource_request.url); |
| std::move(callback).Run(CreateRedirectHandler(https_url)); |
| } |
| |
| bool HttpsUpgradesInterceptor::MaybeCreateLoaderForResponse( |
| const network::URLLoaderCompletionStatus& status, |
| const network::ResourceRequest& request, |
| network::mojom::URLResponseHeadPtr* response_head, |
| mojo::ScopedDataPipeConsumerHandle* response_body, |
| mojo::PendingRemote<network::mojom::URLLoader>* loader, |
| mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver, |
| blink::ThrottlingURLLoader* url_loader) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // When an upgraded navigation fails, this method creates a loader to trigger |
| // the fallback to HTTP. |
| // |
| // Note: MaybeCreateLoaderForResponse() is called for all navigation |
| // responses and failures, but not for things like a NavigationThrottle |
| // cancelling or blocking the navigation. |
| |
| // Only intercept if the navigation failed. |
| if (status.error_code == net::OK) { |
| return false; |
| } |
| |
| auto* web_contents = |
| content::WebContents::FromFrameTreeNodeId(frame_tree_node_id_); |
| if (!web_contents) { |
| // `web_contents` can be null if the tab is being closed. Skip handling |
| // failure in that case since the page is going away anyway. |
| return false; |
| } |
| |
| auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(web_contents); |
| if (!tab_helper || !tab_helper->is_navigation_upgraded()) { |
| return false; |
| } |
| |
| // interstitial_state_ is only created after the initial checks in |
| // MaybeCreateLoader(). If it wasn't created, the load wasn't eligible for |
| // upgrades, so ignore the load here as well. Also explicitly ignore non-main |
| // frame loads because we don't want to trigger a fallback navigation in |
| // non-main frames. |
| // This is a fix for crbug.com/1441276. |
| if (!interstitial_state_ || !request.is_outermost_main_frame) { |
| return false; |
| } |
| |
| // If the navigation resulted in an exempted transient network error, don't |
| // intercept the failure and allow the normal net error page to trigger. Set |
| // the exempt-error flag so if the net error page is reloaded we can try |
| // continuing with the upgrade (and fallback to the original URL if the |
| // transient network error goes away and the navigation fails). |
| // Currently this is only done if the HTTPS-First Mode interstitial is |
| // enabled, because otherwise these would cause the interstitial to be shown |
| // which is confusing. HTTPS-Upgrades will silently fall back to HTTP for |
| // these errors. |
| // |
| // However, if this is a request to a non-unique hostname, don't prefer |
| // net error as it is likely non-recoverable -- we want to fallback to HTTP |
| // and the HTTPS-First Mode interstitial in this case. (In particular, this |
| // avoids breaking corporate single-label hostnames.) Treat all single-label |
| // hostnames as if they were non-unique, since while unique single-label hosts |
| // (i.e. TLDs on the PSL) could get a cert, it's more likely they're being |
| // used as a corporate hostname. These domains are safe to exclude since this |
| // only results in potentially show an extra HFM warning before the net error. |
| if (IsInterstitialEnabled(*interstitial_state_) && |
| IsHttpsFirstModeExemptedError(status.error_code) && |
| !net::IsHostnameNonUnique(request.url.host()) && |
| !net::GetSuperdomain(request.url.host()).empty()) { |
| tab_helper->set_is_exempt_error(true); |
| return false; |
| } |
| |
| Profile* profile = |
| Profile::FromBrowserContext(web_contents->GetBrowserContext()); |
| StatefulSSLHostStateDelegate* state = |
| static_cast<StatefulSSLHostStateDelegate*>( |
| profile->GetSSLHostStateDelegate()); |
| |
| // Record failure type metrics for upgraded navigations. |
| RecordHttpsFirstModeNavigation(Event::kUpgradeFailed, *interstitial_state_); |
| if (net::IsCertificateError(status.error_code)) { |
| RecordHttpsFirstModeNavigation(Event::kUpgradeCertError, |
| *interstitial_state_); |
| } else if (status.error_code == net::ERR_TIMED_OUT) { |
| RecordHttpsFirstModeNavigation(Event::kUpgradeTimedOut, |
| *interstitial_state_); |
| } else { |
| RecordHttpsFirstModeNavigation(Event::kUpgradeNetError, |
| *interstitial_state_); |
| } |
| |
| // If no interstitial will be shown, add the fallback hostname to the |
| // allowlist now before triggering fallback. The interstitial handles this on |
| // the user proceeding through the interstitial only. |
| if (!IsInterstitialEnabled(*interstitial_state_)) { |
| // StatefulSSLHostStateDelegate can be null during tests. |
| if (state) { |
| state->AllowHttpForHost( |
| tab_helper->fallback_url().host(), |
| web_contents->GetPrimaryMainFrame()->GetStoragePartition()); |
| } |
| |
| // Also record this fallback event so that we can auto-enable HFM based on |
| // browsing patterns later on. |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| // HttpsFirstModeService can be null in tests. |
| if (hfm_service) { |
| hfm_service->RecordHttpsUpgradeFallbackEvent(); |
| } |
| } |
| |
| tab_helper->set_is_navigation_upgraded(false); |
| tab_helper->set_is_navigation_fallback(true); |
| tab_helper->add_failed_upgrade(tab_helper->fallback_url()); |
| |
| // `client_` may have been previously bound from handling the initial upgrade |
| // in MaybeCreateLoader(), so reset it before re-binding it to handle this |
| // response. |
| client_.reset(); |
| *client_receiver = client_.BindNewPipeAndPassReceiver(); |
| |
| // Create an artificial redirect back to the fallback URL. |
| auto new_response_head = network::mojom::URLResponseHead::New(); |
| net::RedirectInfo redirect_info = SetupRedirect( |
| request, tab_helper->fallback_url(), new_response_head.get()); |
| |
| client_->OnReceiveRedirect(redirect_info, std::move(new_response_head)); |
| return true; |
| } |
| |
| // static |
| void HttpsUpgradesInterceptor::SetHttpsPortForTesting(int port) { |
| g_https_port_for_testing = port; |
| } |
| |
| // static |
| void HttpsUpgradesInterceptor::SetHttpPortForTesting(int port) { |
| g_http_port_for_testing = port; |
| } |
| |
| // static |
| int HttpsUpgradesInterceptor::GetHttpsPortForTesting() { |
| return g_https_port_for_testing; |
| } |
| |
| // static |
| int HttpsUpgradesInterceptor::GetHttpPortForTesting() { |
| return g_http_port_for_testing; |
| } |
| |
| RequestHandler HttpsUpgradesInterceptor::CreateRedirectHandler( |
| const GURL& new_url) { |
| return base::BindOnce(&HttpsUpgradesInterceptor::RedirectHandler, |
| weak_factory_.GetWeakPtr(), new_url); |
| } |
| |
| void HttpsUpgradesInterceptor::RedirectHandler( |
| const GURL& new_url, |
| const network::ResourceRequest& request, |
| mojo::PendingReceiver<network::mojom::URLLoader> receiver, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Set up Mojo connection and initiate the redirect. `client_` and `receiver_` |
| // may have been previously bound from handling a previous upgrade earlier in |
| // the same navigation, so reset them before re-binding them to handle a new |
| // redirect. |
| receiver_.reset(); |
| client_.reset(); |
| receiver_.Bind(std::move(receiver)); |
| receiver_.set_disconnect_handler( |
| base::BindOnce(&HttpsUpgradesInterceptor::OnConnectionClosed, |
| weak_factory_.GetWeakPtr())); |
| client_.Bind(std::move(client)); |
| |
| // Create redirect. |
| auto response_head = network::mojom::URLResponseHead::New(); |
| net::RedirectInfo redirect_info = |
| SetupRedirect(request, new_url, response_head.get()); |
| |
| client_->OnReceiveRedirect(redirect_info, std::move(response_head)); |
| } |
| |
| void HttpsUpgradesInterceptor::OnConnectionClosed() { |
| // This happens when content cancels the navigation. Reset the receiver and |
| // client handle. |
| receiver_.reset(); |
| client_.reset(); |
| } |