| // 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 "base/test/bind.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/ui/browser.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 "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 "url/url_constants.h" |
| |
| 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 { |
| InProcessBrowserTest::SetUpCommandLine(command_line); |
| 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); |
| } |
| |
| net::EmbeddedTestServer* http_server() { return &http_server_; } |
| net::EmbeddedTestServer* https_server() { return &https_server_; } |
| |
| 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_; |
| }; |
| |
| // 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.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("foo.com", "/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)); |
| } |
| |
| // 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.com", "/simple.html"); |
| auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_TRUE(content::NavigateToURL(contents, https_url)); |
| } |
| |
| // 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)); |
| } |
| |
| // 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()); |
| |
| // TODO(crbug.com/1218526): Update this to use the actual interstitial once it |
| // is added. |
| EXPECT_TRUE(chrome_browser_interstitials::IsInterstitialDisplayingText( |
| contents->GetMainFrame(), "HTTPS-Only Mode")); |
| } |
| |
| // If the user triggers an HTTPS-Only Mode interstitial for a host and then they |
| // then navigate to the HTTP URL again the navigation should end up on that HTTP |
| // URL and no interstitial should be shown. |
| // TODO(crbug.com/1218526): Update this to be an interstitial bypass test. |
| 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::IsInterstitialDisplayingText( |
| contents->GetMainFrame(), "HTTPS-Only Mode")); |
| |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| } |
| |
| // 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.com", "/close-socket"); |
| GURL https_url = https_server()->GetURL("foo.com", "/close-socket"); |
| |
| auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| |
| // TODO(crbug.com/1218526): Update this to use the actual interstitial once it |
| // is added. |
| EXPECT_TRUE(chrome_browser_interstitials::IsInterstitialDisplayingText( |
| contents->GetMainFrame(), "HTTPS-Only Mode")); |
| } |
| |
| // 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.com", "/iframe_blank.html")); |
| const GURL iframe_url(http_server()->GetURL("foo.com", "/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()); |
| } |
| |
| // 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.com", "/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)); |
| |
| // TODO(crbug.com/1218526): Update this to use the actual interstitial once it |
| // is added. |
| EXPECT_TRUE(chrome_browser_interstitials::IsInterstitialDisplayingText( |
| contents->GetMainFrame(), "HTTPS-Only Mode")); |
| // For now, the host is added to the allowlist automatically on showing the |
| // error page. Repeat the navigation and it will succeed. |
| EXPECT_TRUE(content::NavigateToURL(contents, parent_url)); |
| |
| // Navigate the iframe to `iframe_url`. It should successfully navigate and |
| // not get upgraded to HTTPS. |
| 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()); |
| } |
| |
| // 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.com", "/hung"); |
| auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_FALSE(content::NavigateToURL(contents, url)); |
| |
| // TODO(crbug.com/1218526): Update this to use the actual interstitial once it |
| // is added. |
| EXPECT_TRUE(chrome_browser_interstitials::IsInterstitialDisplayingText( |
| contents->GetMainFrame(), "HTTPS-Only Mode")); |
| } |
| |
| // Tests that an HTTP POST form navigation to "bar.com" from an HTTP page on |
| // "foo.com" 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.com". |
| base::StringPairs replacement_text; |
| replacement_text.push_back(make_pair( |
| "REPLACE_WITH_HOST_AND_PORT", |
| net::HostPortPair::FromURL(http_server()->GetURL("foo.com", "/")) |
| .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.com". 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))); |
| // TODO(crbug.com/1218526): Update this to use the actual interstitial once it |
| // is added. |
| EXPECT_TRUE(chrome_browser_interstitials::IsInterstitialDisplayingText( |
| contents->GetMainFrame(), "HTTPS-Only Mode")); |
| |
| // Navigate again to bypass the interstitial. |
| EXPECT_TRUE(content::NavigateToURL( |
| contents, http_server()->GetURL("bad-https.test", replacement_path))); |
| |
| // 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.com", contents->GetLastCommittedURL().host()); |
| EXPECT_TRUE(contents->GetLastCommittedURL().SchemeIs(url::kHttpScheme)); |
| } |
| |
| // 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.com", "/title1.html"); |
| GURL url = https_server()->GetURL("foo.com", |
| "/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.com", contents->GetLastCommittedURL().host()); |
| } |
| |
| // 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 just swapping the scheme. 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_scheme; |
| http_scheme.SetSchemeStr(url::kHttpScheme); |
| auto redirect_url = request.GetURL().ReplaceComponents(http_scheme); |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_TEMPORARY_REDIRECT); |
| response->AddCustomHeader("Location", redirect_url.spec()); |
| return response; |
| })); |
| HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting( |
| downgrading_server.port()); |
| ASSERT_TRUE(downgrading_server.Start()); |
| |
| GURL url = downgrading_server.GetURL("foo.com", "/"); |
| auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_FALSE(content::NavigateToURL(contents, url)); |
| |
| // TODO(crbug.com/1218526): Update this to use the actual interstitial once it |
| // is added. |
| EXPECT_TRUE(chrome_browser_interstitials::IsInterstitialDisplayingText( |
| contents->GetMainFrame(), "HTTPS-Only Mode")); |
| } |
| |
| // 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); |
| |
| GURL http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| |
| // 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()); |
| // TODO(crbug.com/1218526): Update this to use the actual interstitial once it |
| // is added. |
| EXPECT_TRUE(chrome_browser_interstitials::IsInterstitialDisplayingText( |
| contents->GetMainFrame(), "HTTPS-Only Mode")); |
| } |