blob: c2f9c6476d70ff4b5e98c77376535ddf24454c67 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/interstitials/security_interstitial_page_test_utils.h"
#include "chrome/browser/preloading/prerender/prerender_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/security_interstitials/content/security_interstitial_page.h"
#include "components/security_interstitials/content/security_interstitial_tab_helper.h"
#include "components/security_interstitials/core/controller_client.h"
#include "content/public/browser/prerender_handle.h"
#include "content/public/browser/prerender_trigger_type.h"
#include "content/public/browser/reload_type.h"
#include "content/public/browser/ssl_host_state_delegate.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_interceptor.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/request_handler_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using chrome_browser_interstitials::IsShowingSSLInterstitial;
using content::EvalJs;
using content::ExecJs;
using content::RenderFrameHost;
using content::SSLHostStateDelegate;
using content::TestNavigationManager;
using content::URLLoaderInterceptor;
using content::WebContents;
using content::test::PrerenderHostObserver;
using net::EmbeddedTestServer;
using ui_test_utils::NavigateToURL;
namespace {
std::unique_ptr<net::EmbeddedTestServer> CreateExpiredCertServer(
const base::FilePath& data_dir) {
auto server =
std::make_unique<EmbeddedTestServer>(EmbeddedTestServer::TYPE_HTTPS);
server->AddDefaultHandlers(data_dir);
server->SetSSLConfig(EmbeddedTestServer::CERT_EXPIRED);
return server;
}
std::unique_ptr<net::EmbeddedTestServer> CreateHTTPSServer(
const base::FilePath& data_dir) {
auto server =
std::make_unique<EmbeddedTestServer>(EmbeddedTestServer::TYPE_HTTPS);
server->AddDefaultHandlers(data_dir);
server->SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
return server;
}
std::string GetFilePathWithHostAndPortReplacement(
const std::string& original_file_path,
const net::HostPortPair& host_port_pair) {
base::StringPairs replacement_text;
replacement_text.push_back(
make_pair("REPLACE_WITH_HOST_AND_PORT", host_port_pair.ToString()));
return net::test_server::GetFilePathWithReplacements(original_file_path,
replacement_text);
}
} // namespace
class SSLPrerenderTest : public InProcessBrowserTest {
public:
SSLPrerenderTest()
: prerender_helper_(base::BindRepeating(&SSLPrerenderTest::web_contents,
base::Unretained(this))) {}
~SSLPrerenderTest() override = default;
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
}
protected:
content::WebContents* web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
content::test::PrerenderTestHelper prerender_helper_;
};
class SecurityVisibleStateObserver : public content::WebContentsObserver {
public:
explicit SecurityVisibleStateObserver(WebContents& web_contents)
: WebContentsObserver(&web_contents) {}
void DidChangeVisibleSecurityState() override {
is_visible_state_changed_ = true;
}
bool is_visible_state_changed() const { return is_visible_state_changed_; }
private:
bool is_visible_state_changed_ = false;
};
// Verifies that a certificate error in a prerendered page causes cancelation
// of prerendering without showing an interstitial.
// TODO(bokan): In the future, when prerendering supports cross origin
// triggering, this test can be more straightforward by using one server for
// the initial page and another, with bad certs, for the prerendering page.
IN_PROC_BROWSER_TEST_F(SSLPrerenderTest, TestNoInterstitialInPrerender) {
auto server = CreateExpiredCertServer(GetChromeTestDataDir());
ASSERT_TRUE(server->Start());
const GURL kPrerenderUrl = server->GetURL("/empty.html?prerender");
const GURL kInitialUrl = server->GetURL("/empty.html");
// Use an interceptor to load the initial page. This is done because the
// server has certificate errors. If the initial URL is loaded from the test
// server, this will trigger an interstitial before the prerender can be
// triggered. Since the prerender must be same origin with the initial page,
// proceeding through that interstitial would add an exception for the URL,
// and so the error won't be visible to the prerender load. Since this test
// is trying to make sure that interstitials on prerender loads abort the
// prerender, this interceptor ensures the initial load won't have an
// interstitial, but the prerender will.
{
auto url_loader_interceptor =
content::URLLoaderInterceptor::ServeFilesFromDirectoryAtOrigin(
GetChromeTestDataDir().MaybeAsASCII(),
kInitialUrl.DeprecatedGetOriginAsURL());
// Navigate to the initial page.
ASSERT_TRUE(NavigateToURL(browser(), kInitialUrl));
ASSERT_FALSE(IsShowingSSLInterstitial(web_contents()));
// Make sure there is no exception for the prerendering URL, so that an SSL
// error will not be ignored.
Profile* profile =
Profile::FromBrowserContext(web_contents()->GetBrowserContext());
SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate();
ASSERT_FALSE(state->HasAllowException(
kPrerenderUrl.host(),
web_contents()->GetPrimaryMainFrame()->GetStoragePartition()));
}
// Trigger a prerender. Unlike the initial navigation, this will hit the
// server, so it'll respond with a bad certificate. If this request was a
// normal navigation, an interstitial would be shown, but because it is a
// prerender request, the prerender should be canceled and no interstitial
// shown.
{
TestNavigationManager observer(web_contents(), kPrerenderUrl);
// Trigger the prerender. The PrerenderHost starts the request when it is
// created so it should be available after WaitForRequestStart.
prerender_helper_.AddPrerenderAsync(kPrerenderUrl);
ASSERT_TRUE(observer.WaitForRequestStart());
ASSERT_NE(prerender_helper_.GetHostForUrl(kPrerenderUrl),
RenderFrameHost::kNoFrameTreeNodeId);
// The prerender navigation should be canceled as part of the response.
// Ensure the prerender host is destroyed and no interstitial is showing.
EXPECT_FALSE(observer.WaitForResponse());
EXPECT_EQ(prerender_helper_.GetHostForUrl(kPrerenderUrl),
RenderFrameHost::kNoFrameTreeNodeId);
EXPECT_FALSE(IsShowingSSLInterstitial(web_contents()));
}
}
// Verifies that a certificate error in a prerendered page fetched via service
// worker causes cancelation of prerendering without showing an interstitial.
// TODO(bokan): In the future, when prerendering supports cross origin
// triggering, this test can be more straightforward by using one server for
// the initial page and another, with bad certs, for the prerendering page.
// TODO(crbug.com/1464656): the test has been flaky across platforms.
IN_PROC_BROWSER_TEST_F(SSLPrerenderTest,
DISABLED_TestNoInterstitialInPrerenderSW) {
auto server = CreateExpiredCertServer(GetChromeTestDataDir());
ASSERT_TRUE(server->Start());
const GURL kPrerenderUrl = server->GetURL("/service_worker/blank.html");
const GURL kInitialUrl =
server->GetURL("/service_worker/create_service_worker.html");
// Use an interceptor to load the initial URL. This is done because the
// server has certificate errors. If the initial URL is loaded from the test
// server, this will trigger an interstitial before the prerender can be
// triggered. Since the prerender must be same origin with the initial page,
// proceeding through that interstitial would add an exception for the URL,
// and so the error won't be visible to the prerender load. Since this test
// is trying to make sure that interstitials on prerender loads abort the
// prerender, this interceptor ensures the initial load won't have an
// interstitial, but the prerender will.
{
auto url_loader_interceptor =
content::URLLoaderInterceptor::ServeFilesFromDirectoryAtOrigin(
GetChromeTestDataDir().MaybeAsASCII(),
kInitialUrl.DeprecatedGetOriginAsURL());
// Navigate to the initial page and register a service worker that will
// relay the fetch.
ASSERT_TRUE(NavigateToURL(browser(), kInitialUrl));
ASSERT_EQ("DONE", EvalJs(web_contents(),
"register('fetch_event_respond_with_fetch.js');"));
ASSERT_FALSE(IsShowingSSLInterstitial(web_contents()));
// Make sure there is no exception for the prerendering URL, so that an SSL
// error will not be ignored.
Profile* profile =
Profile::FromBrowserContext(web_contents()->GetBrowserContext());
SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate();
ASSERT_FALSE(state->HasAllowException(
kPrerenderUrl.host(),
web_contents()->GetPrimaryMainFrame()->GetStoragePartition()));
}
// Trigger a prerender. Unlike the initial navigation, this will hit the
// server, so it'll respond with a bad certificate. If this request was a
// normal navigation, an interstitial would be shown, but because it is a
// prerender request, the prerender should be canceled and no interstitial
// shown.
{
TestNavigationManager observer(web_contents(), kPrerenderUrl);
// Trigger the prerender. The PrerenderHost starts the request when it is
// created so it should be available after WaitForRequestStart.
prerender_helper_.AddPrerenderAsync(kPrerenderUrl);
ASSERT_TRUE(observer.WaitForRequestStart());
ASSERT_NE(prerender_helper_.GetHostForUrl(kPrerenderUrl),
RenderFrameHost::kNoFrameTreeNodeId);
// The prerender navigation should be canceled as part of the response.
// Ensure the prerender host is destroyed and no interstitial is showing.
EXPECT_FALSE(observer.WaitForResponse());
EXPECT_EQ(prerender_helper_.GetHostForUrl(kPrerenderUrl),
RenderFrameHost::kNoFrameTreeNodeId);
EXPECT_FALSE(IsShowingSSLInterstitial(web_contents()));
}
}
// Prerenders a page that tries to submit an insecure form and checks that this
// cancels the prerender instead.
IN_PROC_BROWSER_TEST_F(SSLPrerenderTest,
InsecureFormSubmissionCancelsPrerender) {
base::HistogramTester histograms;
const std::string kHistogramName =
"Security.MixedForm.InterstitialTriggerState";
// Histogram should start off empty.
histograms.ExpectTotalCount(kHistogramName, 0);
auto https_server = CreateHTTPSServer(GetChromeTestDataDir());
ASSERT_TRUE(https_server->Start());
// Add a "replace_text=" query param that the test server will use to replace
// the string "REPLACE_WITH_HOST_AND_PORT" in the destination page.
net::HostPortPair host_port_pair =
net::HostPortPair::FromURL(https_server->GetURL("a.test", "/"));
std::string replacement_path = GetFilePathWithHostAndPortReplacement(
"/ssl/page_with_form_targeting_http_url.html", host_port_pair);
// Use "a.test" since the default host is 127.0.0.1 and that's considered a
// "potentially trustworthy origin" by the throttle so navigation won't
// trigger the throttle.
const GURL kPrerenderUrl = https_server->GetURL("a.test", replacement_path);
const GURL kInitialUrl = https_server->GetURL("a.test", "/empty.html");
// Test steps
{
ASSERT_TRUE(NavigateToURL(browser(), kInitialUrl));
// Trigger the prerender.
const int kPrerenderHostId = prerender_helper_.AddPrerender(kPrerenderUrl);
ASSERT_NE(kPrerenderHostId, RenderFrameHost::kNoFrameTreeNodeId);
ASSERT_EQ(prerender_helper_.GetHostForUrl(kPrerenderUrl), kPrerenderHostId);
// Submit a form targeting an insecure URL. The prerender should be
// destroyed.
WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
PrerenderHostObserver host_observer(*tab, kPrerenderHostId);
ASSERT_TRUE(
ExecJs(prerender_helper_.GetPrerenderedMainFrameHost(kPrerenderHostId),
"document.getElementById('submit').click();"));
host_observer.WaitForDestroyed();
// The prerender navigation should be canceled as part of the response.
// Ensure the prerender host is destroyed, no interstitial is showing, and
// we didn't affect the relevant metric.
EXPECT_EQ(prerender_helper_.GetHostForUrl(kPrerenderUrl),
RenderFrameHost::kNoFrameTreeNodeId);
security_interstitials::SecurityInterstitialTabHelper* helper =
security_interstitials::SecurityInterstitialTabHelper::FromWebContents(
tab);
EXPECT_FALSE(helper);
histograms.ExpectTotalCount(kHistogramName, 0);
}
}
// Prerenders a page that tries to submit an insecure form and checks that this
// cancels the prerender even if the primary page is proceeding on an insecure
// form.
IN_PROC_BROWSER_TEST_F(SSLPrerenderTest,
InsecureFormSubmissionCancelsPrerenderEvenIfProceeding) {
base::HistogramTester histograms;
const std::string kHistogramName =
"Security.MixedForm.InterstitialTriggerState";
// Histogram should start off empty.
histograms.ExpectTotalCount(kHistogramName, 0);
auto https_server = CreateHTTPSServer(GetChromeTestDataDir());
ASSERT_TRUE(https_server->Start());
// Add a "replace_text=" query param that the test server will use to replace
// the string "REPLACE_WITH_HOST_AND_PORT" in the destination page.
net::HostPortPair host_port_pair =
net::HostPortPair::FromURL(https_server->GetURL("a.test", "/"));
std::string replacement_path = GetFilePathWithHostAndPortReplacement(
"/ssl/page_displays_insecure_form.html",
embedded_test_server()->host_port_pair());
// Use "a.test" since the default host is 127.0.0.1 and that's considered a
// "potentially trustworthy origin" by the throttle so navigation won't
// trigger the throttle.
const GURL kUrl = https_server->GetURL("a.test", replacement_path);
// Test steps
{
ASSERT_TRUE(NavigateToURL(browser(), kUrl));
// Submit a form targeting an insecure URL.
content::TestNavigationObserver nav_observer(web_contents(), 1);
ASSERT_TRUE(ExecJs(web_contents(), "submitForm();"));
nav_observer.Wait();
security_interstitials::SecurityInterstitialTabHelper* helper =
security_interstitials::SecurityInterstitialTabHelper::FromWebContents(
web_contents());
ASSERT_TRUE(helper);
EXPECT_TRUE(helper->IsDisplayingInterstitial());
histograms.ExpectTotalCount(kHistogramName, 1);
// Prerender the same insecure form.
std::unique_ptr<content::PrerenderHandle> prerender_handle =
web_contents()->StartPrerendering(
kUrl, content::PrerenderTriggerType::kEmbedder,
prerender_utils::kDirectUrlInputMetricSuffix,
ui::PageTransitionFromInt(ui::PAGE_TRANSITION_TYPED |
ui::PAGE_TRANSITION_FROM_ADDRESS_BAR),
content::PreloadingHoldbackStatus::kUnspecified, nullptr);
ASSERT_TRUE(prerender_handle);
const int kPrerenderHostId = prerender_helper_.GetHostForUrl(kUrl);
ASSERT_NE(kPrerenderHostId, content::RenderFrameHost::kNoFrameTreeNodeId);
prerender_helper_.WaitForPrerenderLoadCompletion(kPrerenderHostId);
// Proceed with the interstitial page in the primary page.
content::TestNavigationObserver nav_observer2(web_contents(), 1);
helper =
security_interstitials::SecurityInterstitialTabHelper::FromWebContents(
web_contents());
helper->GetBlockingPageForCurrentlyCommittedNavigationForTesting()
->CommandReceived(
base::NumberToString(security_interstitials::CMD_PROCEED));
nav_observer2.Wait();
ASSERT_TRUE(helper);
EXPECT_FALSE(helper->IsDisplayingInterstitial());
// Submit the prerendered form. The prerender should be destroyed.
PrerenderHostObserver host_observer(*web_contents(), kPrerenderHostId);
ASSERT_TRUE(
ExecJs(prerender_helper_.GetPrerenderedMainFrameHost(kPrerenderHostId),
"submitForm();"));
host_observer.WaitForDestroyed();
// The prerender navigation should be canceled as part of the response.
// Ensure the prerender host is destroyed, no interstitial is showing, and
// we didn't affect the relevant metric.
EXPECT_EQ(prerender_helper_.GetHostForUrl(kUrl),
RenderFrameHost::kNoFrameTreeNodeId);
helper =
security_interstitials::SecurityInterstitialTabHelper::FromWebContents(
web_contents());
ASSERT_TRUE(helper);
EXPECT_FALSE(helper->IsDisplayingInterstitial());
histograms.ExpectTotalCount(kHistogramName, 1);
}
}
IN_PROC_BROWSER_TEST_F(SSLPrerenderTest,
TestNoVisibleStateChangedOnInitialPrerendering) {
auto https_server = CreateHTTPSServer(GetChromeTestDataDir());
ASSERT_TRUE(https_server->Start());
const GURL kPrerenderUrl =
https_server->GetURL("a.test", "/empty.html?prerender");
const GURL kInitialUrl = https_server->GetURL("a.test", "/empty.html");
// Test steps
{
ASSERT_TRUE(NavigateToURL(browser(), kInitialUrl));
// Trigger the prerender.
content::TestActivationManager activation_manager(web_contents(),
kPrerenderUrl);
SecurityVisibleStateObserver visible_state_observer(*web_contents());
const int kPrerenderHostId = prerender_helper_.AddPrerender(kPrerenderUrl);
ASSERT_NE(kPrerenderHostId, RenderFrameHost::kNoFrameTreeNodeId);
ASSERT_EQ(prerender_helper_.GetHostForUrl(kPrerenderUrl), kPrerenderHostId);
ASSERT_FALSE(visible_state_observer.is_visible_state_changed());
// Activate.
ASSERT_TRUE(
content::ExecJs(web_contents()->GetPrimaryMainFrame(),
content::JsReplace("location = $1", kPrerenderUrl)));
activation_manager.WaitForNavigationFinished();
EXPECT_TRUE(activation_manager.was_activated());
EXPECT_TRUE(visible_state_observer.is_visible_state_changed());
}
}