blob: 03783b5a39cc50707ba2c0a90208b3fb75ddda94 [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/renderer/safe_browsing/phishing_classifier_delegate.h"
#include <memory>
#include "base/strings/utf_string_conversions.h"
#include "chrome/renderer/safe_browsing/features.h"
#include "chrome/renderer/safe_browsing/phishing_classifier.h"
#include "chrome/renderer/safe_browsing/scorer.h"
#include "chrome/test/base/chrome_render_view_test.h"
#include "chrome/test/base/chrome_unit_test_suite.h"
#include "components/safe_browsing/common/safebrowsing_messages.h"
#include "components/safe_browsing/csd.pb.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_view.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/WebKit/public/platform/WebURL.h"
#include "third_party/WebKit/public/platform/WebURLRequest.h"
#include "url/gurl.h"
using base::ASCIIToUTF16;
using ::testing::_;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::Pointee;
using ::testing::StrictMock;
namespace safe_browsing {
namespace {
class MockPhishingClassifier : public PhishingClassifier {
public:
explicit MockPhishingClassifier(content::RenderFrame* render_frame)
: PhishingClassifier(render_frame, NULL /* clock */) {}
virtual ~MockPhishingClassifier() {}
MOCK_METHOD2(BeginClassification,
void(const base::string16*, const DoneCallback&));
MOCK_METHOD0(CancelPendingClassification, void());
private:
DISALLOW_COPY_AND_ASSIGN(MockPhishingClassifier);
};
class MockScorer : public Scorer {
public:
MockScorer() : Scorer() {}
virtual ~MockScorer() {}
MOCK_CONST_METHOD1(ComputeScore, double(const FeatureMap&));
private:
DISALLOW_COPY_AND_ASSIGN(MockScorer);
};
} // namespace
class FakeRenderThread : public ChromeMockRenderThread {
public:
// Instead of sending this message, we verify its content here.
bool Send(IPC::Message* msg) override {
// handle and verify message here.
IPC_BEGIN_MESSAGE_MAP(FakeRenderThread, *msg)
IPC_MESSAGE_HANDLER(SafeBrowsingHostMsg_PhishingDetectionDone,
VerifyMessageContent)
IPC_END_MESSAGE_MAP()
if (msg) { // Prevent memory leak.
delete msg;
msg = nullptr;
}
// Return true anyway, since we don't want to block other IPC.
return true;
}
void VerifyMessageContent(const std::string& verdict_str) {
ClientPhishingRequest verdict;
if (verdict.ParseFromString(verdict_str)) {
EXPECT_EQ("http://host.com/", verdict.url());
EXPECT_EQ(0.8f, verdict.client_score());
EXPECT_FALSE(verdict.is_phishing());
} else {
NOTREACHED() << "Cannot parse IPC content. Test failed.";
}
}
};
class PhishingClassifierDelegateTest : public ChromeRenderViewTest {
protected:
void SetUp() override {
ChromeUnitTestSuite::InitializeProviders();
ChromeUnitTestSuite::InitializeResourceBundle();
// Plug-in FakeRenderThread.
chrome_render_thread_ = new FakeRenderThread();
render_thread_.reset(chrome_render_thread_);
content::RenderViewTest::SetUp();
content::RenderFrame* render_frame = view_->GetMainRenderFrame();
classifier_ = new StrictMock<MockPhishingClassifier>(render_frame);
delegate_ = PhishingClassifierDelegate::Create(render_frame, classifier_);
}
// Runs the ClassificationDone callback, then verify if message sent
// by FakeRenderThread is correct.
void RunAndVerifyClassificationDone(const ClientPhishingRequest& verdict) {
delegate_->ClassificationDone(verdict);
}
void OnStartPhishingDetection(const GURL& url) {
delegate_->OnStartPhishingDetection(url);
}
void PageCaptured(base::string16* page_text,
bool preliminary_capture,
const GURL& page_url) {
if (preliminary_capture)
return;
delegate_->CancelPendingClassification(
PhishingClassifierDelegate::PAGE_RECAPTURED);
delegate_->last_finished_load_url_ = page_url;
delegate_->classifier_page_text_.swap(*page_text);
delegate_->have_page_text_ = true;
delegate_->MaybeStartClassification();
}
void SimulateRedirection(const GURL& redir_url) {
delegate_->last_url_received_from_browser_ = redir_url;
}
void SimulatePageTrantitionForwardOrBack(const char* html, const char* url) {
LoadHTMLWithUrlOverride(html, url);
delegate_->last_main_frame_transition_ = ui::PAGE_TRANSITION_FORWARD_BACK;
}
StrictMock<MockPhishingClassifier>* classifier_; // Owned by |delegate_|.
PhishingClassifierDelegate* delegate_; // Owned by the RenderFrame.
};
TEST_F(PhishingClassifierDelegateTest, Navigation) {
MockScorer scorer;
delegate_->SetPhishingScorer(&scorer);
ASSERT_TRUE(classifier_->is_ready());
// Test an initial load. We expect classification to happen normally.
EXPECT_CALL(*classifier_, CancelPendingClassification());
std::string html = "<html><body>dummy</body></html>";
GURL url("http://host.com/index.html");
LoadHTMLWithUrlOverride(html.c_str(), url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
OnStartPhishingDetection(url);
base::string16 page_text = ASCIIToUTF16("dummy");
{
InSequence s;
EXPECT_CALL(*classifier_, CancelPendingClassification());
EXPECT_CALL(*classifier_, BeginClassification(Pointee(page_text), _));
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
}
// Reloading the same page should not trigger a reclassification.
// However, it will cancel any pending classification since the
// content is being replaced.
EXPECT_CALL(*classifier_, CancelPendingClassification());
LoadHTMLWithUrlOverride(html.c_str(), url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
OnStartPhishingDetection(url);
page_text = ASCIIToUTF16("dummy");
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
OnStartPhishingDetection(url);
page_text = ASCIIToUTF16("dummy");
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
// Same document navigation works similarly to a subframe navigation, but see
// the TODO in PhishingClassifierDelegate::DidCommitProvisionalLoad.
EXPECT_CALL(*classifier_, CancelPendingClassification());
OnSameDocumentNavigation(GetMainFrame(), true, true);
Mock::VerifyAndClearExpectations(classifier_);
OnStartPhishingDetection(url);
page_text = ASCIIToUTF16("dummy");
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
// Now load a new toplevel page, which should trigger another classification.
EXPECT_CALL(*classifier_, CancelPendingClassification());
GURL new_url("http://host2.com");
LoadHTMLWithUrlOverride("dummy2", new_url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
page_text = ASCIIToUTF16("dummy2");
OnStartPhishingDetection(new_url);
{
InSequence s;
EXPECT_CALL(*classifier_, CancelPendingClassification());
EXPECT_CALL(*classifier_, BeginClassification(Pointee(page_text), _));
PageCaptured(&page_text, false, new_url);
Mock::VerifyAndClearExpectations(classifier_);
}
// No classification should happen on back/forward navigation.
// Note: in practice, the browser will not send a StartPhishingDetection IPC
// in this case. However, we want to make sure that the delegate behaves
// correctly regardless.
EXPECT_CALL(*classifier_, CancelPendingClassification()).Times(1);
// Simulate a go back navigation, i.e. back to http://host.com/index.html.
SimulatePageTrantitionForwardOrBack(html.c_str(), url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
page_text = ASCIIToUTF16("dummy");
OnStartPhishingDetection(new_url);
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, new_url);
Mock::VerifyAndClearExpectations(classifier_);
EXPECT_CALL(*classifier_, CancelPendingClassification());
// Simulate a go forward navigation, i.e. forward to http://host.com
SimulatePageTrantitionForwardOrBack("dummy2", new_url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
page_text = ASCIIToUTF16("dummy2");
OnStartPhishingDetection(url);
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
// Now go back again and navigate to a different place within
// the same page. No classification should happen.
EXPECT_CALL(*classifier_, CancelPendingClassification());
// Simulate a go back again to http://host.com/index.html
SimulatePageTrantitionForwardOrBack(html.c_str(), url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
page_text = ASCIIToUTF16("dummy");
OnStartPhishingDetection(url);
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
EXPECT_CALL(*classifier_, CancelPendingClassification());
// Same document navigation.
OnSameDocumentNavigation(GetMainFrame(), true, true);
Mock::VerifyAndClearExpectations(classifier_);
OnStartPhishingDetection(url);
page_text = ASCIIToUTF16("dummy");
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
// The delegate will cancel pending classification on destruction.
EXPECT_CALL(*classifier_, CancelPendingClassification());
}
TEST_F(PhishingClassifierDelegateTest, NoScorer) {
// For this test, we'll create the delegate with no scorer available yet.
ASSERT_FALSE(classifier_->is_ready());
// Queue up a pending classification, cancel it, then queue up another one.
GURL url("http://host.com");
base::string16 page_text = ASCIIToUTF16("dummy");
LoadHTMLWithUrlOverride("dummy", url.spec().c_str());
OnStartPhishingDetection(url);
PageCaptured(&page_text, false, url);
GURL url2("http://host2.com");
page_text = ASCIIToUTF16("dummy");
LoadHTMLWithUrlOverride("dummy", url2.spec().c_str());
OnStartPhishingDetection(url2);
PageCaptured(&page_text, false, url2);
// Now set a scorer, which should cause a classifier to be created and
// the classification to proceed.
page_text = ASCIIToUTF16("dummy");
EXPECT_CALL(*classifier_, BeginClassification(Pointee(page_text), _));
MockScorer scorer;
delegate_->SetPhishingScorer(&scorer);
Mock::VerifyAndClearExpectations(classifier_);
// If we set a new scorer while a classification is going on the
// classification should be cancelled.
EXPECT_CALL(*classifier_, CancelPendingClassification());
delegate_->SetPhishingScorer(&scorer);
Mock::VerifyAndClearExpectations(classifier_);
// The delegate will cancel pending classification on destruction.
EXPECT_CALL(*classifier_, CancelPendingClassification());
}
TEST_F(PhishingClassifierDelegateTest, NoScorer_Ref) {
// Similar to the last test, but navigates within the page before
// setting the scorer.
ASSERT_FALSE(classifier_->is_ready());
// Queue up a pending classification, cancel it, then queue up another one.
GURL url("http://host.com");
base::string16 page_text = ASCIIToUTF16("dummy");
LoadHTMLWithUrlOverride("dummy", url.spec().c_str());
OnStartPhishingDetection(url);
PageCaptured(&page_text, false, url);
page_text = ASCIIToUTF16("dummy");
OnStartPhishingDetection(url);
PageCaptured(&page_text, false, url);
// Now set a scorer, which should cause a classifier to be created and
// the classification to proceed.
page_text = ASCIIToUTF16("dummy");
EXPECT_CALL(*classifier_, BeginClassification(Pointee(page_text), _));
MockScorer scorer;
delegate_->SetPhishingScorer(&scorer);
Mock::VerifyAndClearExpectations(classifier_);
// If we set a new scorer while a classification is going on the
// classification should be cancelled.
EXPECT_CALL(*classifier_, CancelPendingClassification());
delegate_->SetPhishingScorer(&scorer);
Mock::VerifyAndClearExpectations(classifier_);
// The delegate will cancel pending classification on destruction.
EXPECT_CALL(*classifier_, CancelPendingClassification());
}
TEST_F(PhishingClassifierDelegateTest, NoStartPhishingDetection) {
// Tests the behavior when OnStartPhishingDetection has not yet been called
// when the page load finishes.
MockScorer scorer;
delegate_->SetPhishingScorer(&scorer);
ASSERT_TRUE(classifier_->is_ready());
EXPECT_CALL(*classifier_, CancelPendingClassification());
GURL url("http://host.com");
LoadHTMLWithUrlOverride("<html><body>phish</body></html>",
url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
base::string16 page_text = ASCIIToUTF16("phish");
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
// Now simulate the StartPhishingDetection IPC. We expect classification
// to begin.
page_text = ASCIIToUTF16("phish");
EXPECT_CALL(*classifier_, BeginClassification(Pointee(page_text), _));
OnStartPhishingDetection(url);
Mock::VerifyAndClearExpectations(classifier_);
// Now try again, but this time we will navigate the page away before
// the IPC is sent.
EXPECT_CALL(*classifier_, CancelPendingClassification());
GURL url2("http://host2.com");
LoadHTMLWithUrlOverride("<html><body>phish</body></html>",
url2.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
page_text = ASCIIToUTF16("phish");
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url2);
Mock::VerifyAndClearExpectations(classifier_);
EXPECT_CALL(*classifier_, CancelPendingClassification());
GURL url3("http://host3.com");
LoadHTMLWithUrlOverride("<html><body>phish</body></html>",
url3.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
OnStartPhishingDetection(url);
// In this test, the original page is a redirect, which we do not get a
// StartPhishingDetection IPC for. We simulate the redirection event to
// load a new page while reusing the original session history entry, and
// check that classification begins correctly for the landing page.
EXPECT_CALL(*classifier_, CancelPendingClassification());
GURL url4("http://host4.com");
LoadHTMLWithUrlOverride("<html><body>phish</body></html>",
url4.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
page_text = ASCIIToUTF16("abc");
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url4);
Mock::VerifyAndClearExpectations(classifier_);
EXPECT_CALL(*classifier_, CancelPendingClassification());
GURL redir_url("http://host4.com/redir");
LoadHTMLWithUrlOverride("123", redir_url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
OnStartPhishingDetection(url4);
page_text = ASCIIToUTF16("123");
{
InSequence s;
EXPECT_CALL(*classifier_, CancelPendingClassification());
EXPECT_CALL(*classifier_, BeginClassification(Pointee(page_text), _));
SimulateRedirection(redir_url);
PageCaptured(&page_text, false, redir_url);
Mock::VerifyAndClearExpectations(classifier_);
}
// The delegate will cancel pending classification on destruction.
EXPECT_CALL(*classifier_, CancelPendingClassification());
}
TEST_F(PhishingClassifierDelegateTest, IgnorePreliminaryCapture) {
// Tests that preliminary PageCaptured notifications are ignored.
MockScorer scorer;
delegate_->SetPhishingScorer(&scorer);
ASSERT_TRUE(classifier_->is_ready());
EXPECT_CALL(*classifier_, CancelPendingClassification());
GURL url("http://host.com");
LoadHTMLWithUrlOverride("<html><body>phish</body></html>",
url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
OnStartPhishingDetection(url);
base::string16 page_text = ASCIIToUTF16("phish");
PageCaptured(&page_text, true, url);
// Once the non-preliminary capture happens, classification should begin.
page_text = ASCIIToUTF16("phish");
{
InSequence s;
EXPECT_CALL(*classifier_, CancelPendingClassification());
EXPECT_CALL(*classifier_, BeginClassification(Pointee(page_text), _));
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
}
// The delegate will cancel pending classification on destruction.
EXPECT_CALL(*classifier_, CancelPendingClassification());
}
TEST_F(PhishingClassifierDelegateTest, DuplicatePageCapture) {
// Tests that a second PageCaptured notification causes classification to
// be cancelled.
MockScorer scorer;
delegate_->SetPhishingScorer(&scorer);
ASSERT_TRUE(classifier_->is_ready());
EXPECT_CALL(*classifier_, CancelPendingClassification());
GURL url("http://host.com");
LoadHTMLWithUrlOverride("<html><body>phish</body></html>",
url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
OnStartPhishingDetection(url);
base::string16 page_text = ASCIIToUTF16("phish");
{
InSequence s;
EXPECT_CALL(*classifier_, CancelPendingClassification());
EXPECT_CALL(*classifier_, BeginClassification(Pointee(page_text), _));
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
}
page_text = ASCIIToUTF16("phish");
EXPECT_CALL(*classifier_, CancelPendingClassification());
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
// The delegate will cancel pending classification on destruction.
EXPECT_CALL(*classifier_, CancelPendingClassification());
}
TEST_F(PhishingClassifierDelegateTest, PhishingDetectionDone) {
// Tests that a SafeBrowsingHostMsg_PhishingDetectionDone IPC is
// sent to the browser whenever we finish classification.
MockScorer scorer;
delegate_->SetPhishingScorer(&scorer);
ASSERT_TRUE(classifier_->is_ready());
// Start by loading a page to populate the delegate's state.
EXPECT_CALL(*classifier_, CancelPendingClassification());
GURL url("http://host.com");
LoadHTMLWithUrlOverride("<html><body>phish</body></html>",
url.spec().c_str());
Mock::VerifyAndClearExpectations(classifier_);
base::string16 page_text = ASCIIToUTF16("phish");
OnStartPhishingDetection(url);
{
InSequence s;
EXPECT_CALL(*classifier_, CancelPendingClassification());
EXPECT_CALL(*classifier_, BeginClassification(Pointee(page_text), _));
PageCaptured(&page_text, false, url);
Mock::VerifyAndClearExpectations(classifier_);
}
// Now run the callback to simulate the classifier finishing.
ClientPhishingRequest verdict;
verdict.set_url(url.spec());
verdict.set_client_score(0.8f);
verdict.set_is_phishing(false); // Send IPC even if site is not phishing.
RunAndVerifyClassificationDone(verdict);
// The delegate will cancel pending classification on destruction.
EXPECT_CALL(*classifier_, CancelPendingClassification());
}
} // namespace safe_browsing