blob: 33b37d7bd308d8b1994c9842c12c324e3ef82c23 [file] [log] [blame]
// Copyright 2015 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 "chrome/browser/safe_browsing/ui_manager.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/run_loop.h"
#include "base/values.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chrome/browser/password_manager/password_store_factory.h"
#include "chrome/browser/safe_browsing/chrome_safe_browsing_blocking_page_factory.h"
#include "chrome/browser/safe_browsing/safe_browsing_metrics_collector_factory.h"
#include "chrome/browser/safe_browsing/safe_browsing_navigation_observer_manager_factory.h"
#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
#include "chrome/browser/safe_browsing/ui_manager.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "components/password_manager/core/browser/mock_password_store.h"
#include "components/password_manager/core/browser/password_manager_test_utils.h"
#include "components/prefs/testing_pref_service.h"
#include "components/safe_browsing/content/browser/safe_browsing_blocking_page.h"
#include "components/safe_browsing/content/browser/safe_browsing_blocking_page_factory.h"
#include "components/safe_browsing/core/browser/db/util.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/security_interstitials/content/security_interstitial_controller_client.h"
#include "components/security_interstitials/content/unsafe_resource_util.h"
#include "components/security_interstitials/core/base_safe_browsing_error_ui.h"
#include "components/security_interstitials/core/unsafe_resource.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using content::BrowserThread;
static const char* kGoodURL = "https://www.good.com";
static const char* kBadURL = "https://www.malware.com";
static const char* kBadURLWithPath = "https://www.malware.com/index.html";
static const char* kAnotherBadURL = "https://www.badware.com";
static const char* kLandingURL = "https://www.landing.com";
namespace safe_browsing {
class SafeBrowsingCallbackWaiter {
public:
SafeBrowsingCallbackWaiter() {}
bool callback_called() const { return callback_called_; }
bool proceed() const { return proceed_; }
bool showed_interstitial() const { return showed_interstitial_; }
void OnBlockingPageDone(bool proceed, bool showed_interstitial) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
callback_called_ = true;
proceed_ = proceed;
showed_interstitial_ = showed_interstitial;
loop_.Quit();
}
void OnBlockingPageDoneOnIO(bool proceed, bool showed_interstitial) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
base::Unretained(this), proceed, showed_interstitial));
}
void WaitForCallback() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
loop_.Run();
}
private:
bool callback_called_ = false;
bool proceed_ = false;
bool showed_interstitial_ = false;
base::RunLoop loop_;
};
// A test blocking page that does not create windows.
class TestSafeBrowsingBlockingPage : public SafeBrowsingBlockingPage {
public:
TestSafeBrowsingBlockingPage(BaseUIManager* manager,
content::WebContents* web_contents,
const GURL& main_frame_url,
const UnsafeResourceList& unsafe_resources)
: SafeBrowsingBlockingPage(
manager,
web_contents,
main_frame_url,
unsafe_resources,
ChromeSafeBrowsingBlockingPageFactory::CreateControllerClient(
web_contents,
unsafe_resources,
manager),
BaseSafeBrowsingErrorUI::SBErrorDisplayOptions(
BaseBlockingPage::IsMainPageLoadBlocked(unsafe_resources),
false, // is_extended_reporting_opt_in_allowed
false, // is_off_the_record
false, // is_extended_reporting_enabled
false, // is_extended_reporting_policy_managed
false, // is_enhanced_protection_enabled
false, // is_proceed_anyway_disabled
true, // should_open_links_in_new_tab
true, // always_show_back_to_safety
false, // is_enhanced_protection_message_enabled
false, // is_safe_browsing_managed
"cpn_safe_browsing"), // help_center_article_link
true, // should_trigger_reporting
HistoryServiceFactory::GetForProfile(
Profile::FromBrowserContext(web_contents->GetBrowserContext()),
ServiceAccessType::EXPLICIT_ACCESS),
SafeBrowsingNavigationObserverManagerFactory::GetForBrowserContext(
web_contents->GetBrowserContext()),
SafeBrowsingMetricsCollectorFactory::GetForProfile(
Profile::FromBrowserContext(web_contents->GetBrowserContext())),
g_browser_process->safe_browsing_service()->trigger_manager()) {
// Don't delay details at all for the unittest.
SetThreatDetailsProceedDelayForTesting(0);
DontCreateViewForTesting();
}
};
// A factory that creates TestSafeBrowsingBlockingPages.
class TestSafeBrowsingBlockingPageFactory
: public SafeBrowsingBlockingPageFactory {
public:
TestSafeBrowsingBlockingPageFactory() = default;
~TestSafeBrowsingBlockingPageFactory() override = default;
SafeBrowsingBlockingPage* CreateSafeBrowsingPage(
BaseUIManager* delegate,
content::WebContents* web_contents,
const GURL& main_frame_url,
const SafeBrowsingBlockingPage::UnsafeResourceList& unsafe_resources,
bool should_trigger_reporting) override {
return new TestSafeBrowsingBlockingPage(delegate, web_contents,
main_frame_url, unsafe_resources);
}
};
class TestSafeBrowsingUIManagerDelegate
: public SafeBrowsingUIManager::Delegate {
public:
TestSafeBrowsingUIManagerDelegate() {
safe_browsing::RegisterProfilePrefs(pref_service_.registry());
}
~TestSafeBrowsingUIManagerDelegate() override = default;
// SafeBrowsingUIManager::Delegate:
const std::string& GetApplicationLocale() override { return app_locale_; }
void TriggerSecurityInterstitialShownExtensionEventIfDesired(
content::WebContents* web_contents,
const GURL& page_url,
const std::string& reason,
int net_error_code) override {}
void TriggerSecurityInterstitialProceededExtensionEventIfDesired(
content::WebContents* web_contents,
const GURL& page_url,
const std::string& reason,
int net_error_code) override {}
prerender::NoStatePrefetchContents* GetNoStatePrefetchContentsIfExists(
content::WebContents* web_contents) override {
return nullptr;
}
bool IsHostingExtension(content::WebContents* web_contents) override {
return is_hosting_extension_;
}
PrefService* GetPrefs(content::BrowserContext* browser_context) override {
return &pref_service_;
}
history::HistoryService* GetHistoryService(
content::BrowserContext* browser_context) override {
return nullptr;
}
bool IsMetricsAndCrashReportingEnabled() override { return false; }
void set_is_hosting_extension(bool is_hosting_extension) {
is_hosting_extension_ = is_hosting_extension;
}
private:
std::string app_locale_ = "en-us";
bool is_hosting_extension_ = false;
TestingPrefServiceSimple pref_service_;
};
class SafeBrowsingUIManagerTest : public ChromeRenderViewHostTestHarness {
public:
SafeBrowsingUIManagerTest()
: scoped_testing_local_state_(TestingBrowserProcess::GetGlobal()) {
auto ui_manager_delegate =
std::make_unique<TestSafeBrowsingUIManagerDelegate>();
raw_ui_manager_delegate_ = ui_manager_delegate.get();
ui_manager_ = new SafeBrowsingUIManager(
nullptr, std::move(ui_manager_delegate),
std::make_unique<TestSafeBrowsingBlockingPageFactory>(),
GURL("chrome://new-tab-page/"));
}
~SafeBrowsingUIManagerTest() override {}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
SafeBrowsingUIManager::CreateAllowlistForTesting(web_contents());
safe_browsing::TestSafeBrowsingServiceFactory sb_service_factory;
auto* safe_browsing_service =
sb_service_factory.CreateSafeBrowsingService();
TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(
safe_browsing_service);
g_browser_process->safe_browsing_service()->Initialize();
// A profile was created already but SafeBrowsingService wasn't around to
// get notified of it, so include that notification now.
safe_browsing_service->OnProfileAdded(
Profile::FromBrowserContext(web_contents()->GetBrowserContext()));
content::BrowserThread::RunAllPendingTasksOnThreadForTesting(
content::BrowserThread::IO);
PasswordStoreFactory::GetInstance()->SetTestingFactoryAndUse(
profile(),
base::BindRepeating(
&password_manager::BuildPasswordStore<
content::BrowserContext, password_manager::MockPasswordStore>));
}
void TearDown() override {
TestingBrowserProcess::GetGlobal()->safe_browsing_service()->ShutDown();
TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(nullptr);
// Depends on LocalState from ChromeRenderViewHostTestHarness.
if (SystemNetworkContextManager::GetInstance())
SystemNetworkContextManager::DeleteInstance();
ChromeRenderViewHostTestHarness::TearDown();
}
bool IsAllowlisted(security_interstitials::UnsafeResource resource) {
return ui_manager_->IsAllowlisted(resource);
}
void AddToAllowlist(security_interstitials::UnsafeResource resource) {
ui_manager_->AddToAllowlistUrlSet(
SafeBrowsingUIManager::GetMainFrameAllowlistUrlForResourceForTesting(
resource),
web_contents(), false, resource.threat_type);
}
security_interstitials::UnsafeResource MakeUnsafeResource(
const char* url,
bool is_subresource) {
security_interstitials::UnsafeResource resource;
resource.url = GURL(url);
resource.is_subresource = is_subresource;
resource.web_contents_getter = security_interstitials::GetWebContentsGetter(
web_contents()->GetMainFrame()->GetProcess()->GetID(),
web_contents()->GetMainFrame()->GetRoutingID());
resource.threat_type = SB_THREAT_TYPE_URL_MALWARE;
return resource;
}
security_interstitials::UnsafeResource MakeUnsafeResourceAndStartNavigation(
const char* url) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResource(url, false /* is_subresource */);
// The WC doesn't have a URL without a navigation. A main-frame malware
// unsafe resource must be a pending navigation.
auto navigation = content::NavigationSimulator::CreateBrowserInitiated(
GURL(url), web_contents());
navigation->Start();
return resource;
}
void SimulateBlockingPageDone(
const std::vector<security_interstitials::UnsafeResource>& resources,
bool proceed) {
GURL main_frame_url;
content::NavigationEntry* entry =
web_contents()->GetController().GetVisibleEntry();
if (entry)
main_frame_url = entry->GetURL();
ui_manager_->OnBlockingPageDone(resources, proceed, web_contents(),
main_frame_url,
true /* showed_interstitial */);
}
protected:
SafeBrowsingUIManager* ui_manager() { return ui_manager_.get(); }
TestSafeBrowsingUIManagerDelegate* ui_manager_delegate() {
return raw_ui_manager_delegate_;
}
private:
scoped_refptr<SafeBrowsingUIManager> ui_manager_;
TestSafeBrowsingUIManagerDelegate* raw_ui_manager_delegate_ = nullptr;
ScopedTestingLocalState scoped_testing_local_state_;
};
TEST_F(SafeBrowsingUIManagerTest, Allowlist) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kBadURL);
AddToAllowlist(resource);
EXPECT_TRUE(IsAllowlisted(resource));
}
TEST_F(SafeBrowsingUIManagerTest, AllowlistIgnoresSitesNotAdded) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kGoodURL);
EXPECT_FALSE(IsAllowlisted(resource));
}
TEST_F(SafeBrowsingUIManagerTest, AllowlistRemembersThreatType) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kBadURL);
AddToAllowlist(resource);
EXPECT_TRUE(IsAllowlisted(resource));
SBThreatType threat_type;
content::NavigationEntry* entry =
web_contents()->GetController().GetVisibleEntry();
ASSERT_TRUE(entry);
EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
resource.url, resource.is_subresource, entry,
resource.web_contents_getter.Run(), true, &threat_type));
EXPECT_EQ(resource.threat_type, threat_type);
}
TEST_F(SafeBrowsingUIManagerTest, AllowlistIgnoresPath) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kBadURL);
AddToAllowlist(resource);
EXPECT_TRUE(IsAllowlisted(resource));
content::WebContentsTester::For(web_contents())->CommitPendingNavigation();
security_interstitials::UnsafeResource resource_path =
MakeUnsafeResourceAndStartNavigation(kBadURLWithPath);
EXPECT_TRUE(IsAllowlisted(resource_path));
}
TEST_F(SafeBrowsingUIManagerTest, AllowlistIgnoresThreatType) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kBadURL);
AddToAllowlist(resource);
EXPECT_TRUE(IsAllowlisted(resource));
security_interstitials::UnsafeResource resource_phishing =
MakeUnsafeResource(kBadURL, false /* is_subresource */);
resource_phishing.threat_type = SB_THREAT_TYPE_URL_PHISHING;
EXPECT_TRUE(IsAllowlisted(resource_phishing));
}
TEST_F(SafeBrowsingUIManagerTest, AllowlistWithUnrelatedPendingLoad) {
// Commit load of landing page.
NavigateAndCommit(GURL(kLandingURL));
auto unrelated_navigation =
content::NavigationSimulator::CreateBrowserInitiated(GURL(kGoodURL),
web_contents());
{
// Simulate subresource malware hit on the landing page.
security_interstitials::UnsafeResource resource =
MakeUnsafeResource(kBadURL, true /* is_subresource */);
// Start pending load to unrelated site.
unrelated_navigation->Start();
// Allowlist the resource on the landing page.
AddToAllowlist(resource);
EXPECT_TRUE(IsAllowlisted(resource));
}
// Commit the pending load of unrelated site.
unrelated_navigation->Commit();
{
// The unrelated site is not on the allowlist, even if the same subresource
// was on it.
security_interstitials::UnsafeResource resource =
MakeUnsafeResource(kBadURL, true /* is_subresource */);
EXPECT_FALSE(IsAllowlisted(resource));
}
// Navigate back to the original landing url.
NavigateAndCommit(GURL(kLandingURL));
{
security_interstitials::UnsafeResource resource =
MakeUnsafeResource(kBadURL, true /* is_subresource */);
// Original resource url is allowlisted.
EXPECT_TRUE(IsAllowlisted(resource));
}
{
// A different malware subresource on the same page is also allowlisted.
// (The allowlist is by the page url, not the resource url.)
security_interstitials::UnsafeResource resource2 =
MakeUnsafeResource(kAnotherBadURL, true /* is_subresource */);
EXPECT_TRUE(IsAllowlisted(resource2));
}
}
TEST_F(SafeBrowsingUIManagerTest, UICallbackProceed) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kBadURL);
SafeBrowsingCallbackWaiter waiter;
resource.callback =
base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
base::Unretained(&waiter));
resource.callback_sequence = content::GetUIThreadTaskRunner({});
std::vector<security_interstitials::UnsafeResource> resources;
resources.push_back(resource);
SimulateBlockingPageDone(resources, true);
EXPECT_TRUE(IsAllowlisted(resource));
waiter.WaitForCallback();
EXPECT_TRUE(waiter.callback_called());
EXPECT_TRUE(waiter.proceed());
}
TEST_F(SafeBrowsingUIManagerTest, UICallbackDontProceed) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kBadURL);
SafeBrowsingCallbackWaiter waiter;
resource.callback =
base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
base::Unretained(&waiter));
resource.callback_sequence = content::GetUIThreadTaskRunner({});
std::vector<security_interstitials::UnsafeResource> resources;
resources.push_back(resource);
SimulateBlockingPageDone(resources, false);
EXPECT_FALSE(IsAllowlisted(resource));
waiter.WaitForCallback();
EXPECT_TRUE(waiter.callback_called());
EXPECT_FALSE(waiter.proceed());
}
TEST_F(SafeBrowsingUIManagerTest, IOCallbackProceed) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kBadURL);
SafeBrowsingCallbackWaiter waiter;
resource.callback =
base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDoneOnIO,
base::Unretained(&waiter));
resource.callback_sequence = content::GetIOThreadTaskRunner({});
std::vector<security_interstitials::UnsafeResource> resources;
resources.push_back(resource);
SimulateBlockingPageDone(resources, true);
EXPECT_TRUE(IsAllowlisted(resource));
waiter.WaitForCallback();
EXPECT_TRUE(waiter.callback_called());
EXPECT_TRUE(waiter.proceed());
}
TEST_F(SafeBrowsingUIManagerTest, IOCallbackDontProceed) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kBadURL);
SafeBrowsingCallbackWaiter waiter;
resource.callback =
base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDoneOnIO,
base::Unretained(&waiter));
resource.callback_sequence = content::GetIOThreadTaskRunner({});
std::vector<security_interstitials::UnsafeResource> resources;
resources.push_back(resource);
SimulateBlockingPageDone(resources, false);
EXPECT_FALSE(IsAllowlisted(resource));
waiter.WaitForCallback();
EXPECT_TRUE(waiter.callback_called());
EXPECT_FALSE(waiter.proceed());
}
namespace {
// A WebContentsDelegate that records whether
// VisibleSecurityStateChanged() was called.
class SecurityStateWebContentsDelegate : public content::WebContentsDelegate {
public:
SecurityStateWebContentsDelegate() {}
~SecurityStateWebContentsDelegate() override {}
bool visible_security_state_changed() const {
return visible_security_state_changed_;
}
void ClearVisibleSecurityStateChanged() {
visible_security_state_changed_ = false;
}
// WebContentsDelegate:
void VisibleSecurityStateChanged(content::WebContents* source) override {
visible_security_state_changed_ = true;
}
private:
bool visible_security_state_changed_ = false;
DISALLOW_COPY_AND_ASSIGN(SecurityStateWebContentsDelegate);
};
} // namespace
// Tests that the WebContentsDelegate is notified of a visible security
// state change when a blocking page is shown for a subresource.
TEST_F(SafeBrowsingUIManagerTest,
VisibleSecurityStateChangedForUnsafeSubresource) {
SecurityStateWebContentsDelegate delegate;
web_contents()->SetDelegate(&delegate);
// Simulate a blocking page showing for an unsafe subresource.
security_interstitials::UnsafeResource resource =
MakeUnsafeResource(kBadURL, true /* is_subresource */);
// Needed for showing the blocking page.
resource.threat_source = safe_browsing::ThreatSource::REMOTE;
NavigateAndCommit(GURL("http://example.test"));
delegate.ClearVisibleSecurityStateChanged();
EXPECT_FALSE(delegate.visible_security_state_changed());
ui_manager()->DisplayBlockingPage(resource);
EXPECT_TRUE(delegate.visible_security_state_changed());
// Simulate proceeding through the blocking page.
SafeBrowsingCallbackWaiter waiter;
resource.callback =
base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDoneOnIO,
base::Unretained(&waiter));
resource.callback_sequence = content::GetIOThreadTaskRunner({});
std::vector<security_interstitials::UnsafeResource> resources;
resources.push_back(resource);
delegate.ClearVisibleSecurityStateChanged();
EXPECT_FALSE(delegate.visible_security_state_changed());
SimulateBlockingPageDone(resources, true);
EXPECT_TRUE(delegate.visible_security_state_changed());
waiter.WaitForCallback();
EXPECT_TRUE(waiter.callback_called());
EXPECT_TRUE(waiter.proceed());
EXPECT_TRUE(IsAllowlisted(resource));
}
TEST_F(SafeBrowsingUIManagerTest, ShowBlockPageNoCallback) {
SecurityStateWebContentsDelegate delegate;
web_contents()->SetDelegate(&delegate);
// Simulate a blocking page showing for an unsafe subresource.
security_interstitials::UnsafeResource resource =
MakeUnsafeResource(kBadURL, false /* is_subresource */);
// Needed for showing the blocking page.
resource.threat_source = safe_browsing::ThreatSource::REMOTE;
// This call caused a crash in https://crbug.com/1058094. Just verify that we
// don't crash anymore.
ui_manager()->DisplayBlockingPage(resource);
}
TEST_F(SafeBrowsingUIManagerTest, NoInterstitialInExtensions) {
// Pretend the current web contents is in an extension.
ui_manager_delegate()->set_is_hosting_extension(true);
security_interstitials::UnsafeResource resource =
MakeUnsafeResource(kBadURL, false /* is_subresource */);
resource.web_contents_getter = security_interstitials::GetWebContentsGetter(
web_contents()->GetMainFrame()->GetProcess()->GetID(),
web_contents()->GetMainFrame()->GetRoutingID());
SafeBrowsingCallbackWaiter waiter;
resource.callback =
base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
base::Unretained(&waiter));
resource.callback_sequence = content::GetUIThreadTaskRunner({});
ui_manager()->StartDisplayingBlockingPage(resource);
waiter.WaitForCallback();
EXPECT_FALSE(waiter.proceed());
EXPECT_FALSE(waiter.showed_interstitial());
}
TEST_F(SafeBrowsingUIManagerTest, InvalidRenderFrameHostId) {
security_interstitials::UnsafeResource resource =
MakeUnsafeResourceAndStartNavigation(kBadURL);
resource.web_contents_getter = security_interstitials::GetWebContentsGetter(
web_contents()->GetMainFrame()->GetProcess()->GetID(), -1);
EXPECT_FALSE(IsAllowlisted(resource));
}
} // namespace safe_browsing