blob: c43fa233d821bcf45adc302d052163912c93f4c0 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <algorithm>
#include <vector>
#include "base/strings/stringprintf.h"
#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/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 "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;
class HttpsOnlyModeBrowserTest : public InProcessBrowserTest {
public:
HttpsOnlyModeBrowserTest() = default;
~HttpsOnlyModeBrowserTest() override = default;
void SetUp() override {
feature_list_.InitAndEnableFeature(features::kHttpsOnlyMode);
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<net::X509Certificate> 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();
}
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(HttpsOnlyModeBrowserTest,
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(HttpsOnlyModeBrowserTest,
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(HttpsOnlyModeBrowserTest, 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(HttpsOnlyModeBrowserTest,
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(HttpsOnlyModeBrowserTest,
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();
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
EXPECT_EQ(https_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(HttpsOnlyModeBrowserTest,
InterstitialBypassed_HttpFallbackLoaded) {
GURL http_url = http_server()->GetURL("bad-https.test", "/simple.html");
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
EXPECT_FALSE(content::NavigateToURL(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(HttpsOnlyModeBrowserTest,
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();
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
EXPECT_EQ(https_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(HttpsOnlyModeBrowserTest,
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(HttpsOnlyModeBrowserTest,
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.
// TODO(crbug.com/1218526): Update this to act on the interstitial once it is
// added.
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
EXPECT_FALSE(content::NavigateToURL(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(HttpsOnlyModeBrowserTest, SlowHttps_ShouldInterstitial) {
// Set timeout to zero so that HTTPS upgrades immediately timeout.
HttpsOnlyModeNavigationThrottle::set_timeout_for_testing(0);
const GURL url = http_server()->GetURL("foo.test", "/hung");
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
EXPECT_FALSE(content::NavigateToURL(contents, url));
EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(
contents));
}
// 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(HttpsOnlyModeBrowserTest, HttpPageHttpPost_NotUpgraded) {
// Point the HTTP form target to "bar.test".
base::StringPairs replacement_text;
replacement_text.push_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();
EXPECT_FALSE(content::NavigateToURL(
contents, http_server()->GetURL("bad-https.test", replacement_path)));
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(HttpsOnlyModeBrowserTest,
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(HttpsOnlyModeBrowserTest,
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<net::test_server::HttpResponse> {
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<net::test_server::BasicHttpResponse>();
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();
EXPECT_FALSE(content::NavigateToURL(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 (if no testing port is specified), the upgraded HTTPS version of
// an HTTP navigation will use the default HTTPS port 443.
IN_PROC_BROWSER_TEST_F(HttpsOnlyModeBrowserTest, HttpsUpgrade_DefaultPort) {
// Unset the custom testing port so that the redirect uses the default
// behavior of clearing the port.
HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting(0);
// Explicitly create an HTTP URL with the default port 80 specified.
GURL http_url = GURL("http://foo.test:80/");
// Navigate to the HTTP URL, which will get redirected to HTTPS/443.
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
content::TestNavigationObserver nav_observer(contents, 1);
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
nav_observer.Wait();
// Upgraded navigation should fail to connect as no test server is listening
// on port 443. The HTTPS-Only Mode interstitial should be showing.
EXPECT_FALSE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(url::kHttpsScheme, contents->GetLastCommittedURL().scheme());
EXPECT_EQ(443, contents->GetLastCommittedURL().EffectiveIntPort());
EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(
contents));
}
// Tests that (if no testing port is specified), the upgraded HTTPS version of
// an HTTP navigation with a non-default port will retain that non-default port.
IN_PROC_BROWSER_TEST_F(HttpsOnlyModeBrowserTest, HttpsUpgrade_NonDefaultPort) {
// Unset the custom testing port so that the redirect uses the default
// behavior of clearing the port.
HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting(0);
// Explicitly create an HTTP URL with a non-default port 8000 specified.
GURL http_url = GURL("http://foo.test:8000/");
// Navigate to the HTTP URL, which will get redirected to HTTPS/443.
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
content::TestNavigationObserver nav_observer(contents, 1);
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
nav_observer.Wait();
// Upgraded navigation should fail to connect as no test server is listening
// on port 8000. The HTTPS-Only Mode interstitial should be showing.
EXPECT_FALSE(nav_observer.last_navigation_succeeded());
EXPECT_EQ(url::kHttpsScheme, contents->GetLastCommittedURL().scheme());
EXPECT_EQ(8000, contents->GetLastCommittedURL().EffectiveIntPort());
EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(
contents));
}
// 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(HttpsOnlyModeBrowserTest,
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();
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
EXPECT_EQ(https_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(HttpsOnlyModeBrowserTest,
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();
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
EXPECT_EQ(https_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(HttpsOnlyModeBrowserTest,
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<net::test_server::HttpResponse> {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
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();
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
EXPECT_EQ(https_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(HttpsOnlyModeBrowserTest, 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();
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
EXPECT_EQ(https_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(HttpsOnlyModeBrowserTest, 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<StatefulSSLHostStateDelegate*>(
profile->GetSSLHostStateDelegate());
// First check that main frame requests revoke the decision.
// Navigate to `http_url`, which will get upgraded to `bad_https_url`.
EXPECT_FALSE(content::NavigateToURL(tab, http_url));
ASSERT_TRUE(
chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(tab));
ProceedThroughInterstitial(tab);
EXPECT_TRUE(state->HasAllowException(http_url.host(), tab));
EXPECT_TRUE(content::NavigateToURL(tab, good_https_url));
EXPECT_FALSE(state->HasAllowException(http_url.host(), tab));
// 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`.
EXPECT_FALSE(content::NavigateToURL(tab, http_url));
ASSERT_TRUE(
chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(tab));
ProceedThroughInterstitial(tab);
EXPECT_TRUE(state->HasAllowException(http_url.host(), tab));
// 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));
}
// 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(HttpsOnlyModeBrowserTest, 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();
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
EXPECT_EQ(https_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(HttpsOnlyModeBrowserTest, 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();
EXPECT_FALSE(content::NavigateToURL(contents, http_url));
EXPECT_EQ(https_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(HttpsOnlyModeBrowserTest, 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<base::SimpleTestClock>();
auto* clock_ptr = clock.get();
StatefulSSLHostStateDelegate* chrome_state =
static_cast<StatefulSSLHostStateDelegate*>(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");
EXPECT_FALSE(content::NavigateToURL(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));
// 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));
EXPECT_FALSE(content::NavigateToURL(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(HttpsOnlyModeBrowserTest, 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<base::SimpleTestClock>();
auto* clock_ptr = clock.get();
StatefulSSLHostStateDelegate* chrome_state =
static_cast<StatefulSSLHostStateDelegate*>(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");
EXPECT_FALSE(content::NavigateToURL(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));
// 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(content::NavigateToURL(contents, http_url));
EXPECT_FALSE(
chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(
contents));
}
// A simple test fixture that ensures the kHttpsOnlyMode feature is enabled and
// constructs a HistogramTester (so that it gets initialized before browser
// startup). Used for testing pref tracking logic.
class HttpsOnlyModePrefsBrowserTest : public InProcessBrowserTest {
public:
HttpsOnlyModePrefsBrowserTest() = default;
~HttpsOnlyModePrefsBrowserTest() override = default;
void SetUp() override {
feature_list_.InitAndEnableFeature(features::kHttpsOnlyMode);
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(HttpsOnlyModePrefsBrowserTest, 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(HttpsOnlyModePrefsBrowserTest, 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);
}