blob: 5ce3c1988f925092a85abbaa03a77216d1108171 [file] [log] [blame]
// Copyright (c) 2012 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/chrome_client_side_detection_host_delegate.h"
#include <memory>
#include <tuple>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/files/file_path.h"
#include "base/macros.h"
#include "base/memory/read_only_shared_memory_region.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/synchronization/waitable_event.h"
#include "base/test/gmock_move_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_command_line.h"
#include "base/test/simple_test_tick_clock.h"
#include "chrome/browser/safe_browsing/chrome_safe_browsing_blocking_page_factory.h"
#include "chrome/browser/safe_browsing/chrome_ui_manager_delegate.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/url_constants.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/safe_browsing/content/browser/client_side_detection_service.h"
#include "components/safe_browsing/content/browser/client_side_phishing_model.h"
#include "components/safe_browsing/content/browser/ui_manager.h"
#include "components/safe_browsing/content/common/safe_browsing.mojom-shared.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
#include "components/safe_browsing/core/browser/db/test_database_manager.h"
#include "components/safe_browsing/core/browser/sync/sync_utils.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/proto/csd.pb.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/security_interstitials/content/unsafe_resource_util.h"
#include "components/security_interstitials/core/unsafe_resource.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "content/public/browser/back_forward_cache.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/web_contents.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "ipc/ipc_test_sink.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using content::BrowserThread;
using content::RenderFrameHostTester;
using content::WebContents;
using ::testing::_;
using ::testing::DeleteArg;
using ::testing::DoAll;
using ::testing::Eq;
using ::testing::Invoke;
using ::testing::IsNull;
using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::NotNull;
using ::testing::Pointee;
using ::testing::Return;
using ::testing::ReturnRef;
using ::testing::SaveArg;
using ::testing::SetArgPointee;
using ::testing::StrictMock;
namespace {
const bool kFalse = false;
const bool kTrue = true;
std::unique_ptr<content::NavigationSimulator> NavigateAndKeepLoading(
content::WebContents* web_contents,
const GURL& url) {
auto navigation =
content::NavigationSimulator::CreateBrowserInitiated(url, web_contents);
navigation->SetKeepLoading(true);
navigation->Commit();
return navigation;
}
} // namespace
namespace safe_browsing {
namespace {
class MockSafeBrowsingTokenFetcher : public SafeBrowsingTokenFetcher {
public:
MockSafeBrowsingTokenFetcher() = default;
MockSafeBrowsingTokenFetcher(const MockSafeBrowsingTokenFetcher&) = delete;
MockSafeBrowsingTokenFetcher& operator=(const MockSafeBrowsingTokenFetcher&) =
delete;
~MockSafeBrowsingTokenFetcher() override = default;
MOCK_METHOD1(Start, void(Callback));
MOCK_METHOD1(OnInvalidAccessToken, void(const std::string&));
};
// This matcher verifies that the client computed verdict
// (ClientPhishingRequest) which is passed to SendClientReportPhishingRequest
// has the expected fields set. Note: we can't simply compare the protocol
// buffer strings because the BrowserFeatureExtractor might add features to the
// verdict object before calling SendClientReportPhishingRequest.
MATCHER_P(PartiallyEqualVerdict, other, "") {
return (other.url() == arg->url() &&
other.client_score() == arg->client_score() &&
other.is_phishing() == arg->is_phishing());
}
// Test that the callback is nullptr when the verdict is not phishing.
MATCHER(CallbackIsNull, "") {
return arg.is_null();
}
class MockClientSideDetectionService : public ClientSideDetectionService {
public:
MockClientSideDetectionService() : ClientSideDetectionService(nullptr) {}
MockClientSideDetectionService(const MockClientSideDetectionService&) =
delete;
MockClientSideDetectionService& operator=(
const MockClientSideDetectionService&) = delete;
~MockClientSideDetectionService() override {}
MOCK_METHOD3(SendClientReportPhishingRequest,
void(std::unique_ptr<ClientPhishingRequest>,
ClientReportPhishingRequestCallback,
const std::string&));
MOCK_CONST_METHOD1(IsPrivateIPAddress, bool(const net::IPAddress&));
MOCK_CONST_METHOD1(IsLocalResource, bool(const net::IPAddress&));
MOCK_METHOD2(GetValidCachedResult, bool(const GURL&, bool*));
MOCK_METHOD1(IsInCache, bool(const GURL&));
MOCK_METHOD0(OverPhishingReportLimit, bool());
MOCK_METHOD0(GetModelStr, std::string&());
MOCK_METHOD0(GetModelSharedMemoryRegion, base::ReadOnlySharedMemoryRegion());
MOCK_METHOD0(GetModelType, CSDModelType());
};
class MockSafeBrowsingUIManager : public SafeBrowsingUIManager {
public:
MockSafeBrowsingUIManager()
: SafeBrowsingUIManager(
std::make_unique<ChromeSafeBrowsingUIManagerDelegate>(),
std::make_unique<ChromeSafeBrowsingBlockingPageFactory>(),
GURL(chrome::kChromeUINewTabURL)) {}
MockSafeBrowsingUIManager(const MockSafeBrowsingUIManager&) = delete;
MockSafeBrowsingUIManager& operator=(const MockSafeBrowsingUIManager&) =
delete;
MOCK_METHOD1(DisplayBlockingPage, void(const UnsafeResource& resource));
// Helper function which calls OnBlockingPageComplete for this client
// object.
void InvokeOnBlockingPageComplete(
const security_interstitials::UnsafeResource::UrlCheckCallback& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Note: this will delete the client object in the case of the CsdClient
// implementation.
if (!callback.is_null())
callback.Run(false /*proceed*/, true /*showed_interstitial*/);
}
protected:
~MockSafeBrowsingUIManager() override {}
};
class MockSafeBrowsingDatabaseManager : public TestSafeBrowsingDatabaseManager {
public:
MockSafeBrowsingDatabaseManager()
: safe_browsing::TestSafeBrowsingDatabaseManager(
content::GetUIThreadTaskRunner({}),
content::GetIOThreadTaskRunner({})) {}
MockSafeBrowsingDatabaseManager(const MockSafeBrowsingDatabaseManager&) =
delete;
MockSafeBrowsingDatabaseManager& operator=(
const MockSafeBrowsingDatabaseManager&) = delete;
MOCK_METHOD2(CheckCsdAllowlistUrl,
AsyncMatch(const GURL&, SafeBrowsingDatabaseManager::Client*));
protected:
~MockSafeBrowsingDatabaseManager() override {}
};
} // namespace
class FakePhishingDetector : public mojom::PhishingDetector {
public:
FakePhishingDetector() = default;
FakePhishingDetector(const FakePhishingDetector&) = delete;
FakePhishingDetector& operator=(const FakePhishingDetector&) = delete;
~FakePhishingDetector() override = default;
void BindReceiver(mojo::ScopedMessagePipeHandle handle) {
receivers_.Add(this, mojo::PendingReceiver<mojom::PhishingDetector>(
std::move(handle)));
}
// mojom::PhishingDetector
void SetPhishingModel(const std::string& model, base::File file) override {
model_ = model;
}
// mojom::PhishingDetector
void SetPhishingFlatBufferModel(base::ReadOnlySharedMemoryRegion region,
base::File file) override {
region_ = std::move(region);
}
// mojom::PhishingDetector
void StartPhishingDetection(
const GURL& url,
StartPhishingDetectionCallback callback) override {
url_ = url;
phishing_detection_started_ = true;
// The callback must be run before destruction, so send a minimal
// ClientPhishingRequest.
ClientPhishingRequest request;
request.set_client_score(0.8);
std::move(callback).Run(mojom::PhishingDetectorResult::SUCCESS,
request.SerializeAsString());
return;
}
void CheckMessage(const GURL* url) {
if (!url) {
EXPECT_FALSE(phishing_detection_started_);
} else {
ASSERT_TRUE(phishing_detection_started_);
EXPECT_EQ(*url, url_);
}
}
void CheckModel(const std::string& model) { EXPECT_EQ(model, model_); }
void CheckModel(base::ReadOnlySharedMemoryRegion region) {
EXPECT_EQ(region.GetGUID(), region_.GetGUID());
}
void Reset() {
phishing_detection_started_ = false;
url_ = GURL();
model_ = "";
region_ = base::ReadOnlySharedMemoryRegion();
}
private:
mojo::ReceiverSet<mojom::PhishingDetector> receivers_;
bool phishing_detection_started_ = false;
GURL url_;
std::string model_ = "";
base::ReadOnlySharedMemoryRegion region_ = base::ReadOnlySharedMemoryRegion();
};
class ClientSideDetectionHostTestBase : public ChromeRenderViewHostTestHarness {
public:
typedef security_interstitials::UnsafeResource UnsafeResource;
class WebContentsObserver : public content::WebContentsObserver {
public:
WebContentsObserver(ClientSideDetectionHostTestBase* harness,
content::WebContents* contents)
: content::WebContentsObserver(contents), harness_(harness) {}
void RenderFrameCreated(
content::RenderFrameHost* render_frame_host) override {
harness_->InitTestApi(render_frame_host);
}
private:
// The raw pointer is safe because `harness_` owns this.
ClientSideDetectionHostTestBase* harness_;
};
explicit ClientSideDetectionHostTestBase(bool is_incognito)
: is_incognito_(is_incognito), model_str_("model_str") {}
void InitTestApi(content::RenderFrameHost* rfh) {
service_manager::InterfaceProvider* remote_interfaces =
rfh->GetRemoteInterfaces();
service_manager::InterfaceProvider::TestApi test_api(remote_interfaces);
test_api.SetBinderForName(
mojom::PhishingDetector::Name_,
base::BindRepeating(&FakePhishingDetector::BindReceiver,
base::Unretained(&fake_phishing_detector_)));
}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
observer_ = std::make_unique<WebContentsObserver>(this, web_contents());
if (is_incognito_) {
auto incognito_web_contents =
content::WebContentsTester::CreateTestWebContents(
profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true),
nullptr);
SetContents(std::move(incognito_web_contents));
}
// Initiate the connection to a (pretend) renderer process.
NavigateAndCommit(GURL("about:blank"));
InitTestApi(web_contents()->GetMainFrame());
// Inject service classes.
csd_service_ = std::make_unique<MockClientSideDetectionService>();
database_manager_ = new StrictMock<MockSafeBrowsingDatabaseManager>();
ui_manager_ = new StrictMock<MockSafeBrowsingUIManager>();
identity_test_env_.MakePrimaryAccountAvailable("user@gmail.com",
signin::ConsentLevel::kSync);
csd_host_ =
ChromeClientSideDetectionHostDelegate::CreateHost(web_contents());
csd_host_->set_client_side_detection_service(csd_service_.get());
csd_host_->set_ui_manager(ui_manager_.get());
csd_host_->set_database_manager(database_manager_.get());
csd_host_->set_tick_clock_for_testing(&clock_);
csd_host_->set_is_off_the_record_for_testing(is_incognito_);
csd_host_->set_account_signed_in_for_testing(
base::BindRepeating(&safe_browsing::SyncUtils::IsPrimaryAccountSignedIn,
identity_test_env_.identity_manager()));
auto token_fetcher =
std::make_unique<StrictMock<MockSafeBrowsingTokenFetcher>>();
raw_token_fetcher_ = token_fetcher.get();
csd_host_->set_token_fetcher_for_testing(std::move(token_fetcher));
testing::DefaultValue<CSDModelType>::Set(CSDModelType::kProtobuf);
}
void TearDown() override {
// Delete the host object on the UI thread and release the
// SafeBrowsingService.
content::GetUIThreadTaskRunner({})->DeleteSoon(FROM_HERE,
csd_host_.release());
database_manager_.reset();
ui_manager_.reset();
base::RunLoop().RunUntilIdle();
ChromeRenderViewHostTestHarness::TearDown();
}
void PhishingDetectionDone(const std::string& verdict_str) {
csd_host_->PhishingDetectionDone(mojom::PhishingDetectorResult::SUCCESS,
verdict_str);
}
void PhishingDetectionError(mojom::PhishingDetectorResult error) {
csd_host_->PhishingDetectionDone(error, "");
}
void ExpectPreClassificationChecks(const GURL& url,
const bool* is_private,
const bool* match_csd_allowlist,
const bool* get_valid_cached_result,
const bool* is_in_cache,
const bool* over_phishing_report_limit,
const bool* is_local) {
if (is_private) {
EXPECT_CALL(*csd_service_, IsPrivateIPAddress(_))
.WillOnce(Return(*is_private));
}
if (match_csd_allowlist) {
EXPECT_CALL(*database_manager_.get(), CheckCsdAllowlistUrl(url, _))
.WillOnce(Return(*match_csd_allowlist ? AsyncMatch::MATCH
: AsyncMatch::NO_MATCH));
}
if (get_valid_cached_result) {
EXPECT_CALL(*csd_service_, GetValidCachedResult(url, NotNull()))
.WillOnce(
DoAll(SetArgPointee<1>(true), Return(*get_valid_cached_result)));
}
if (is_in_cache) {
EXPECT_CALL(*csd_service_, IsInCache(url)).WillOnce(Return(*is_in_cache));
}
if (over_phishing_report_limit) {
EXPECT_CALL(*csd_service_, OverPhishingReportLimit())
.WillOnce(Return(*over_phishing_report_limit));
}
if (is_local) {
EXPECT_CALL(*csd_service_, IsLocalResource(_))
.WillOnce(Return(*is_local));
}
}
void WaitAndCheckPreClassificationChecks() {
// Wait for CheckCsdAllowlist and CheckCache() to be called if at all.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get()));
EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get()));
EXPECT_TRUE(Mock::VerifyAndClear(database_manager_.get()));
}
void NavigateAndCommit(const GURL& safe_url) {
controller().LoadURL(
safe_url, content::Referrer(), ui::PAGE_TRANSITION_LINK,
std::string());
content::WebContentsTester::For(web_contents())->CommitPendingNavigation();
}
void AdvanceTimeTickClock(base::TimeDelta delta) { clock_.Advance(delta); }
void SetFeatures(const std::vector<base::Feature>& enabled_features,
const std::vector<base::Feature>& disabled_features) {
feature_list_.InitWithFeatures(enabled_features, disabled_features);
}
protected:
std::unique_ptr<ClientSideDetectionHost> csd_host_;
std::unique_ptr<MockClientSideDetectionService> csd_service_;
scoped_refptr<StrictMock<MockSafeBrowsingUIManager> > ui_manager_;
scoped_refptr<StrictMock<MockSafeBrowsingDatabaseManager>> database_manager_;
FakePhishingDetector fake_phishing_detector_;
StrictMock<MockSafeBrowsingTokenFetcher>* raw_token_fetcher_ = nullptr;
base::SimpleTestTickClock clock_;
const bool is_incognito_;
signin::IdentityTestEnvironment identity_test_env_;
base::test::ScopedFeatureList feature_list_;
std::unique_ptr<WebContentsObserver> observer_;
std::string model_str_;
};
class ClientSideDetectionHostTest : public ClientSideDetectionHostTestBase {
public:
ClientSideDetectionHostTest()
: ClientSideDetectionHostTestBase(false /*is_incognito*/) {}
};
class ClientSideDetectionHostIncognitoTest
: public ClientSideDetectionHostTestBase {
public:
ClientSideDetectionHostIncognitoTest()
: ClientSideDetectionHostTestBase(true /*is_incognito*/) {}
};
TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneInvalidVerdict) {
// Case 0: renderer sends an invalid verdict string that we're unable to
// parse.
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _)).Times(0);
PhishingDetectionDone("Invalid Protocol Buffer");
EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get()));
}
TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneNotPhishing) {
// Case 1: client thinks the page is phishing. The server does not agree.
// No interstitial is shown.
ClientSideDetectionService::ClientReportPhishingRequestCallback cb;
ClientPhishingRequest verdict;
verdict.set_url("http://phishingurl.com/");
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(
PartiallyEqualVerdict(verdict), _, _))
.WillOnce(MoveArg<1>(&cb));
PhishingDetectionDone(verdict.SerializeAsString());
EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get()));
ASSERT_FALSE(cb.is_null());
// Make sure DisplayBlockingPage is not going to be called.
EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)).Times(0);
std::move(cb).Run(GURL(verdict.url()), false);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get()));
}
TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneDisabled) {
// Case 2: client thinks the page is phishing and so does the server but
// showing the interstitial is disabled => no interstitial is shown.
ClientSideDetectionService::ClientReportPhishingRequestCallback cb;
ClientPhishingRequest verdict;
verdict.set_url("http://phishingurl.com/");
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(
PartiallyEqualVerdict(verdict), _, _))
.WillOnce(MoveArg<1>(&cb));
PhishingDetectionDone(verdict.SerializeAsString());
EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get()));
ASSERT_FALSE(cb.is_null());
// Make sure DisplayBlockingPage is not going to be called.
EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_)).Times(0);
std::move(cb).Run(GURL(verdict.url()), false);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get()));
}
TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneShowInterstitial) {
// Case 3: client thinks the page is phishing and so does the server.
// We show an interstitial.
ClientSideDetectionService::ClientReportPhishingRequestCallback cb;
GURL phishing_url("http://phishingurl.com/");
ClientPhishingRequest verdict;
verdict.set_url(phishing_url.spec());
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(
PartiallyEqualVerdict(verdict), _, _))
.WillOnce(MoveArg<1>(&cb));
PhishingDetectionDone(verdict.SerializeAsString());
EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get()));
EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get()));
ASSERT_FALSE(cb.is_null());
UnsafeResource resource;
EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_))
.WillOnce(SaveArg<0>(&resource));
std::move(cb).Run(phishing_url, true);
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get()));
EXPECT_EQ(phishing_url, resource.url);
EXPECT_EQ(phishing_url, resource.original_url);
EXPECT_FALSE(resource.is_subresource);
EXPECT_EQ(SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING, resource.threat_type);
EXPECT_EQ(ThreatSource::CLIENT_SIDE_DETECTION, resource.threat_source);
EXPECT_EQ(web_contents(),
security_interstitials::GetWebContentsForResource(resource));
// Make sure the client object will be deleted.
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&MockSafeBrowsingUIManager::InvokeOnBlockingPageComplete,
ui_manager_, resource.callback));
}
TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneMultiplePings) {
// Case 4 & 5: client thinks a page is phishing then navigates to
// another page which is also considered phishing by the client
// before the server responds with a verdict. After a while the
// server responds for both requests with a phishing verdict. Only
// a single interstitial is shown for the second URL.
ClientSideDetectionService::ClientReportPhishingRequestCallback cb;
GURL phishing_url("http://phishingurl.com/");
ClientPhishingRequest verdict;
verdict.set_url(phishing_url.spec());
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(
PartiallyEqualVerdict(verdict), _, _))
.WillOnce(MoveArg<1>(&cb));
PhishingDetectionDone(verdict.SerializeAsString());
EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get()));
EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get()));
ASSERT_FALSE(cb.is_null());
GURL other_phishing_url("http://other_phishing_url.com/bla");
ExpectPreClassificationChecks(other_phishing_url, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse, &kFalse);
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
// We navigate away. The callback cb should be revoked.
NavigateAndCommit(other_phishing_url);
// Wait for the pre-classification checks to finish for other_phishing_url.
WaitAndCheckPreClassificationChecks();
ClientSideDetectionService::ClientReportPhishingRequestCallback cb_other;
verdict.set_url(other_phishing_url.spec());
verdict.set_client_score(0.8f);
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(
PartiallyEqualVerdict(verdict), _, _))
.WillOnce(MoveArg<1>(&cb_other));
PhishingDetectionDone(verdict.SerializeAsString());
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get()));
EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get()));
ASSERT_FALSE(cb_other.is_null());
// We expect that the interstitial is shown for the second phishing URL and
// not for the first phishing URL.
UnsafeResource resource;
EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_))
.WillOnce(SaveArg<0>(&resource));
std::move(cb).Run(phishing_url, true); // Should have no effect.
std::move(cb_other).Run(other_phishing_url,
true); // Should show interstitial.
base::RunLoop().RunUntilIdle();
EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get()));
EXPECT_EQ(other_phishing_url, resource.url);
EXPECT_EQ(other_phishing_url, resource.original_url);
EXPECT_FALSE(resource.is_subresource);
EXPECT_EQ(SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING, resource.threat_type);
EXPECT_EQ(ThreatSource::CLIENT_SIDE_DETECTION, resource.threat_source);
EXPECT_EQ(web_contents(),
security_interstitials::GetWebContentsForResource(resource));
// Make sure the client object will be deleted.
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&MockSafeBrowsingUIManager::InvokeOnBlockingPageComplete,
ui_manager_, resource.callback));
}
TEST_F(ClientSideDetectionHostTest, PhishingDetectionDoneVerdictNotPhishing) {
// Case 6: renderer sends a verdict string that isn't phishing.
ClientPhishingRequest verdict;
verdict.set_url("http://not-phishing.com/");
verdict.set_client_score(0.1f);
verdict.set_is_phishing(false);
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _)).Times(0);
PhishingDetectionDone(verdict.SerializeAsString());
EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get()));
}
TEST_F(ClientSideDetectionHostTest,
PhishingDetectionDoneVerdictNotPhishingButSBMatchSubResource) {
// Case 7: renderer sends a verdict string that isn't phishing but the URL
// of a subresource was on the regular phishing or malware lists.
GURL url("http://not-phishing.com/");
ClientPhishingRequest verdict;
verdict.set_url(url.spec());
verdict.set_client_score(0.1f);
verdict.set_is_phishing(false);
// First we have to navigate to the URL to set the unique page ID.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndCommit(url);
WaitAndCheckPreClassificationChecks();
PhishingDetectionDone(verdict.SerializeAsString());
}
TEST_F(ClientSideDetectionHostTest,
PhishingDetectionDoneVerdictNotPhishingButSBMatchOnNewRVH) {
// When navigating to a different host (thus creating a pending RVH) which
// matches regular malware list, and after navigation the renderer sends a
// verdict string that isn't phishing, we should still send the report.
// Do an initial navigation to a safe host.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL start_url("http://safe.example.com/");
ExpectPreClassificationChecks(start_url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndCommit(start_url);
WaitAndCheckPreClassificationChecks();
// Now navigate to a different host which will have a malware hit before the
// navigation commits.
GURL url("http://malware-but-not-phishing.com/");
ClientPhishingRequest verdict;
verdict.set_url(url.spec());
verdict.set_client_score(0.1f);
verdict.set_is_phishing(false);
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndCommit(url);
WaitAndCheckPreClassificationChecks();
PhishingDetectionDone(verdict.SerializeAsString());
}
TEST_F(
ClientSideDetectionHostTest,
PhishingDetectionDoneVerdictNotPhishingButSBMatchOnSubresourceWhileNavPending) {
// When a malware hit happens on a committed page while a slow pending load is
// in progress, the csd report should be sent for the committed page.
// Do an initial navigation to a safe host.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL start_url("http://safe.example.com/");
ExpectPreClassificationChecks(start_url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndCommit(start_url);
WaitAndCheckPreClassificationChecks();
// Now navigate to a different host which does not have a SB hit.
GURL url("http://not-malware-not-phishing-but-malware-subresource.com/");
ClientPhishingRequest verdict;
verdict.set_url(url.spec());
verdict.set_client_score(0.1f);
verdict.set_is_phishing(false);
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndCommit(url);
// Create a pending navigation, but don't commit it.
GURL pending_url("http://slow.example.com/");
auto pending_navigation =
content::NavigationSimulator::CreateBrowserInitiated(pending_url,
web_contents());
pending_navigation->Start();
WaitAndCheckPreClassificationChecks();
PhishingDetectionDone(verdict.SerializeAsString());
}
TEST_F(ClientSideDetectionHostTest,
PhishingDetectionDoneEnhancedProtectionShouldHaveToken) {
SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true);
SetFeatures(
/*enable_features*/ {kClientSideDetectionWithToken},
/*disable_features*/ {});
ClientPhishingRequest verdict;
verdict.set_url("http://example.com/");
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
// Set up mock call to csd service.
EXPECT_CALL(*csd_service_,
SendClientReportPhishingRequest(PartiallyEqualVerdict(verdict), _,
"fake_access_token"));
// Set up mock call to token fetcher.
SafeBrowsingTokenFetcher::Callback cb;
EXPECT_CALL(*raw_token_fetcher_, Start(_)).WillOnce(MoveArg<0>(&cb));
// Make the call.
PhishingDetectionDone(verdict.SerializeAsString());
// Wait for token fetcher to be called.
EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_));
ASSERT_FALSE(cb.is_null());
std::move(cb).Run("fake_access_token");
}
TEST_F(ClientSideDetectionHostTest,
PhishingDetectionDoneCalledTwiceShouldSucceed) {
SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true);
SetFeatures(
/*enable_features*/ {kClientSideDetectionWithToken},
/*disable_features*/ {});
ClientPhishingRequest verdict;
verdict.set_url("http://example.com/");
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
// Set up mock call to csd service.
EXPECT_CALL(*csd_service_,
SendClientReportPhishingRequest(PartiallyEqualVerdict(verdict), _,
"fake_access_token_1"))
.Times(1);
// Set up mock call to csd service.
EXPECT_CALL(*csd_service_,
SendClientReportPhishingRequest(PartiallyEqualVerdict(verdict), _,
"fake_access_token_2"))
.Times(1);
// Set up mock call to token fetcher.
SafeBrowsingTokenFetcher::Callback cb;
EXPECT_CALL(*raw_token_fetcher_, Start(_))
.Times(1)
.WillRepeatedly(MoveArg<0>(&cb));
// Make the call.
PhishingDetectionDone(verdict.SerializeAsString());
// Wait for token fetcher to be called.
EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_));
ASSERT_FALSE(cb.is_null());
std::move(cb).Run("fake_access_token_1");
// Make the call again.
EXPECT_CALL(*raw_token_fetcher_, Start(_))
.Times(1)
.WillRepeatedly(MoveArg<0>(&cb));
PhishingDetectionDone(verdict.SerializeAsString());
EXPECT_TRUE(Mock::VerifyAndClear(raw_token_fetcher_));
ASSERT_FALSE(cb.is_null());
std::move(cb).Run("fake_access_token_2");
}
TEST_F(ClientSideDetectionHostIncognitoTest,
PhishingDetectionDoneIncognitoShouldNotHaveToken) {
SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true);
SetFeatures(
/*enable_features*/ {kClientSideDetectionWithToken},
/*disable_features*/ {});
ClientPhishingRequest verdict;
verdict.set_url("http://example.com/");
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
// Set up mock call to csd service.
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(
PartiallyEqualVerdict(verdict), _, ""));
// Set up mock call to token fetcher.
SafeBrowsingTokenFetcher::Callback cb;
EXPECT_CALL(*raw_token_fetcher_, Start(_)).Times(0);
// Make the call.
PhishingDetectionDone(verdict.SerializeAsString());
}
TEST_F(ClientSideDetectionHostTest,
PhishingDetectionDoneNoEnhancedProtectionShouldNotHaveToken) {
SetFeatures(/*enable_features*/ {},
/*disable_features*/ {kClientSideDetectionWithToken});
ClientPhishingRequest verdict;
verdict.set_url("http://example.com/");
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
// Set up mock call to csd service.
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(
PartiallyEqualVerdict(verdict), _, ""));
// Set up mock call to token fetcher.
SafeBrowsingTokenFetcher::Callback cb;
EXPECT_CALL(*raw_token_fetcher_, Start(_)).Times(0);
// Make the call.
PhishingDetectionDone(verdict.SerializeAsString());
}
TEST_F(ClientSideDetectionHostTest,
PhishingDetectionDoneDisabledFeatureShouldNotHaveToken) {
SetEnhancedProtectionPrefForTests(profile()->GetPrefs(), true);
SetFeatures(/*enable_features*/ {},
/*disable_features*/ {kClientSideDetectionWithToken});
ClientPhishingRequest verdict;
verdict.set_url("http://example.com/");
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
// Set up mock call to csd service.
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(
PartiallyEqualVerdict(verdict), _, ""));
// Set up mock call to token fetcher.
SafeBrowsingTokenFetcher::Callback cb;
EXPECT_CALL(*raw_token_fetcher_, Start(_)).Times(0);
// Make the call.
PhishingDetectionDone(verdict.SerializeAsString());
}
// This test doesn't work because it makes assumption about how
// the message loop is run, and those assumptions are wrong when properly
// simulating a navigation with browser-side navigations.
// TODO(clamy): Fix the test and re-enable. See crbug.com/753357.
TEST_F(ClientSideDetectionHostTest,
DISABLED_NavigationCancelsShouldClassifyUrl) {
// Test that canceling pending should classify requests works as expected.
GURL first_url("http://first.phishy.url.com");
GURL second_url("http://second.url.com/");
// The first few checks are done synchronously so check that they have been
// done for the first URL, while the second URL has all the checks done. We
// need to manually set up the IsPrivateIPAddress mock since if the same mock
// expectation is specified twice, gmock will only use the last instance of
// it, meaning the first will never be matched.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
EXPECT_CALL(*csd_service_, IsPrivateIPAddress(_))
.WillOnce(Return(false))
.WillOnce(Return(false));
ExpectPreClassificationChecks(first_url, nullptr, &kFalse, nullptr, nullptr,
nullptr, nullptr);
ExpectPreClassificationChecks(second_url, nullptr, &kFalse, &kFalse, &kFalse,
&kFalse, nullptr);
NavigateAndCommit(first_url);
// Don't flush the message loop, as we want to navigate to a different
// url before the final pre-classification checks are run.
NavigateAndCommit(second_url);
WaitAndCheckPreClassificationChecks();
}
TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckPass) {
// Navigate the tab to a page. We should see a StartPhishingDetection IPC.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host.com/");
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url);
}
TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckMatchAllowlist) {
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host.com/");
ExpectPreClassificationChecks(url, &kFalse, &kTrue, nullptr, nullptr, nullptr,
&kFalse);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
}
TEST_F(ClientSideDetectionHostTest,
TestPreClassificationCheckSameDocumentNavigation) {
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host.com/");
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url);
fake_phishing_detector_.Reset();
// Now try an same-document navigation. This should not trigger an IPC.
EXPECT_CALL(*csd_service_, IsPrivateIPAddress(_)).Times(0);
GURL inpage("http://host.com/#foo");
ExpectPreClassificationChecks(inpage, nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr);
NavigateAndKeepLoading(web_contents(), inpage);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(nullptr);
}
TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckXHTML) {
// Check that XHTML is supported, in addition to the default HTML type.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host.com/xhtml");
auto navigation =
content::NavigationSimulator::CreateBrowserInitiated(url, web_contents());
navigation->SetContentsMimeType("application/xhtml+xml");
navigation->SetKeepLoading(true);
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
navigation->Commit();
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url);
}
TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckTwoNavigations) {
// Navigate to two hosts, which should cause two IPCs.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url1("http://host1.com/");
ExpectPreClassificationChecks(url1, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndKeepLoading(web_contents(), url1);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url1);
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url2("http://host2.com/");
ExpectPreClassificationChecks(url2, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndKeepLoading(web_contents(), url2);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url2);
}
TEST_F(ClientSideDetectionHostTest,
TestPreClassificationCheckPrivateIpAddress) {
// If IsPrivateIPAddress returns true, no IPC should be triggered.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host3.com/");
ExpectPreClassificationChecks(url, &kTrue, nullptr, nullptr, nullptr, nullptr,
nullptr);
NavigateAndCommit(url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(nullptr);
}
TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckLocalResource) {
// If IsLocalResource returns true, no IPC should be triggered.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host3.com/");
ExpectPreClassificationChecks(url, nullptr, nullptr, nullptr, nullptr,
nullptr, &kTrue);
NavigateAndCommit(url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(nullptr);
}
TEST_F(ClientSideDetectionHostIncognitoTest,
TestPreClassificationCheckIncognito) {
// If the tab is incognito there should be no IPC. Also, we shouldn't
// even check the csd-allowlist.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host4.com/");
ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr,
nullptr, &kFalse);
content::WebContentsTester::For(web_contents())->NavigateAndCommit(url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(nullptr);
}
TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckInvalidCache) {
// If item is in the cache but it isn't valid, we will classify regardless
// of whether we are over the reporting limit.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host6.com/");
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kTrue, nullptr,
&kFalse);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url);
}
TEST_F(ClientSideDetectionHostTest,
TestPreClassificationCheckOverPhishingReportingLimit) {
// If the url isn't in the cache and we are over the reporting limit, we
// don't do classification.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host7.com/");
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, &kTrue,
&kFalse);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(nullptr);
}
TEST_F(ClientSideDetectionHostTest,
TestPreClassificationCheckOverBothReportingLimits) {
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host.com/");
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, &kTrue,
&kFalse);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(nullptr);
}
TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckHttpsUrl) {
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("https://host.com/");
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url);
}
TEST_F(ClientSideDetectionHostTest,
TestPreClassificationCheckNoneHttpOrHttpsUrl) {
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("file://host.com/");
ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr,
nullptr, &kFalse);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(nullptr);
}
TEST_F(ClientSideDetectionHostTest, TestPreClassificationCheckValidCached) {
// If result is cached, we will try and display the blocking page directly
// with no start classification message.
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host8.com/");
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kTrue, &kFalse, &kFalse,
&kFalse);
UnsafeResource resource;
EXPECT_CALL(*ui_manager_.get(), DisplayBlockingPage(_))
.WillOnce(SaveArg<0>(&resource));
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
EXPECT_EQ(url, resource.url);
EXPECT_EQ(url, resource.original_url);
fake_phishing_detector_.CheckMessage(nullptr);
}
TEST_F(ClientSideDetectionHostTest, TestPreClassificationAllowlistedByPolicy) {
// Configures enterprise allowlist.
ListPrefUpdate update(profile()->GetPrefs(),
prefs::kSafeBrowsingAllowlistDomains);
update->Append("example.com");
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://example.com/");
ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr,
nullptr, &kFalse);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(nullptr);
}
TEST_F(ClientSideDetectionHostTest, RecordsPhishingDetectorResults) {
{
ClientPhishingRequest verdict;
verdict.set_url("http://not-phishing.com/");
verdict.set_client_score(0.1f);
verdict.set_is_phishing(false);
base::HistogramTester histogram_tester;
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _))
.Times(0);
PhishingDetectionDone(verdict.SerializeAsString());
EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get()));
histogram_tester.ExpectUniqueSample(
"SBClientPhishing.PhishingDetectorResult",
mojom::PhishingDetectorResult::SUCCESS, 1);
}
{
base::HistogramTester histogram_tester;
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _))
.Times(0);
PhishingDetectionError(mojom::PhishingDetectorResult::CLASSIFIER_NOT_READY);
EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get()));
histogram_tester.ExpectUniqueSample(
"SBClientPhishing.PhishingDetectorResult",
mojom::PhishingDetectorResult::CLASSIFIER_NOT_READY, 1);
}
{
base::HistogramTester histogram_tester;
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _))
.Times(0);
PhishingDetectionError(
mojom::PhishingDetectorResult::FORWARD_BACK_TRANSITION);
EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get()));
histogram_tester.ExpectUniqueSample(
"SBClientPhishing.PhishingDetectorResult",
mojom::PhishingDetectorResult::FORWARD_BACK_TRANSITION, 1);
}
}
TEST_F(ClientSideDetectionHostTest, RecordsPhishingDetectionDuration) {
base::HistogramTester histogram_tester;
histogram_tester.ExpectTotalCount(
"SBClientPhishing.PhishingDetectionDuration", 0);
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL start_url("http://safe.example.com/");
ExpectPreClassificationChecks(start_url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndCommit(start_url);
WaitAndCheckPreClassificationChecks();
histogram_tester.ExpectTotalCount(
"SBClientPhishing.PhishingDetectionDuration", 1);
GURL url("http://phishing.example.com/");
ClientPhishingRequest verdict;
verdict.set_url(url.spec());
verdict.set_client_score(0.1f);
verdict.set_is_phishing(false);
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse,
&kFalse, &kFalse);
NavigateAndCommit(url);
WaitAndCheckPreClassificationChecks();
const base::TimeDelta duration = base::Milliseconds(10);
AdvanceTimeTickClock(duration);
PhishingDetectionDone(verdict.SerializeAsString());
histogram_tester.ExpectTotalCount(
"SBClientPhishing.PhishingDetectionDuration", 3);
EXPECT_LE(duration.InMilliseconds(),
histogram_tester
.GetAllSamples("SBClientPhishing.PhishingDetectionDuration")
.front()
.min);
}
TEST_F(ClientSideDetectionHostTest, TestSendFlatBufferModelToRenderFrame) {
base::MappedReadOnlyRegion mapped_region =
base::ReadOnlySharedMemoryRegion::Create(10);
EXPECT_CALL(*csd_service_, GetModelType())
.WillRepeatedly(Return(CSDModelType::kFlatbuffer));
EXPECT_CALL(*csd_service_, GetModelSharedMemoryRegion())
.WillRepeatedly(
Return(testing::ByMove(mapped_region.region.Duplicate())));
csd_host_->SendModelToRenderFrame();
base::RunLoop().RunUntilIdle();
fake_phishing_detector_.CheckModel(mapped_region.region.Duplicate());
fake_phishing_detector_.Reset();
}
TEST_F(ClientSideDetectionHostTest, TestSendModelToRenderFrame) {
std::string stardard("standard");
EXPECT_CALL(*csd_service_, GetModelStr()).WillRepeatedly(ReturnRef(stardard));
csd_host_->SendModelToRenderFrame();
base::RunLoop().RunUntilIdle();
fake_phishing_detector_.CheckModel("standard");
fake_phishing_detector_.Reset();
}
TEST_F(ClientSideDetectionHostTest, ClearsScreenshotData) {
profile()->GetPrefs()->SetBoolean(prefs::kSafeBrowsingScoutReportingEnabled,
false);
ClientPhishingRequest verdict;
verdict.set_url("http://phishingurl.com/");
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
verdict.set_screenshot_digest("screenshot_digest");
verdict.set_screenshot_phash("screenshot_phash");
verdict.set_phash_dimension_size(48);
ClientPhishingRequest request;
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _))
.WillOnce(testing::SaveArgPointee<0>(&request));
PhishingDetectionDone(verdict.SerializeAsString());
EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get()));
EXPECT_FALSE(request.has_phash_dimension_size());
EXPECT_FALSE(request.has_screenshot_phash());
EXPECT_FALSE(request.has_screenshot_digest());
}
TEST_F(ClientSideDetectionHostTest, AllowsScreenshotDataForSBER) {
profile()->GetPrefs()->SetBoolean(prefs::kSafeBrowsingScoutReportingEnabled,
true);
ClientPhishingRequest verdict;
verdict.set_url("http://phishingurl.com/");
verdict.set_client_score(1.0f);
verdict.set_is_phishing(true);
verdict.set_screenshot_digest("screenshot_digest");
verdict.set_screenshot_phash("screenshot_phash");
verdict.set_phash_dimension_size(48);
ClientPhishingRequest request;
EXPECT_CALL(*csd_service_, SendClientReportPhishingRequest(_, _, _))
.WillOnce(testing::SaveArgPointee<0>(&request));
PhishingDetectionDone(verdict.SerializeAsString());
EXPECT_TRUE(Mock::VerifyAndClear(csd_host_.get()));
EXPECT_TRUE(request.has_phash_dimension_size());
EXPECT_TRUE(request.has_screenshot_phash());
EXPECT_TRUE(request.has_screenshot_digest());
}
class ClientSideDetectionHostDebugFeaturesTest
: public ClientSideDetectionHostTest {
public:
ClientSideDetectionHostDebugFeaturesTest() = default;
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
command_line_.GetProcessCommandLine()->AppendSwitchPath(
"--csd-debug-feature-directory", temp_dir_.GetPath());
ClientSideDetectionHostTest::SetUp();
}
private:
base::ScopedTempDir temp_dir_;
base::test::ScopedCommandLine command_line_;
};
TEST_F(ClientSideDetectionHostDebugFeaturesTest,
SkipsAllowlistWhenDumpingFeatures) {
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host.com/");
ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr,
nullptr, &kFalse);
EXPECT_CALL(*database_manager_.get(), CheckCsdAllowlistUrl(url, _)).Times(0);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url);
}
TEST_F(ClientSideDetectionHostDebugFeaturesTest,
SkipsCacheWhenDumpingFeatures) {
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host.com/");
ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr,
nullptr, &kFalse);
EXPECT_CALL(*csd_service_, GetValidCachedResult(url, NotNull())).Times(0);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url);
}
TEST_F(ClientSideDetectionHostDebugFeaturesTest,
SkipsReportLimitWhenDumpingFeatures) {
EXPECT_CALL(*csd_service_, GetModelStr())
.WillRepeatedly(ReturnRef(model_str_));
GURL url("http://host.com/");
ExpectPreClassificationChecks(url, &kFalse, nullptr, nullptr, nullptr,
nullptr, &kFalse);
EXPECT_CALL(*csd_service_, OverPhishingReportLimit()).Times(0);
NavigateAndKeepLoading(web_contents(), url);
WaitAndCheckPreClassificationChecks();
fake_phishing_detector_.CheckMessage(&url);
}
} // namespace safe_browsing