From f5cf5631e5c6ed4531c07adf6dcdb373edc65e5f Mon Sep 17 00:00:00 2001 From: Chris Thompson Date: Sat, 17 Dec 2022 01:32:30 +0000 Subject: [PATCH] Initial version of HTTPS-First Mode V2 This adds a rearchitected version of HTTPS-First Mode to address some edge cases that occurred in the initial version, behind a new kHttpsFirstModeV2 flag. The new version cancels the upgraded inavigation when it fails (or times out) and initiates a new fallback HTTP navigation. That fallback navigation is then replaced with the HTTPS-First Mode interstitial warning. This addresses a number of issues such as showing the HTTPS URL when the interstitial is showing (crbug.com/1257272), sometimes losing history entries when going back/forward from an HTTPS-First Mode interstitial (crbug.com/crbug.com/1272781), and will allow follow-up work to better handle interactions with network errors (crbug.com/1277211) among others. This rearchitecture will also allow us to more easily add the new HTTPS Upgrades feature (https://chromestatus.com/feature/6056181032812544). Bug: 1394910,1257272,1272781,1277211 Change-Id: I787ecff185ab78099c04b458441c302cc254eb06 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4114422 Reviewed-by: Mustafa Emre Acer Commit-Queue: Chris Thompson Cr-Commit-Position: refs/heads/main@{#1084642} --- chrome/browser/BUILD.gn | 4 + chrome/browser/about_flags.cc | 4 + .../browser/chrome_content_browser_client.cc | 25 +- chrome/browser/flag-metadata.json | 8 + chrome/browser/flag_descriptions.cc | 4 + chrome/browser/flag_descriptions.h | 3 + .../ssl/https_only_mode_browsertest.cc | 2 + .../ssl/https_only_mode_controller_client.cc | 12 +- .../browser/ssl/https_upgrades_browsertest.cc | 1044 +++++++++++++++++ .../browser/ssl/https_upgrades_interceptor.cc | 223 ++++ .../browser/ssl/https_upgrades_interceptor.h | 75 ++ .../ssl/https_upgrades_navigation_throttle.cc | 223 ++++ .../ssl/https_upgrades_navigation_throttle.h | 59 + chrome/common/chrome_features.cc | 5 + chrome/common/chrome_features.h | 1 + chrome/test/BUILD.gn | 1 + tools/metrics/histograms/enums.xml | 2 + 17 files changed, 1688 insertions(+), 7 deletions(-) create mode 100644 chrome/browser/ssl/https_upgrades_browsertest.cc create mode 100644 chrome/browser/ssl/https_upgrades_interceptor.cc create mode 100644 chrome/browser/ssl/https_upgrades_interceptor.h create mode 100644 chrome/browser/ssl/https_upgrades_navigation_throttle.cc create mode 100644 chrome/browser/ssl/https_upgrades_navigation_throttle.h diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn index 9b13c0906f7de..9c1c2f9d27e62 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn @@ -1709,6 +1709,10 @@ static_library("browser") { "ssl/https_only_mode_upgrade_interceptor.h", "ssl/https_only_mode_upgrade_url_loader.cc", "ssl/https_only_mode_upgrade_url_loader.h", + "ssl/https_upgrades_interceptor.cc", + "ssl/https_upgrades_interceptor.h", + "ssl/https_upgrades_navigation_throttle.cc", + "ssl/https_upgrades_navigation_throttle.h", "ssl/insecure_form/insecure_form_controller_client.cc", "ssl/insecure_form/insecure_form_controller_client.h", "ssl/known_interception_disclosure_infobar_delegate.cc", diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc index b2a549459fb40..c2173f8d1590f 100644 --- a/chrome/browser/about_flags.cc +++ b/chrome/browser/about_flags.cc @@ -8367,6 +8367,10 @@ const FeatureEntry kFeatureEntries[] = { flag_descriptions::kHttpsOnlyModeDescription, kOsDesktop | kOsAndroid, FEATURE_VALUE_TYPE(features::kHttpsOnlyMode)}, + {"https-first-mode-v2", flag_descriptions::kHttpsFirstModeV2Name, + flag_descriptions::kHttpsFirstModeV2Description, kOsDesktop | kOsAndroid, + FEATURE_VALUE_TYPE(features::kHttpsFirstModeV2)}, + {"https-upgrades", flag_descriptions::kHttpsUpgradesName, flag_descriptions::kHttpsUpgradesDescription, kOsDesktop | kOsAndroid, FEATURE_VALUE_TYPE(features::kHttpsUpgrades)}, diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc index 3b0d49489c4f1..6cc71c96e9ab4 100644 --- a/chrome/browser/chrome_content_browser_client.cc +++ b/chrome/browser/chrome_content_browser_client.cc @@ -137,6 +137,8 @@ #include "chrome/browser/ssl/https_defaulted_callbacks.h" #include "chrome/browser/ssl/https_only_mode_navigation_throttle.h" #include "chrome/browser/ssl/https_only_mode_upgrade_interceptor.h" +#include "chrome/browser/ssl/https_upgrades_interceptor.h" +#include "chrome/browser/ssl/https_upgrades_navigation_throttle.h" #include "chrome/browser/ssl/sct_reporting_service.h" #include "chrome/browser/ssl/ssl_client_auth_metrics.h" #include "chrome/browser/ssl/ssl_client_certificate_selector.h" @@ -5059,11 +5061,19 @@ ChromeContentBrowserClient::CreateThrottlesForNavigation( #endif if (profile && profile->GetPrefs()) { - MaybeAddThrottle( - HttpsOnlyModeNavigationThrottle::MaybeCreateThrottleFor( - handle, std::make_unique(), - profile->GetPrefs()), - &throttles); + if (base::FeatureList::IsEnabled(features::kHttpsFirstModeV2)) { + MaybeAddThrottle( + HttpsUpgradesNavigationThrottle::MaybeCreateThrottleFor( + handle, std::make_unique(), + profile->GetPrefs()), + &throttles); + } else { + MaybeAddThrottle( + HttpsOnlyModeNavigationThrottle::MaybeCreateThrottleFor( + handle, std::make_unique(), + profile->GetPrefs()), + &throttles); + } } MaybeAddThrottle(MaybeCreateNavigationAblationThrottle(handle), &throttles); @@ -5892,7 +5902,10 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors( interceptors.push_back( std::make_unique(frame_tree_node_id)); - if (base::FeatureList::IsEnabled(features::kHttpsOnlyMode)) { + if (base::FeatureList::IsEnabled(features::kHttpsFirstModeV2)) { + interceptors.push_back( + std::make_unique(frame_tree_node_id)); + } else { interceptors.push_back( std::make_unique(frame_tree_node_id)); } diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json index 3baaa92b51ca9..c50353a531df7 100644 --- a/chrome/browser/flag-metadata.json +++ b/chrome/browser/flag-metadata.json @@ -4120,6 +4120,14 @@ "owners": [ "shivanisha" ], "expiry_milestone": 95 }, + { + "name": "https-first-mode-v2", + "owners": [ + "cthomp", + "trusty-transport@chromium.org" + ], + "expiry_milestone": 118 + }, { "name": "https-only-mode", "owners": [ "meacer", "trusty-transport@chromium.org" ], diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc index 47358b81439e2..93c3a796ca958 100644 --- a/chrome/browser/flag_descriptions.cc +++ b/chrome/browser/flag_descriptions.cc @@ -1666,6 +1666,10 @@ const char kHttpsOnlyModeDescription[] = "Adds a setting under chrome://settings/security to opt-in to HTTPS-First " "Mode."; +const char kHttpsFirstModeV2Name[] = "HTTPS-First Mode V2"; +const char kHttpsFirstModeV2Description[] = + "Enable rearchitected version of HTTPS-First Mode."; + const char kHttpsUpgradesName[] = "HTTPS Upgrades"; const char kHttpsUpgradesDescription[] = "Enable automatically upgrading all top-level navigations to HTTPS with " diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h index c71e3020e8043..9d28117341ffa 100644 --- a/chrome/browser/flag_descriptions.h +++ b/chrome/browser/flag_descriptions.h @@ -928,6 +928,9 @@ extern const char kHideShelfControlsInTabletModeDescription[]; extern const char kHttpsOnlyModeName[]; extern const char kHttpsOnlyModeDescription[]; +extern const char kHttpsFirstModeV2Name[]; +extern const char kHttpsFirstModeV2Description[]; + extern const char kHttpsUpgradesName[]; extern const char kHttpsUpgradesDescription[]; diff --git a/chrome/browser/ssl/https_only_mode_browsertest.cc b/chrome/browser/ssl/https_only_mode_browsertest.cc index 0e8fc040a62d0..dc7a4c3518cd8 100644 --- a/chrome/browser/ssl/https_only_mode_browsertest.cc +++ b/chrome/browser/ssl/https_only_mode_browsertest.cc @@ -44,6 +44,8 @@ using security_interstitials::https_only_mode::Event; using security_interstitials::https_only_mode::kEventHistogram; +// Tests for the v1 implementation of HTTPS-First Mode. See +// https_upgrade_browsertest.cc for the tests for v2. class HttpsOnlyModeBrowserTest : public InProcessBrowserTest { public: HttpsOnlyModeBrowserTest() = default; diff --git a/chrome/browser/ssl/https_only_mode_controller_client.cc b/chrome/browser/ssl/https_only_mode_controller_client.cc index 690225fd55710..4da77532fc113 100644 --- a/chrome/browser/ssl/https_only_mode_controller_client.cc +++ b/chrome/browser/ssl/https_only_mode_controller_client.cc @@ -9,6 +9,7 @@ #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ssl/https_only_mode_tab_helper.h" #include "chrome/browser/ssl/stateful_ssl_host_state_delegate_factory.h" +#include "chrome/common/chrome_features.h" #include "chrome/common/webui_url_constants.h" #include "components/security_interstitials/content/settings_page_helper.h" #include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h" @@ -57,7 +58,16 @@ void HttpsOnlyModeControllerClient::Proceed() { } auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(web_contents_); tab_helper->set_is_navigation_upgraded(false); - tab_helper->set_is_navigation_fallback(true); + + // Proceeding through the interstitial triggers the fallback navigation for + // the initial version of HTTPS-First Mode, but in the new version the + // interstitial is the result of the fallback navigation. Update state + // accordingly. + if (base::FeatureList::IsEnabled(features::kHttpsFirstModeV2)) { + tab_helper->set_is_navigation_fallback(false); + } else { + tab_helper->set_is_navigation_fallback(true); + } web_contents_->GetController().Reload(content::ReloadType::NORMAL, false); // The failed https navigation will remain as a forward entry, so it needs to // be removed. diff --git a/chrome/browser/ssl/https_upgrades_browsertest.cc b/chrome/browser/ssl/https_upgrades_browsertest.cc new file mode 100644 index 0000000000000..1c7afd6b027fe --- /dev/null +++ b/chrome/browser/ssl/https_upgrades_browsertest.cc @@ -0,0 +1,1044 @@ +// 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 +#include + +#include "base/test/bind.h" +#include "base/test/metrics/histogram_tester.h" +#include "base/test/simple_test_clock.h" +#include "chrome/browser/interstitials/security_interstitial_page_test_utils.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ssl/https_only_mode_navigation_throttle.h" +#include "chrome/browser/ssl/https_only_mode_upgrade_interceptor.h" +#include "chrome/browser/ssl/security_state_tab_helper.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_tabstrip.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "components/prefs/pref_service.h" +#include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h" +#include "components/security_interstitials/core/https_only_mode_metrics.h" +#include "components/security_interstitials/core/metrics_helper.h" +#include "components/strings/grit/components_strings.h" +#include "components/variations/active_field_trials.h" +#include "components/variations/hashing.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/content_browser_test_utils.h" +#include "content/public/test/content_mock_cert_verifier.h" +#include "content/public/test/test_navigation_observer.h" +#include "net/dns/mock_host_resolver.h" +#include "net/test/cert_test_util.h" +#include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/request_handler_util.h" +#include "net/test/test_data_directory.h" +#include "services/network/public/mojom/network_context.mojom.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/url_constants.h" + +using security_interstitials::https_only_mode::Event; +using security_interstitials::https_only_mode::kEventHistogram; + +// Tests for the v2 implementation of HTTPS-First Mode. +class HttpsUpgradesBrowserTest : public InProcessBrowserTest { + public: + HttpsUpgradesBrowserTest() = default; + ~HttpsUpgradesBrowserTest() override = default; + + void SetUp() override { + feature_list_.InitAndEnableFeature(features::kHttpsFirstModeV2); + InProcessBrowserTest::SetUp(); + } + + void SetUpOnMainThread() override { + // By default allow all hosts on HTTPS. + mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); + host_resolver()->AddRule("*", "127.0.0.1"); + + // Set up "bad-https.test" as a hostname with an SSL error. HTTPS upgrades + // to this host will fail. + scoped_refptr cert(https_server_.GetCertificate()); + net::CertVerifyResult verify_result; + verify_result.is_issued_by_known_root = false; + verify_result.verified_cert = cert; + verify_result.cert_status = net::CERT_STATUS_COMMON_NAME_INVALID; + mock_cert_verifier_.mock_cert_verifier()->AddResultForCertAndHost( + cert, "bad-https.test", verify_result, + net::ERR_CERT_COMMON_NAME_INVALID); + + http_server_.AddDefaultHandlers(GetChromeTestDataDir()); + https_server_.AddDefaultHandlers(GetChromeTestDataDir()); + ASSERT_TRUE(http_server_.Start()); + ASSERT_TRUE(https_server_.Start()); + + HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting( + https_server()->port()); + HttpsOnlyModeUpgradeInterceptor::SetHttpPortForTesting( + http_server()->port()); + + SetPref(true); + } + + void TearDownOnMainThread() override { SetPref(false); } + + void SetUpCommandLine(base::CommandLine* command_line) override { + mock_cert_verifier_.SetUpCommandLine(command_line); + } + + void SetUpInProcessBrowserTestFixture() override { + mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); + } + + void TearDownInProcessBrowserTestFixture() override { + mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); + } + + protected: + void SetPref(bool enabled) { + auto* prefs = browser()->profile()->GetPrefs(); + prefs->SetBoolean(prefs::kHttpsOnlyModeEnabled, enabled); + } + + bool GetPref() const { + auto* prefs = browser()->profile()->GetPrefs(); + return prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled); + } + + void ProceedThroughInterstitial(content::WebContents* tab) { + content::TestNavigationObserver nav_observer(tab, 1); + std::string javascript = "window.certificateErrorPageController.proceed();"; + ASSERT_TRUE(content::ExecuteScript(tab, javascript)); + nav_observer.Wait(); + } + + void DontProceedThroughInterstitial(content::WebContents* tab) { + content::TestNavigationObserver nav_observer(tab, 1); + std::string javascript = + "window.certificateErrorPageController.dontProceed();"; + ASSERT_TRUE(content::ExecuteScript(tab, javascript)); + nav_observer.Wait(); + } + + void NavigateAndWaitForFallback(content::WebContents* tab, const GURL& url) { + // Fallback to HTTP (and showing the HTTPS-First Mode interstitial, if + // enabled) is a new navigation, so navigate to the initial URL and wait + // for *two* navigations to complete. + content::NavigateToURLBlockUntilNavigationsComplete(tab, url, 2); + } + + net::EmbeddedTestServer* http_server() { return &http_server_; } + net::EmbeddedTestServer* https_server() { return &https_server_; } + base::HistogramTester* histograms() { return &histograms_; } + + private: + base::test::ScopedFeatureList feature_list_; + net::EmbeddedTestServer http_server_{net::EmbeddedTestServer::TYPE_HTTP}; + net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; + content::ContentMockCertVerifier mock_cert_verifier_; + base::HistogramTester histograms_; +}; + +// If the user navigates to an HTTP URL for a site that supports HTTPS, the +// navigation should end up on the HTTPS version of the URL. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + UrlWithHttpScheme_ShouldUpgrade) { + GURL http_url = http_server()->GetURL("foo.test", "/simple.html"); + GURL https_url = https_server()->GetURL("foo.test", "/simple.html"); + + // The NavigateToURL() call returns `false` because the navigation is + // redirected to HTTPS. + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + content::TestNavigationObserver nav_observer(contents, 1); + EXPECT_FALSE(content::NavigateToURL(contents, http_url)); + nav_observer.Wait(); + + EXPECT_TRUE(nav_observer.last_navigation_succeeded()); + EXPECT_EQ(https_url, contents->GetLastCommittedURL()); + EXPECT_FALSE(chrome_browser_interstitials::IsShowingInterstitial(contents)); + + // Verify that navigation event metrics were correctly recorded. + histograms()->ExpectTotalCount(kEventHistogram, 2); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeSucceeded, 1); +} + +// If the user navigates to an HTTPS URL for a site that supports HTTPS, the +// navigation should end up on that exact URL. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + UrlWithHttpsScheme_ShouldLoad) { + GURL https_url = https_server()->GetURL("foo.test", "/simple.html"); + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + EXPECT_TRUE(content::NavigateToURL(contents, https_url)); + + // Verify that navigation event metrics were not recorded as the navigation + // was not upgraded. + histograms()->ExpectTotalCount(kEventHistogram, 0); +} + +// If the user navigates to a localhost URL, the navigation should end up on +// that exact URL. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, Localhost_ShouldNotUpgrade) { + GURL localhost_url = http_server()->GetURL("localhost", "/simple.html"); + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + EXPECT_TRUE(content::NavigateToURL(contents, localhost_url)); + + // Verify that navigation event metrics were not recorded as the navigation + // was not upgraded. + histograms()->ExpectTotalCount(kEventHistogram, 0); +} + +// If the user navigates to an HTTPS URL, the navigation should end up on that +// exact URL, even if the site has an SSL error. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + UrlWithHttpsScheme_BrokenSSL_ShouldNotFallback) { + GURL https_url = https_server()->GetURL("bad-https.test", "/simple.html"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + EXPECT_FALSE(content::NavigateToURL(contents, https_url)); + EXPECT_EQ(https_url, contents->GetLastCommittedURL()); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingSSLInterstitial(contents)); + + // Verify that navigation event metrics were not recorded as the navigation + // was not upgraded. + histograms()->ExpectTotalCount(kEventHistogram, 0); +} + +// If the user navigates to an HTTP URL for a site with broken HTTPS, the +// navigation should end up on the HTTPS URL and show the HTTPS-Only Mode +// interstitial. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + UrlWithHttpScheme_BrokenSSL_ShouldInterstitial) { + GURL http_url = http_server()->GetURL("bad-https.test", "/simple.html"); + GURL https_url = https_server()->GetURL("bad-https.test", "/simple.html"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Verify that navigation event metrics were correctly recorded. + histograms()->ExpectTotalCount(kEventHistogram, 3); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, 1); +} + +// If the user triggers an HTTPS-Only Mode interstitial for a host and then +// clicks through the interstitial, they should end up on the HTTP URL. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + InterstitialBypassed_HttpFallbackLoaded) { + GURL http_url = http_server()->GetURL("bad-https.test", "/simple.html"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Proceed through the interstitial, which will add the host to the allowlist + // and navigate to the HTTP fallback URL. + ProceedThroughInterstitial(contents); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + + // Verify that navigation event metrics were correctly recorded. + histograms()->ExpectTotalCount(kEventHistogram, 3); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, 1); + + // Verify that the interstitial metrics were correctly recorded. + histograms()->ExpectTotalCount("interstitial.https_first_mode.decision", 2); + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.decision", + security_interstitials::MetricsHelper::Decision::SHOW, 1); + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.decision", + security_interstitials::MetricsHelper::Decision::PROCEED, 1); +} + +// If the upgraded HTTPS URL is not available due to a net error, it should +// trigger the HTTPS-Only Mode interstitial and offer fallback. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + NetErrorOnUpgrade_ShouldInterstitial) { + GURL http_url = http_server()->GetURL("foo.test", "/close-socket"); + GURL https_url = https_server()->GetURL("foo.test", "/close-socket"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Verify that navigation event metrics were correctly recorded. + histograms()->ExpectTotalCount(kEventHistogram, 3); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeNetError, 1); +} + +// Navigations in subframes should not get upgraded by HTTPS-Only Mode. They +// should be blocked as mixed content. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + HttpsParentHttpSubframeNavigation_Blocked) { + const GURL parent_url( + https_server()->GetURL("foo.test", "/iframe_blank.html")); + const GURL iframe_url(http_server()->GetURL("foo.test", "/simple.html")); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + EXPECT_TRUE(content::NavigateToURL(contents, parent_url)); + + content::TestNavigationObserver nav_observer(contents, 1); + EXPECT_TRUE(content::NavigateIframeToURL(contents, "test", iframe_url)); + nav_observer.Wait(); + EXPECT_NE(iframe_url, nav_observer.last_navigation_url()); + + // Verify that no navigation event metrics were recorded. + histograms()->ExpectTotalCount(kEventHistogram, 0); +} + +// Navigating to an HTTP URL in a subframe of an HTTP page should not upgrade +// the subframe navigation to HTTPS (even if the subframe navigation is to a +// different host than the parent frame). +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + HttpParentHttpSubframeNavigation_NotUpgraded) { + // The parent frame will fail to upgrade to HTTPS. + const GURL parent_url( + http_server()->GetURL("bad-https.test", "/iframe_blank.html")); + const GURL iframe_url(http_server()->GetURL("bar.test", "/simple.html")); + + // Navigate to `parent_url` and bypass the HTTPS-Only Mode warning. + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, parent_url); + + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + // Proceeding through the interstitial will add the hostname to the allowlist. + ProceedThroughInterstitial(contents); + + // Verify that navigation event metrics were recorded for the main frame. + histograms()->ExpectTotalCount(kEventHistogram, 3); + + // Navigate the iframe to `iframe_url`. It should successfully navigate and + // not get upgraded to HTTPS as the hostname is now in the allowlist. + content::TestNavigationObserver nav_observer(contents, 1); + EXPECT_TRUE(content::NavigateIframeToURL(contents, "test", iframe_url)); + nav_observer.Wait(); + EXPECT_EQ(iframe_url, nav_observer.last_navigation_url()); + + // Verify that no new navigation event metrics were recorded for the subframe. + histograms()->ExpectTotalCount(kEventHistogram, 3); +} + +// Tests that a navigation to the HTTP version of a site with an HTTPS version +// that is slow to respond gets upgraded to HTTPS but times out and shows the +// HTTPS-Only Mode interstitial. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, SlowHttps_ShouldInterstitial) { + // Set timeout to zero so that HTTPS upgrades immediately timeout. + HttpsOnlyModeNavigationThrottle::set_timeout_for_testing(0); + + const GURL http_url = http_server()->GetURL("foo.test", "/hung"); + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); +} + +// Tests that an HTTP POST form navigation to "bar.test" from an HTTP page on +// "foo.test" is not upgraded to HTTPS. (HTTP form navigations from HTTPS are +// blocked by the Mixed Forms warning.) +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, HttpPageHttpPost_NotUpgraded) { + // Point the HTTP form target to "bar.test". + base::StringPairs replacement_text; + replacement_text.emplace_back(make_pair( + "REPLACE_WITH_HOST_AND_PORT", + net::HostPortPair::FromURL(http_server()->GetURL("foo.test", "/")) + .ToString())); + auto replacement_path = net::test_server::GetFilePathWithReplacements( + "/ssl/page_with_form_targeting_http_url.html", replacement_text); + + // Navigate to the page hosting the form on "foo.test". The HTTPS-Only Mode + // interstitial should trigger. + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + content::NavigateToURLBlockUntilNavigationsComplete( + contents, http_server()->GetURL("bad-https.test", replacement_path), 2); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Proceed through the interstitial to add the hostname to the allowlist. + ProceedThroughInterstitial(contents); + + // Verify that navigation event metrics were recorded for the initial page. + histograms()->ExpectTotalCount(kEventHistogram, 3); + + // Submit the form and wait for the navigation to complete. + content::TestNavigationObserver nav_observer(contents, 1); + ASSERT_TRUE(content::ExecuteScript( + contents, "document.getElementById('submit').click();")); + nav_observer.Wait(); + + // Check that the navigation has ended up on the HTTP target. + EXPECT_EQ("foo.test", contents->GetLastCommittedURL().host()); + EXPECT_TRUE(contents->GetLastCommittedURL().SchemeIs(url::kHttpScheme)); + + // Verify that no new navigation event metrics were recorded for the POST + // navigation. + histograms()->ExpectTotalCount(kEventHistogram, 3); +} + +// Tests that if an HTTPS navigation redirects to HTTP on a different host, it +// should upgrade to HTTPS on that new host. (A downgrade redirect on the same +// host would imply a redirect loop.) +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + HttpsToHttpRedirect_ShouldUpgrade) { + GURL target_url = http_server()->GetURL("bar.test", "/title1.html"); + GURL url = https_server()->GetURL("foo.test", + "/server-redirect?" + target_url.spec()); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + + // NavigateToURL() returns `false` because the final redirected URL does not + // match `url`. Separately ensure the navigation succeeded using a navigation + // observer. + content::TestNavigationObserver nav_observer(contents, 1); + EXPECT_FALSE(content::NavigateToURL(contents, url)); + nav_observer.Wait(); + EXPECT_TRUE(nav_observer.last_navigation_succeeded()); + + EXPECT_TRUE(contents->GetLastCommittedURL().SchemeIs(url::kHttpsScheme)); + EXPECT_EQ("bar.test", contents->GetLastCommittedURL().host()); + + // Verify that navigation event metrics were correctly recorded. + histograms()->ExpectTotalCount(kEventHistogram, 2); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeSucceeded, 1); +} + +// Tests that navigating to an HTTPS page that downgrades to HTTP on the same +// host will fail and trigger the HTTPS-Only Mode interstitial (due to the +// redirect loop hitting the redirect limit). +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + RedirectLoop_ShouldInterstitial) { + // Set up a new test server instance so it can have a custom handler. + net::EmbeddedTestServer downgrading_server{ + net::EmbeddedTestServer::TYPE_HTTPS}; + // Downgrade by swapping the scheme for HTTP. HTTPS-Only Mode will upgrade it + // back to HTTPS. + downgrading_server.RegisterRequestHandler(base::BindLambdaForTesting( + [&](const net::test_server::HttpRequest& request) + -> std::unique_ptr { + GURL::Replacements http_downgrade; + http_downgrade.SetSchemeStr(url::kHttpScheme); + // The HttpRequest will by default refer to the test server by the + // loopback address rather than any hostname in the navigation (i.e., + // the EmbeddedTestServer has no notion of virtual hosts). This + // explicitly sets the hostname back to the test host so that this + // doesn't fail due to the exception for localhost. + http_downgrade.SetHostStr("foo.test"); + auto redirect_url = request.GetURL().ReplaceComponents(http_downgrade); + auto response = std::make_unique(); + response->set_code(net::HTTP_TEMPORARY_REDIRECT); + response->AddCustomHeader("Location", redirect_url.spec()); + return response; + })); + ASSERT_TRUE(downgrading_server.Start()); + HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting( + downgrading_server.port()); + + GURL url = downgrading_server.GetURL("foo.test", "/"); + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, url); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Verify that navigation event metrics were correctly recorded. + histograms()->ExpectTotalCount(kEventHistogram, 3); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeNetError, 1); +} + +// Tests that the security level is WARNING when the HTTPS-Only Mode +// interstitial is shown for a net error on HTTPS. (Without HTTPS-Only Mode, a +// net error would be a security level of NONE.) +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + NetErrorOnUpgrade_SecurityLevelWarning) { + GURL http_url = http_server()->GetURL("foo.test", "/close-socket"); + GURL https_url = https_server()->GetURL("foo.test", "/close-socket"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + auto* helper = SecurityStateTabHelper::FromWebContents(contents); + EXPECT_EQ(security_state::WARNING, helper->GetSecurityLevel()); + + // Proceed through the interstitial to navigate to the HTTP site. The HTTP + // site results in a net error, which should have security level NONE (as no + // connection was made). + ProceedThroughInterstitial(contents); + EXPECT_EQ(security_state::NONE, helper->GetSecurityLevel()); +} + +// Tests that the security level is WARNING when the HTTPS-Only Mode +// interstitial is shown for a cert error on HTTPS. (Without HTTPS-Only Mode, a +// a cert error would be a security level of DANGEROUS.) After clicking through +// the interstitial, the security level should still be WARNING. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + BrokenSSLOnUpgrade_SecurityLevelWarning) { + GURL http_url = http_server()->GetURL("bad-https.test", "/simple.html"); + GURL https_url = https_server()->GetURL("bad-https.test", "/simple.html"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + auto* helper = SecurityStateTabHelper::FromWebContents(contents); + EXPECT_EQ(security_state::WARNING, helper->GetSecurityLevel()); + + // Proceed through the interstitial to navigate to the HTTP page. The security + // level should still be WARNING. + ProceedThroughInterstitial(contents); + EXPECT_EQ(security_state::WARNING, helper->GetSecurityLevel()); +} + +// Regression test for crbug.com/1233207. +// Tests the case where the HTTP version of a site redirects to HTTPS, but the +// HTTPS version of the site has a cert error. If the user initially navigates +// to the HTTP URL, then HTTPS-First Mode should upgrade the navigation to HTTPS +// and trigger the HTTPS-First Mode interstitial when that fails, but if the +// user clicks through the HTTPS-First Mode interstitial and falls back into the +// HTTP->HTTPS redirect back to the cert error, then the SSL interstitial should +// be shown and the user should be able to click through the SSL interstitial to +// visit the HTTPS version of the site (but in a DANGEROUS security level +// state). +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + HttpsUpgradeWithBrokenSSL_ShouldTriggerSSLInterstitial) { + // Set up a new test server instance so it can have a custom handler that + // redirects to the HTTPS server. + net::EmbeddedTestServer upgrading_server{net::EmbeddedTestServer::TYPE_HTTP}; + upgrading_server.RegisterRequestHandler(base::BindLambdaForTesting( + [&](const net::test_server::HttpRequest& request) + -> std::unique_ptr { + auto response = std::make_unique(); + response->set_code(net::HTTP_TEMPORARY_REDIRECT); + response->AddCustomHeader( + "Location", + "https://bad-https.test:" + + base::NumberToString( + HttpsOnlyModeUpgradeInterceptor::GetHttpsPortForTesting()) + + "/simple.html"); + return response; + })); + HttpsOnlyModeUpgradeInterceptor::SetHttpPortForTesting( + upgrading_server.port()); + ASSERT_TRUE(upgrading_server.Start()); + + GURL http_url = upgrading_server.GetURL("bad-https.test", "/simple.html"); + // HTTPS server will have a cert error. + GURL https_url = https_server()->GetURL("bad-https.test", "/simple.html"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + + // The HTTPS-First Mode interstitial should trigger first. + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Proceeding through the HTTPS-First Mode interstitial will hit the upgrading + // server's HTTP->HTTPS redirect. This should result in an SSL interstitial + // (not an HTTPS-First Mode interstitial). + ProceedThroughInterstitial(contents); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingSSLInterstitial(contents)); + + // Proceeding through the SSL interstitial should navigate to the HTTPS + // version of the site but with the DANGEROUS security level. + ProceedThroughInterstitial(contents); + EXPECT_EQ(https_url, contents->GetLastCommittedURL()); + auto* helper = SecurityStateTabHelper::FromWebContents(contents); + EXPECT_EQ(security_state::DANGEROUS, helper->GetSecurityLevel()); + + // Verify that navigation event metrics were correctly recorded. They should + // only have been recorded for the initial navigation that resulted in the + // HTTPS-First Mode interstitial. + histograms()->ExpectTotalCount(kEventHistogram, 3); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); + histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, 1); + + // Verify that the interstitial metrics were correctly recorded. + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.decision", + security_interstitials::MetricsHelper::Decision::SHOW, 1); + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.decision", + security_interstitials::MetricsHelper::Decision::PROCEED, 1); +} + +// Tests that clicking the "Learn More" link in the HTTPS-First Mode +// interstitial opens a new tab for the help center article. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, InterstitialLearnMoreLink) { + GURL http_url = http_server()->GetURL("foo.test", "/close-socket"); + GURL https_url = https_server()->GetURL("foo.test", "/close-socket"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Simulate clicking the learn more link (CMD_OPEN_HELP_CENTER). + ASSERT_TRUE(content::ExecuteScript( + contents, "window.certificateErrorPageController.openHelpCenter();")); + + // New tab should include the p-link "first_mode". + EXPECT_EQ(browser() + ->tab_strip_model() + ->GetActiveWebContents() + ->GetVisibleURL() + .query(), + "p=first_mode"); + + // Verify that the interstitial metrics were correctly recorded. + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.decision", + security_interstitials::MetricsHelper::Decision::SHOW, 1); + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.interaction", + security_interstitials::MetricsHelper::Interaction::TOTAL_VISITS, 1); + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.interaction", + security_interstitials::MetricsHelper::Interaction::SHOW_LEARN_MORE, 1); +} + +// Tests that if the user bypasses the HTTPS-First Mode interstitial, and then +// later the server fixes their HTTPS support and the user successfully connects +// over HTTPS, the allowlist entry is cleared (so HFM will kick in again for +// that site). +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, BadHttpsFollowedByGoodHttps) { + GURL http_url = http_server()->GetURL("foo.test", "/close-socket"); + GURL bad_https_url = https_server()->GetURL("foo.test", "/close-socket"); + GURL good_https_url = https_server()->GetURL("foo.test", "/ssl/google.html"); + + ASSERT_EQ(http_url.host(), bad_https_url.host()); + ASSERT_EQ(bad_https_url.host(), good_https_url.host()); + + auto* tab = browser()->tab_strip_model()->GetActiveWebContents(); + auto* profile = Profile::FromBrowserContext(tab->GetBrowserContext()); + auto* state = static_cast( + profile->GetSSLHostStateDelegate()); + + // First check that main frame requests revoke the decision. + + // Navigate to `http_url`, which will get upgraded to `bad_https_url`. + NavigateAndWaitForFallback(tab, http_url); + + ASSERT_TRUE( + chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(tab)); + ProceedThroughInterstitial(tab); + EXPECT_TRUE(state->HasAllowException( + http_url.host(), tab->GetPrimaryMainFrame()->GetStoragePartition())); + + EXPECT_TRUE(content::NavigateToURL(tab, good_https_url)); + EXPECT_FALSE(state->HasAllowException( + http_url.host(), tab->GetPrimaryMainFrame()->GetStoragePartition())); + + // Rarely, an open connection with the bad cert might be reused for the next + // navigation, which is supposed to show an interstitial. Close open + // connections to ensure a fresh connection (and certificate validation) for + // the next navigation. See https://crbug.com/1150592. A deeper fix for this + // issue would be to unify certificate bypass logic which is currently split + // between the net stack and content layer; see https://crbug.com/488043. + // See also: SSLUITest.BadCertFollowedByGoodCert. + state->RevokeUserAllowExceptionsHard(http_url.host()); + + // Now check that subresource requests revoke the decision. + + // Navigate to `http_url`, which will get upgraded to `bad_https_url`. + NavigateAndWaitForFallback(tab, http_url); + + ASSERT_TRUE( + chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(tab)); + ProceedThroughInterstitial(tab); + EXPECT_TRUE(state->HasAllowException( + http_url.host(), tab->GetPrimaryMainFrame()->GetStoragePartition())); + + // Load "logo.gif" as an image on the page. + GURL image = https_server()->GetURL("foo.test", "/ssl/google_files/logo.gif"); + bool result = false; + EXPECT_TRUE(ExecuteScriptAndExtractBool( + tab, + std::string("var img = document.createElement('img');img.src ='") + + image.spec() + + "';img.onload=function() { " + "window.domAutomationController.send(true); };" + "document.body.appendChild(img);", + &result)); + EXPECT_TRUE(result); + + EXPECT_FALSE(state->HasAllowException( + http_url.host(), tab->GetPrimaryMainFrame()->GetStoragePartition())); +} + +// Tests that clicking the "Go back" button in the HTTPS-First Mode interstitial +// navigates back to the previous page (about:blank in this case). +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, InterstitialGoBack) { + GURL http_url = http_server()->GetURL("foo.test", "/close-socket"); + GURL https_url = https_server()->GetURL("foo.test", "/close-socket"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Simulate clicking the "Go back" button. + DontProceedThroughInterstitial(contents); + + EXPECT_EQ(GURL("about:blank"), contents->GetLastCommittedURL()); + + // Verify that the interstitial metrics were correctly recorded. + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.decision", + security_interstitials::MetricsHelper::Decision::SHOW, 1); + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.decision", + security_interstitials::MetricsHelper::Decision::DONT_PROCEED, 1); +} + +// Tests that closing the tab of the HTTPS-First Mode interstitial counts as +// not proceeding through the interstitial for metrics. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, CloseInterstitialTab) { + GURL http_url = http_server()->GetURL("foo.test", "/close-socket"); + GURL https_url = https_server()->GetURL("foo.test", "/close-socket"); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Leave the interstitial by closing the tab. + chrome::CloseWebContents(browser(), contents, false); + + // Verify that the interstitial metrics were correctly recorded. + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.decision", + security_interstitials::MetricsHelper::Decision::SHOW, 1); + histograms()->ExpectBucketCount( + "interstitial.https_first_mode.decision", + security_interstitials::MetricsHelper::Decision::DONT_PROCEED, 1); +} + +// Tests that if a user allowlists a host and then does not visit it again for +// seven days (the expiration period), then the interstitial will be shown again +// the next time they visit the host. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, AllowlistEntryExpires) { + content::WebContents* contents = + browser()->tab_strip_model()->GetActiveWebContents(); + Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); + content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); + + // Set a testing clock on the StatefulSSLHostStateDelegate, keeping a pointer + // to the clock object around so the test can manipulate time. `chrome_state` + // takes ownership of `clock`. + auto clock = std::make_unique(); + auto* clock_ptr = clock.get(); + StatefulSSLHostStateDelegate* chrome_state = + static_cast(state); + chrome_state->SetClockForTesting(std::move(clock)); + + // Start the clock at standard system time. + clock_ptr->SetNow(base::Time::NowFromSystemTime()); + + // Visit a host that doesn't support HTTPS for the first time, and click + // through the HTTPS-First Mode interstitial to allowlist the host. + GURL http_url = http_server()->GetURL("bad-https.test", "/simple.html"); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + ProceedThroughInterstitial(contents); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + EXPECT_TRUE(state->IsHttpAllowedForHost( + http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); + + // Simulate the clock advancing by eight days, which is past the expiration + // point. + clock_ptr->Advance(base::Days(8)); + + // The host should no longer be allowlisted, and the interstitial should + // trigger again. + EXPECT_FALSE(state->IsHttpAllowedForHost( + http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); +} + +// Tests that re-visiting an allowlisted host bumps the expiration time to a new +// seven days in the future from now. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, RevisitingBumpsExpiration) { + content::WebContents* contents = + browser()->tab_strip_model()->GetActiveWebContents(); + Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); + content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); + + // Set a testing clock on the StatefulSSLHostStateDelegate, keeping a pointer + // to the clock object around so the test can manipulate time. `chrome_state` + // takes ownership of `clock`. + auto clock = std::make_unique(); + auto* clock_ptr = clock.get(); + StatefulSSLHostStateDelegate* chrome_state = + static_cast(state); + chrome_state->SetClockForTesting(std::move(clock)); + + // Start the clock at standard system time. + clock_ptr->SetNow(base::Time::NowFromSystemTime()); + + // Visit a host that doesn't support HTTPS for the first time, and click + // through the HTTPS-First Mode interstitial to allowlist the host. + GURL http_url = http_server()->GetURL("bad-https.test", "/simple.html"); + NavigateAndWaitForFallback(contents, http_url); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + ProceedThroughInterstitial(contents); + EXPECT_EQ(http_url, contents->GetLastCommittedURL()); + EXPECT_TRUE(state->IsHttpAllowedForHost( + http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); + + // Simulate the clock advancing by five days. + clock_ptr->Advance(base::Days(5)); + + // Navigate to the host again; this will reset the allowlist expiration to + // now + 7 days. + EXPECT_TRUE(content::NavigateToURL(contents, http_url)); + + // Simulate the clock advancing another five days. This will be _after_ the + // initial expiration date of the allowlist entry, but _before_ the bumped + // expiration date from the second navigation. + clock_ptr->Advance(base::Days(5)); + EXPECT_TRUE(state->IsHttpAllowedForHost( + http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); + EXPECT_TRUE(content::NavigateToURL(contents, http_url)); + EXPECT_FALSE( + chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); +} + +// Tests that if a hostname has an HSTS entry registered, then HTTPS-First Mode +// should not try to upgrade it (instead allowing HSTS to handle the upgrade as +// it is more strict). +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, PreferHstsOverHttpsFirstMode) { + content::WebContents* contents = + browser()->tab_strip_model()->GetActiveWebContents(); + Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); + + // URL for HTTPS server that will result in a certificate error. + GURL https_url = https_server()->GetURL("bad-https.test", "/simple.html"); + + // HTTP version of that URL that will get upgraded to HTTPS (but with the + // correct port for the HTTPS server -- the test code can configure + // HTTPS-First Mode to be aware of the different ports, but can't do that for + // HSTS). + GURL::Replacements downgrade_scheme_to_http; + downgrade_scheme_to_http.SetSchemeStr(url::kHttpScheme); + GURL http_url = https_url.ReplaceComponents(downgrade_scheme_to_http); + + // Add hostname to the TransportSecurityState. + base::Time expiry = base::Time::Now() + base::Days(100); + bool include_subdomains = false; + auto* network_context = + profile->GetDefaultStoragePartition()->GetNetworkContext(); + base::RunLoop run_loop; + network_context->AddHSTS(http_url.host(), expiry, include_subdomains, + run_loop.QuitClosure()); + run_loop.Run(); + + // Navigate to the HTTP URL. It should get upgraded to HTTPS and trigger a + // fatal certificate error (because of HTTPS) instead of falling back to the + // HTTPS-First Mode interstitial. + EXPECT_FALSE(content::NavigateToURL(contents, http_url)); + EXPECT_FALSE( + chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingSSLInterstitial(contents)); + + // Verify that no HFM event histograms were emitted (to check that HFM did not + // trigger for this navigation at all). + histograms()->ExpectTotalCount(kEventHistogram, 0); +} + +// Regression test for crbug.com/1272781. Previously, performing back/forward +// navigations around the HTTPS-First Mode interstitial could cause history +// entries to dropped. +// TODO(crbug.com/1394910): Going back from HFM interstitial to an HTTPS page +// still loses the security indicator of the previous HTTPS page. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest, + InterstitialFallbackMaintainsHistory) { + GURL good_https_url = + https_server()->GetURL("site1.test", "/defaultresponse"); + + // Set up a new test server instance so it can have a custom handler. + net::EmbeddedTestServer downgrading_server{ + net::EmbeddedTestServer::TYPE_HTTPS}; + // Downgrade by swapping the scheme for HTTP. HTTPS-First Mode will upgrade it + // back to HTTPS. + downgrading_server.RegisterRequestHandler(base::BindLambdaForTesting( + [&](const net::test_server::HttpRequest& request) + -> std::unique_ptr { + GURL::Replacements http_downgrade; + http_downgrade.SetSchemeStr(url::kHttpScheme); + // The HttpRequest will by default refer to the test server by the + // loopback address rather than any hostname in the navigation (i.e., + // the EmbeddedTestServer has no notion of virtual hosts). This + // explicitly sets the hostname back to the test host so that this + // doesn't fail due to the exception for localhost. + http_downgrade.SetHostStr("site2.test"); + auto redirect_url = request.GetURL().ReplaceComponents(http_downgrade); + auto response = std::make_unique(); + response->set_code(net::HTTP_TEMPORARY_REDIRECT); + response->AddCustomHeader("Location", redirect_url.spec()); + return response; + })); + ASSERT_TRUE(downgrading_server.Start()); + HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting( + downgrading_server.port()); + + GURL downgrading_https_url = downgrading_server.GetURL("site2.test", "/"); + GURL::Replacements swap_http_scheme; + swap_http_scheme.SetSchemeStr(url::kHttpScheme); + GURL downgrading_http_url = + downgrading_https_url.ReplaceComponents(swap_http_scheme); + + auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); + + // Navigate to a "good" HTTPS site. + EXPECT_TRUE(content::NavigateToURL(contents, good_https_url)); + + // Navigate to the HTTP version of `downgrading_https_url`, which will get + // upgraded to HTTPS and fail, triggering the HTTPS-First Mode + // interstitial. + content::NavigateToURLBlockUntilNavigationsComplete(contents, + downgrading_http_url, 2); + EXPECT_EQ(downgrading_http_url, contents->GetLastCommittedURL()); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // Simulate clicking the browser "back" button. + // TODO(crbug.com/1394910): The incorrect WARNING security state is retained + // from the interstitial page. + EXPECT_TRUE(content::HistoryGoBack(contents)); + EXPECT_EQ(good_https_url, contents->GetLastCommittedURL()); + auto* helper = SecurityStateTabHelper::FromWebContents(contents); + EXPECT_EQ(security_state::WARNING, helper->GetSecurityLevel()); + + // Simulate clicking the browser "forward" button. (The HistoryGoForward() + // call returns `false` because it is an error page.) + EXPECT_FALSE(content::HistoryGoForward(contents)); + EXPECT_EQ(downgrading_http_url, contents->GetLastCommittedURL()); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + + // No forward entry should be present. + EXPECT_FALSE(contents->GetController().CanGoForward()); + + // Simulate clicking the browser "back" button again. Previously this would + // result in `about:blank` being shown. + EXPECT_TRUE(content::HistoryGoBack(contents)); + EXPECT_EQ(good_https_url, contents->GetLastCommittedURL()); + + // Repeat forward one last time. (Previously the user would no longer be able + // to go back any more as the history entries were lost.) + // The HistoryGoForward() call returns `false` because it is an error page. + EXPECT_FALSE(content::HistoryGoForward(contents)); + EXPECT_EQ(downgrading_http_url, contents->GetLastCommittedURL()); + EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( + contents)); + EXPECT_TRUE(contents->GetController().CanGoBack()); +} + +// A simple test fixture that ensures the kHttpsFirstModeV2 feature is enabled +// and constructs a HistogramTester (so that it gets initialized before browser +// startup). Used for testing pref tracking logic. +class HttpsUpgradesPrefsBrowserTest : public InProcessBrowserTest { + public: + HttpsUpgradesPrefsBrowserTest() = default; + ~HttpsUpgradesPrefsBrowserTest() override = default; + + void SetUp() override { + feature_list_.InitAndEnableFeature(features::kHttpsFirstModeV2); + InProcessBrowserTest::SetUp(); + } + + protected: + void SetPref(bool enabled) { + auto* prefs = browser()->profile()->GetPrefs(); + prefs->SetBoolean(prefs::kHttpsOnlyModeEnabled, enabled); + } + + bool GetPref() const { + auto* prefs = browser()->profile()->GetPrefs(); + return prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled); + } + + base::HistogramTester* histograms() { return &histograms_; } + + private: + base::test::ScopedFeatureList feature_list_; + base::HistogramTester histograms_; +}; + +// Tests that the HTTPS-First Mode pref is recorded at startup and when changed. +// This test requires restarting the browser to test the "at startup" metric in +// order for the preference state to be set up before the HttpsFirstModeService +// is created. +IN_PROC_BROWSER_TEST_F(HttpsUpgradesPrefsBrowserTest, PRE_PrefStatesRecorded) { + // The default pref state is `false`, which should get recorded when the + // initial browser instance is started here. + histograms()->ExpectUniqueSample( + "Security.HttpsFirstMode.SettingEnabledAtStartup", false, 1); + + EXPECT_TRUE(variations::IsInSyntheticTrialGroup("HttpsFirstModeClientSetting", + "Disabled")); + + // Change the pref to true. This should get recorded in the histogram. + SetPref(true); + histograms()->ExpectUniqueSample("Security.HttpsFirstMode.SettingChanged", + true, 1); + EXPECT_TRUE(variations::IsInSyntheticTrialGroup("HttpsFirstModeClientSetting", + "Enabled")); +} + +IN_PROC_BROWSER_TEST_F(HttpsUpgradesPrefsBrowserTest, PrefStatesRecorded) { + // Restarting the browser from the PRE_ test should record the startup pref + // histogram. Checking the unique count also ensures that other profile types + // (e.g. the ChromeOS sign-in profile) don't cause double-counting. + EXPECT_TRUE(GetPref()); + histograms()->ExpectUniqueSample( + "Security.HttpsFirstMode.SettingEnabledAtStartup", true, 1); + EXPECT_TRUE(variations::IsInSyntheticTrialGroup("HttpsFirstModeClientSetting", + "Enabled")); + + // Open an Incognito window. Startup metrics should not get recorded. + CreateIncognitoBrowser(); + histograms()->ExpectTotalCount( + "Security.HttpsFirstMode.SettingEnabledAtStartup", 1); +} diff --git a/chrome/browser/ssl/https_upgrades_interceptor.cc b/chrome/browser/ssl/https_upgrades_interceptor.cc new file mode 100644 index 0000000000000..955bb9df9a801 --- /dev/null +++ b/chrome/browser/ssl/https_upgrades_interceptor.cc @@ -0,0 +1,223 @@ +// 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/bind.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ssl/https_only_mode_tab_helper.h" +#include "chrome/browser/ssl/https_only_mode_upgrade_url_loader.h" +#include "chrome/browser/ssl/stateful_ssl_host_state_delegate_factory.h" +#include "chrome/common/pref_names.h" +#include "components/prefs/pref_service.h" +#include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h" +#include "content/public/browser/storage_partition.h" +#include "content/public/browser/web_contents.h" +#include "extensions/buildflags/buildflags.h" +#include "mojo/public/cpp/bindings/callback_helpers.h" +#include "net/base/url_util.h" +#include "services/network/public/mojom/network_context.mojom.h" +#include "third_party/blink/public/mojom/loader/resource_load_info.mojom.h" +#include "url/gurl.h" +#include "url/url_constants.h" +#include "url/url_util.h" + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#include "components/guest_view/browser/guest_view_base.h" +#endif // BUILDFLAG(ENABLE_EXTENSIONS) + +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; + +// Only serve upgrade redirects for main frame, GET requests to HTTP URLs. This +// excludes "localhost" (and loopback addresses) as they do not expose traffic +// over the network. +bool ShouldCreateLoader(const network::ResourceRequest& resource_request, + HttpsOnlyModeTabHelper* tab_helper) { + if (resource_request.is_outermost_main_frame && + resource_request.method == "GET" && + !net::IsLocalhost(resource_request.url) && + resource_request.url.SchemeIs(url::kHttpScheme) && + !tab_helper->is_navigation_fallback()) { + return true; + } + return false; +} + +} // namespace + +HttpsUpgradesInterceptor::HttpsUpgradesInterceptor(int frame_tree_node_id) + : frame_tree_node_id_(frame_tree_node_id) {} + +HttpsUpgradesInterceptor::~HttpsUpgradesInterceptor() = default; + +void HttpsUpgradesInterceptor::MaybeCreateLoader( + const network::ResourceRequest& tentative_resource_request, + content::BrowserContext* browser_context, + content::URLLoaderRequestInterceptor::LoaderCallback callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // 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; + } + + // Don't upgrade if the HTTPS-First Mode setting isn't enabled. + auto* prefs = profile->GetPrefs(); + if (!prefs || !prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled)) { + 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 ) then HTTPS-First Mode + // should not apply. See crbug.com/1233889 for more details. + if (guest_view::GuestViewBase::IsGuest(web_contents)) { + 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); + } + + // Don't upgrade navigation if it is allowlisted. + StatefulSSLHostStateDelegate* state = + static_cast( + 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); + + std::move(callback).Run({}); + return; + } + + if (!ShouldCreateLoader(tentative_resource_request, tab_helper)) { + 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). + auto query_complete_callback = base::BindOnce( + &HttpsUpgradesInterceptor::MaybeCreateLoaderOnHstsQueryCompleted, + weak_factory_.GetWeakPtr(), tentative_resource_request, + std::move(callback), tab_helper); + network::mojom::NetworkContext* network_context = + profile->GetDefaultStoragePartition()->GetNetworkContext(); + network_context->IsHSTSActiveForHost( + tentative_resource_request.url.host(), + 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, + HttpsOnlyModeTabHelper* tab_helper, + bool is_hsts_active_for_host) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // Don't upgrade this request if HSTS is active for this host. + if (is_hsts_active_for_host) { + std::move(callback).Run({}); + return; + } + + // Mark navigation as upgraded. + tab_helper->set_is_navigation_upgraded(true); + tab_helper->set_fallback_url(tentative_resource_request.url); + CreateHttpsRedirectLoader(tentative_resource_request, std::move(callback)); + // `redirect_url_loader_` can be null after this call. + redirect_url_loader_->StartRedirectToHttps(frame_tree_node_id_); +} + +// 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; +} + +// Creates a redirect URL loader that immediately serves a redirect to the +// upgraded HTTPS version of the URL. +void HttpsUpgradesInterceptor::CreateHttpsRedirectLoader( + const network::ResourceRequest& tentative_resource_request, + content::URLLoaderRequestInterceptor::LoaderCallback callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + redirect_url_loader_ = std::make_unique( + tentative_resource_request, + base::BindOnce(&HttpsUpgradesInterceptor::HandleRedirectLoader, + base::Unretained(this), std::move(callback))); +} + +// Runs `callback` with `handler`. +void HttpsUpgradesInterceptor::HandleRedirectLoader( + content::URLLoaderRequestInterceptor::LoaderCallback callback, + RequestHandler handler) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + // Handle any failure by using default loader. + if (handler.is_null()) { + redirect_url_loader_.reset(); + // PROCEED. + std::move(callback).Run({}); + return; + } + + // `redirect_url_loader_` now manages its own lifetime via a mojo channel. + // `handler` is guaranteed to be called. It will complete by serving the + // artificial redirect. + redirect_url_loader_.release(); + std::move(callback).Run(std::move(handler)); +} diff --git a/chrome/browser/ssl/https_upgrades_interceptor.h b/chrome/browser/ssl/https_upgrades_interceptor.h new file mode 100644 index 0000000000000..9c0e7cf0ab774 --- /dev/null +++ b/chrome/browser/ssl/https_upgrades_interceptor.h @@ -0,0 +1,75 @@ +// 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. + +#ifndef CHROME_BROWSER_SSL_HTTPS_UPGRADES_INTERCEPTOR_H_ +#define CHROME_BROWSER_SSL_HTTPS_UPGRADES_INTERCEPTOR_H_ + +#include + +#include "base/sequence_checker.h" +#include "chrome/browser/ssl/https_only_mode_tab_helper.h" +#include "chrome/browser/ssl/https_only_mode_upgrade_url_loader.h" +#include "content/public/browser/url_loader_request_interceptor.h" +#include "services/network/public/cpp/resource_request.h" + +namespace content { +class BrowserContext; +class WebContents; +} // namespace content + +// A class that attempts to intercept HTTP navigation requests and redirect them +// to HTTPS. Its lifetime matches that of the content/ navigation loader code. +class HttpsUpgradesInterceptor : public content::URLLoaderRequestInterceptor { + public: + explicit HttpsUpgradesInterceptor(int frame_tree_node_id); + ~HttpsUpgradesInterceptor() override; + + HttpsUpgradesInterceptor(const HttpsUpgradesInterceptor&) = delete; + HttpsUpgradesInterceptor& operator=(const HttpsUpgradesInterceptor&) = delete; + + // content::URLLoaderRequestInterceptor: + void MaybeCreateLoader( + const network::ResourceRequest& tentative_resource_request, + content::BrowserContext* browser_context, + content::URLLoaderRequestInterceptor::LoaderCallback callback) override; + + // Continuation of MaybeCreateLoader() after querying the network service for + // the HSTS status for the hostname in the request. + void MaybeCreateLoaderOnHstsQueryCompleted( + const network::ResourceRequest& tentative_resource_request, + content::URLLoaderRequestInterceptor::LoaderCallback callback, + HttpsOnlyModeTabHelper* tab_helper, + bool is_hsts_active_for_host); + + // Sets the ports used by the EmbeddedTestServer (which uses random ports) + // to determine the correct port to upgrade/fallback to in tests. + static void SetHttpsPortForTesting(int port); + static void SetHttpPortForTesting(int port); + static int GetHttpsPortForTesting(); + static int GetHttpPortForTesting(); + + private: + // Creates a URL loader that immediately serves a redirect to the HTTPS + // version of the URL. + void CreateHttpsRedirectLoader( + const network::ResourceRequest& tentative_resource_request, + content::URLLoaderRequestInterceptor::LoaderCallback callback); + + // Runs `callback` with `handler`. + void HandleRedirectLoader( + content::URLLoaderRequestInterceptor::LoaderCallback callback, + RequestHandler handler); + + // URLLoader that serves redirects. + std::unique_ptr redirect_url_loader_; + + // Used to access the WebContents for the navigation. + int frame_tree_node_id_; + + SEQUENCE_CHECKER(sequence_checker_); + + base::WeakPtrFactory weak_factory_{this}; +}; + +#endif // CHROME_BROWSER_SSL_HTTPS_ONLY_MODE_UPGRADE_INTERCEPTOR_H_ diff --git a/chrome/browser/ssl/https_upgrades_navigation_throttle.cc b/chrome/browser/ssl/https_upgrades_navigation_throttle.cc new file mode 100644 index 0000000000000..a425532ecd7e5 --- /dev/null +++ b/chrome/browser/ssl/https_upgrades_navigation_throttle.cc @@ -0,0 +1,223 @@ +// 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_navigation_throttle.h" + +#include "base/feature_list.h" +#include "base/metrics/histogram_functions.h" +#include "base/time/time.h" +#include "chrome/browser/ssl/https_only_mode_tab_helper.h" +#include "chrome/browser/ssl/https_upgrades_navigation_throttle.h" +#include "chrome/common/chrome_features.h" +#include "chrome/common/pref_names.h" +#include "components/prefs/pref_service.h" +#include "components/security_interstitials/content/security_interstitial_tab_helper.h" +#include "components/security_interstitials/core/https_only_mode_metrics.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/navigation_handle.h" +#include "content/public/browser/navigation_throttle.h" +#include "content/public/browser/web_contents.h" +#include "net/base/net_errors.h" + +using security_interstitials::https_only_mode::Event; + +namespace { + +// Time that the throttle will wait before canceling the upgraded navigation and +// showing the HTTPS-First Mode interstitial. +base::TimeDelta g_fallback_delay = base::Seconds(3); + +// Helper to record an HTTPS-First Mode navigation event. +void RecordHttpsFirstModeNavigation( + security_interstitials::https_only_mode::Event event) { + base::UmaHistogramEnumeration( + security_interstitials::https_only_mode::kEventHistogram, event); +} + +} // namespace + +// static +std::unique_ptr +HttpsUpgradesNavigationThrottle::MaybeCreateThrottleFor( + content::NavigationHandle* handle, + std::unique_ptr blocking_page_factory, + PrefService* prefs) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + // HTTPS-First Mode is only relevant for primary main-frame HTTP(S) + // navigations. + if (!handle->GetURL().SchemeIsHTTPOrHTTPS() || + !handle->IsInPrimaryMainFrame() || handle->IsSameDocument()) { + return nullptr; + } + + if (!base::FeatureList::IsEnabled(features::kHttpsOnlyMode) || !prefs || + !prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled)) { + return nullptr; + } + + // Ensure that the HttpsOnlyModeTabHelper has been created (this does nothing + // if it has already been created for the WebContents). There are cases where + // the tab helper won't get created by the initialization in + // chrome/browser/ui/tab_helpers.cc but the criteria for adding the throttle + // are still met (see crbug.com/1233889 for one example). + HttpsOnlyModeTabHelper::CreateForWebContents(handle->GetWebContents()); + + return std::make_unique( + handle, std::move(blocking_page_factory)); +} + +HttpsUpgradesNavigationThrottle::HttpsUpgradesNavigationThrottle( + content::NavigationHandle* handle, + std::unique_ptr blocking_page_factory) + : content::NavigationThrottle(handle), + blocking_page_factory_(std::move(blocking_page_factory)) {} + +HttpsUpgradesNavigationThrottle::~HttpsUpgradesNavigationThrottle() = default; + +content::NavigationThrottle::ThrottleCheckResult +HttpsUpgradesNavigationThrottle::WillStartRequest() { + // If the navigation is fallback to HTTP, trigger the interstitial. + auto* handle = navigation_handle(); + auto* contents = handle->GetWebContents(); + auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(contents); + if (tab_helper->is_navigation_fallback() && + !handle->GetURL().SchemeIsCryptographic()) { + std::unique_ptr + blocking_page = blocking_page_factory_->CreateHttpsOnlyModeBlockingPage( + contents, handle->GetURL()); + std::string interstitial_html = blocking_page->GetHTMLContents(); + security_interstitials::SecurityInterstitialTabHelper:: + AssociateBlockingPage(handle, std::move(blocking_page)); + return content::NavigationThrottle::ThrottleCheckResult( + content::NavigationThrottle::CANCEL, net::ERR_BLOCKED_BY_CLIENT, + interstitial_html); + } + + // Navigation is HTTPS or an initial HTTP navigation (which will get + // upgraded by the interceptor). + return content::NavigationThrottle::ThrottleAction::PROCEED; +} + +// Called if there is a non-OK net::Error in the completion status. +content::NavigationThrottle::ThrottleCheckResult +HttpsUpgradesNavigationThrottle::WillFailRequest() { + auto* handle = navigation_handle(); + + // If there was no certificate error, SSLInfo will be empty. + const net::SSLInfo info = handle->GetSSLInfo().value_or(net::SSLInfo()); + int cert_status = info.cert_status; + if (!net::IsCertStatusError(cert_status) && + handle->GetNetErrorCode() == net::OK) { + // Don't fallback. + return content::NavigationThrottle::PROCEED; + } + + // Only show the interstitial if the Interceptor attempted to upgrade the + // navigation. + auto* contents = handle->GetWebContents(); + auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(contents); + if (tab_helper->is_navigation_upgraded()) { + // Record failure type metrics for upgraded navigations. + RecordHttpsFirstModeNavigation(Event::kUpgradeFailed); + if (net::IsCertificateError(handle->GetNetErrorCode())) { + RecordHttpsFirstModeNavigation(Event::kUpgradeCertError); + } else if (handle->GetNetErrorCode() == net::ERR_TIMED_OUT) { + RecordHttpsFirstModeNavigation(Event::kUpgradeTimedOut); + } else { + RecordHttpsFirstModeNavigation(Event::kUpgradeNetError); + } + + // Mark the navigation as fallback and trigger a new navigation to the + // fallback URL. + tab_helper->set_is_navigation_fallback(true); + + // Copy the original navigation's params to the extent possible but update + // the URL to navigate to the fallback HTTP URL. + content::OpenURLParams params = + content::OpenURLParams::FromNavigationHandle(handle); + params.url = tab_helper->fallback_url(); + // Post a task to navigate to the fallback URL. We don't navigate + // synchronously here, as starting a navigation within a navigation is an + // antipattern. + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce( + [](base::WeakPtr web_contents, + const content::OpenURLParams& url_params) { + if (!web_contents) { + return; + } + web_contents->OpenURL(url_params); + }, + contents->GetWeakPtr(), std::move(params))); + return content::NavigationThrottle::CANCEL_AND_IGNORE; + } + + return content::NavigationThrottle::PROCEED; +} + +content::NavigationThrottle::ThrottleCheckResult +HttpsUpgradesNavigationThrottle::WillRedirectRequest() { + // If the navigation was upgraded by the Interceptor, then the Throttle's + // WillRedirectRequest() will get triggered by the artificial redirect to + // HTTPS. The HTTPS upgrade will always happen after the Throttle's + // WillStartRequest() (which only checks for fallback HTTP), so tracking + // upgraded requests is deferred to WillRedirectRequest() here. Which + // navigations to upgrade is determined by the Interceptor, not the Throttle. + // + // The navigation may get upgraded at various points during redirects: + // 1. The Interceptor serves an artificial redirect to HTTPS if the + // navigation is upgraded. This means the Throttle will see the upgraded + // navigation state for the first time here in WillRedirectRequest(). + // 2. HTTPS->HTTP downgrades can occur later in the lifecycle of a + // navigation, and will also result in the Interceptor serving an + // artificial redirect to upgrade the navigation. + // + // HTTPS->HTTP downgrades may result in net::ERR_TOO_MANY_REDIRECTS, but these + // redirect loops should hit the cache and not cost too much. If they go too + // long, the fallback timer will kick in. ERR_TOO_MANY_REDIRECTS should result + // in the request failing and triggering fallback. Alternately, the + // Interceptor could log URLs seen and bail if it encounters a redirect loop, + // but it is simpler to rely on existing handling unless the optimization is + // needed. + auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents( + navigation_handle()->GetWebContents()); + if (tab_helper->is_navigation_upgraded()) { + // Check if the timer is already started, as there may be additional + // redirects on the navigation after the artificial upgrade redirect. + bool timer_started = + navigation_handle()->SetNavigationTimeout(g_fallback_delay); + if (timer_started) { + RecordHttpsFirstModeNavigation(Event::kUpgradeAttempted); + } + } + + return content::NavigationThrottle::PROCEED; +} + +content::NavigationThrottle::ThrottleCheckResult +HttpsUpgradesNavigationThrottle::WillProcessResponse() { + // Clear the status for this navigation as it will successfully commit. + auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents( + navigation_handle()->GetWebContents()); + if (tab_helper->is_navigation_upgraded()) { + RecordHttpsFirstModeNavigation(Event::kUpgradeSucceeded); + tab_helper->set_is_navigation_upgraded(false); + } + + // Clear the fallback flag, if set. + tab_helper->set_is_navigation_fallback(false); + + return content::NavigationThrottle::PROCEED; +} + +const char* HttpsUpgradesNavigationThrottle::GetNameForLogging() { + return "HttpsUpgradesNavigationThrottle"; +} + +// static +void HttpsUpgradesNavigationThrottle::set_timeout_for_testing( + int timeout_in_seconds) { + g_fallback_delay = base::Seconds(timeout_in_seconds); +} diff --git a/chrome/browser/ssl/https_upgrades_navigation_throttle.h b/chrome/browser/ssl/https_upgrades_navigation_throttle.h new file mode 100644 index 0000000000000..e31f9f649daf9 --- /dev/null +++ b/chrome/browser/ssl/https_upgrades_navigation_throttle.h @@ -0,0 +1,59 @@ +// 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. + +#ifndef CHROME_BROWSER_SSL_HTTPS_UPGRADES_NAVIGATION_THROTTLE_H_ +#define CHROME_BROWSER_SSL_HTTPS_UPGRADES_NAVIGATION_THROTTLE_H_ + +#include + +#include "components/security_interstitials/content/security_blocking_page_factory.h" +#include "content/public/browser/navigation_throttle.h" + +class PrefService; + +// HttpsUpgradesNavigationThrottle is responsible for observing HTTPS-First Mode +// navigations that have been upgraded by HttpsUpgradesInterceptor, timing them +// out if they take too long, and handling failure by triggering fallback +// navigations to HTTP and triggering the HTTPS-First Mode interstitial. +// +// Metadata about the navigation state (as it pertains to HTTPS-First Mode) +// shared between HttpsUpgradesInterceptor and HttpsUpgradesNavigationThrottle +// is stored in an HttpsOnlyModeTabHelper set as user-data on the WebContents in +// which the navigation occurs. (Such metadata might ordinarily be added to +// ChromeNavigationUIData, but the Interceptor only receives a clone of the +// data, so it can't be used as a channel between these classes.) +class HttpsUpgradesNavigationThrottle : public content::NavigationThrottle { + public: + static std::unique_ptr + MaybeCreateThrottleFor( + content::NavigationHandle* handle, + std::unique_ptr blocking_page_factory, + PrefService* prefs); + + HttpsUpgradesNavigationThrottle( + content::NavigationHandle* handle, + std::unique_ptr blocking_page_factory); + ~HttpsUpgradesNavigationThrottle() override; + + HttpsUpgradesNavigationThrottle(const HttpsUpgradesNavigationThrottle&) = + delete; + HttpsUpgradesNavigationThrottle& operator=( + const HttpsUpgradesNavigationThrottle&) = delete; + + // content::NavigationThrottle: + content::NavigationThrottle::ThrottleCheckResult WillStartRequest() override; + content::NavigationThrottle::ThrottleCheckResult WillRedirectRequest() + override; + content::NavigationThrottle::ThrottleCheckResult WillFailRequest() override; + content::NavigationThrottle::ThrottleCheckResult WillProcessResponse() + override; + const char* GetNameForLogging() override; + + static void set_timeout_for_testing(int timeout_in_seconds); + + private: + std::unique_ptr blocking_page_factory_; +}; + +#endif // CHROME_BROWSER_SSL_HTTPS_UPGRADES_NAVIGATION_THROTTLE_H_ diff --git a/chrome/common/chrome_features.cc b/chrome/common/chrome_features.cc index afc98d10abcef..a8723f6faf731 100644 --- a/chrome/common/chrome_features.cc +++ b/chrome/common/chrome_features.cc @@ -632,6 +632,11 @@ BASE_FEATURE(kHideWebAppOriginText, // Sets whether the HTTPS-Only Mode setting is displayed in the settings UI. BASE_FEATURE(kHttpsOnlyMode, "HttpsOnlyMode", base::FEATURE_ENABLED_BY_DEFAULT); +// Enables the new implementation of HTTPS-First Mode. +BASE_FEATURE(kHttpsFirstModeV2, + "HttpsFirstModeV2", + base::FEATURE_DISABLED_BY_DEFAULT); + // Enables automatically upgrading main frame navigations to HTTPS. BASE_FEATURE(kHttpsUpgrades, "HttpsUpgrades", diff --git a/chrome/common/chrome_features.h b/chrome/common/chrome_features.h index 045fdc78cb4c0..09dd7cdd8c929 100644 --- a/chrome/common/chrome_features.h +++ b/chrome/common/chrome_features.h @@ -370,6 +370,7 @@ BASE_DECLARE_FEATURE(kHappinessTrackingGeneralCamera); COMPONENT_EXPORT(CHROME_FEATURES) BASE_DECLARE_FEATURE(kHideWebAppOriginText); COMPONENT_EXPORT(CHROME_FEATURES) BASE_DECLARE_FEATURE(kHttpsOnlyMode); +COMPONENT_EXPORT(CHROME_FEATURES) BASE_DECLARE_FEATURE(kHttpsFirstModeV2); COMPONENT_EXPORT(CHROME_FEATURES) BASE_DECLARE_FEATURE(kHttpsUpgrades); #if BUILDFLAG(IS_MAC) diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn index b10c79361012f..6322de08c60fe 100644 --- a/chrome/test/BUILD.gn +++ b/chrome/test/BUILD.gn @@ -2118,6 +2118,7 @@ if (!is_android) { "../browser/ssl/connection_help_tab_helper_browsertest.cc", "../browser/ssl/crlset_browsertest.cc", "../browser/ssl/https_only_mode_browsertest.cc", + "../browser/ssl/https_upgrades_browsertest.cc", "../browser/ssl/known_interception_disclosure_infobar_browsertest.cc", "../browser/ssl/known_interception_disclosure_ui_browsertest.cc", "../browser/ssl/ocsp_browsertest.cc", diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml index 3b29deaa9c4d8..efd947aadb7e5 100644 --- a/tools/metrics/histograms/enums.xml +++ b/tools/metrics/histograms/enums.xml @@ -59356,6 +59356,7 @@ from previous Chrome versions. + @@ -59893,6 +59894,7 @@ from previous Chrome versions. +