1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "base/files/file_path.h" 6 #include "base/memory/ref_counted.h" 7 #include "base/memory/scoped_ptr.h" 8 #include "base/run_loop.h" 9 #include "base/strings/stringprintf.h" 10 #include "base/synchronization/waitable_event.h" 11 #include "chrome/browser/safe_browsing/browser_feature_extractor.h" 12 #include "chrome/browser/safe_browsing/client_side_detection_host.h" 13 #include "chrome/browser/safe_browsing/client_side_detection_service.h" 14 #include "chrome/browser/safe_browsing/database_manager.h" 15 #include "chrome/browser/safe_browsing/safe_browsing_service.h" 16 #include "chrome/browser/safe_browsing/ui_manager.h" 17 #include "chrome/common/chrome_switches.h" 18 #include "chrome/common/safe_browsing/csd.pb.h" 19 #include "chrome/common/safe_browsing/safebrowsing_messages.h" 20 #include "chrome/test/base/chrome_render_view_host_test_harness.h" 21 #include "chrome/test/base/testing_profile.h" 22 #include "content/public/browser/web_contents.h" 23 #include "content/public/test/mock_render_process_host.h" 24 #include "content/public/test/test_browser_thread.h" 25 #include "content/public/test/test_renderer_host.h" 26 #include "ipc/ipc_test_sink.h" 27 #include "testing/gmock/include/gmock/gmock.h" 28 #include "testing/gtest/include/gtest/gtest.h" 29 #include "url/gurl.h" 30 31 using ::testing::_; 32 using ::testing::DeleteArg; 33 using ::testing::DoAll; 34 using ::testing::Eq; 35 using ::testing::IsNull; 36 using ::testing::Mock; 37 using ::testing::NiceMock; 38 using ::testing::NotNull; 39 using ::testing::Pointee; 40 using ::testing::Return; 41 using ::testing::SaveArg; 42 using ::testing::SetArgumentPointee; 43 using ::testing::StrictMock; 44 using content::BrowserThread; 45 using content::RenderViewHostTester; 46 using content::WebContents; 47 48 namespace { 49 const bool kFalse = false; 50 const bool kTrue = true; 51 } 52 53 namespace safe_browsing { 54 namespace { 55 // This matcher verifies that the client computed verdict 56 // (ClientPhishingRequest) which is passed to SendClientReportPhishingRequest 57 // has the expected fields set. Note: we can't simply compare the protocol 58 // buffer strings because the BrowserFeatureExtractor might add features to the 59 // verdict object before calling SendClientReportPhishingRequest. 60 MATCHER_P(PartiallyEqualVerdict, other, "") { 61 return (other.url() == arg.url() && 62 other.client_score() == arg.client_score() && 63 other.is_phishing() == arg.is_phishing()); 64 } 65 66 MATCHER_P(PartiallyEqualMalwareVerdict, other, "") { 67 return (other.url() == arg.url() && 68 other.feature_map_size() == arg.feature_map_size()); 69 } 70 71 // Test that the callback is NULL when the verdict is not phishing. 72 MATCHER(CallbackIsNull, "") { 73 return arg.is_null(); 74 } 75 76 ACTION(QuitUIMessageLoop) { 77 EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::UI)); 78 base::MessageLoopForUI::current()->Quit(); 79 } 80 81 // It's kind of insane that InvokeArgument doesn't work with callbacks, but it 82 // doesn't seem like it. 83 ACTION_TEMPLATE(InvokeCallbackArgument, 84 HAS_1_TEMPLATE_PARAMS(int, k), 85 AND_2_VALUE_PARAMS(p0, p1)) { 86 ::std::tr1::get<k>(args).Run(p0, p1); 87 } 88 89 void EmptyUrlCheckCallback(bool processed) { 90 } 91 92 class MockClientSideDetectionService : public ClientSideDetectionService { 93 public: 94 MockClientSideDetectionService() : ClientSideDetectionService(NULL) {} 95 virtual ~MockClientSideDetectionService() {}; 96 97 MOCK_METHOD2(SendClientReportPhishingRequest, 98 void(ClientPhishingRequest*, 99 const ClientReportPhishingRequestCallback&)); 100 MOCK_METHOD2(SendClientReportMalwareRequest, 101 void(ClientMalwareRequest*, 102 const ClientReportMalwareRequestCallback&)); 103 MOCK_CONST_METHOD1(IsPrivateIPAddress, bool(const std::string&)); 104 MOCK_METHOD2(GetValidCachedResult, bool(const GURL&, bool*)); 105 MOCK_METHOD1(IsInCache, bool(const GURL&)); 106 MOCK_METHOD0(OverPhishingReportLimit, bool()); 107 108 private: 109 DISALLOW_COPY_AND_ASSIGN(MockClientSideDetectionService); 110 }; 111 112 class MockSafeBrowsingUIManager : public SafeBrowsingUIManager { 113 public: 114 explicit MockSafeBrowsingUIManager(SafeBrowsingService* service) 115 : SafeBrowsingUIManager(service) { } 116 117 MOCK_METHOD1(DoDisplayBlockingPage, void(const UnsafeResource& resource)); 118 119 // Helper function which calls OnBlockingPageComplete for this client 120 // object. 121 void InvokeOnBlockingPageComplete(const UrlCheckCallback& callback) { 122 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 123 DCHECK(!callback.is_null()); 124 // Note: this will delete the client object in the case of the CsdClient 125 // implementation. 126 callback.Run(false); 127 } 128 129 protected: 130 virtual ~MockSafeBrowsingUIManager() { } 131 132 private: 133 DISALLOW_COPY_AND_ASSIGN(MockSafeBrowsingUIManager); 134 }; 135 136 class MockSafeBrowsingDatabaseManager : public SafeBrowsingDatabaseManager { 137 public: 138 explicit MockSafeBrowsingDatabaseManager(SafeBrowsingService* service) 139 : SafeBrowsingDatabaseManager(service) { } 140 141 MOCK_METHOD1(MatchCsdWhitelistUrl, bool(const GURL&)); 142 143 protected: 144 virtual ~MockSafeBrowsingDatabaseManager() {} 145 146 private: 147 DISALLOW_COPY_AND_ASSIGN(MockSafeBrowsingDatabaseManager); 148 }; 149 150 class MockTestingProfile : public TestingProfile { 151 public: 152 MockTestingProfile() {} 153 virtual ~MockTestingProfile() {} 154 155 MOCK_CONST_METHOD0(IsOffTheRecord, bool()); 156 }; 157 158 class MockBrowserFeatureExtractor : public BrowserFeatureExtractor { 159 public: 160 explicit MockBrowserFeatureExtractor( 161 WebContents* tab, 162 ClientSideDetectionService* service) 163 : BrowserFeatureExtractor(tab, service) {} 164 virtual ~MockBrowserFeatureExtractor() {} 165 166 MOCK_METHOD3(ExtractFeatures, 167 void(const BrowseInfo* info, 168 ClientPhishingRequest*, 169 const BrowserFeatureExtractor::DoneCallback&)); 170 171 MOCK_METHOD2(ExtractMalwareFeatures, 172 void(const BrowseInfo* info, 173 ClientMalwareRequest*)); 174 }; 175 176 } // namespace 177 178 class ClientSideDetectionHostTest : public ChromeRenderViewHostTestHarness { 179 public: 180 typedef SafeBrowsingUIManager::UnsafeResource UnsafeResource; 181 182 virtual void SetUp() { 183 ChromeRenderViewHostTestHarness::SetUp(); 184 185 // Inject service classes. 186 csd_service_.reset(new StrictMock<MockClientSideDetectionService>()); 187 // Only used for initializing mock objects. 188 SafeBrowsingService* sb_service = 189 SafeBrowsingService::CreateSafeBrowsingService(); 190 database_manager_ = 191 new StrictMock<MockSafeBrowsingDatabaseManager>(sb_service); 192 ui_manager_ = new StrictMock<MockSafeBrowsingUIManager>(sb_service); 193 csd_host_.reset(safe_browsing::ClientSideDetectionHost::Create( 194 web_contents())); 195 csd_host_->set_client_side_detection_service(csd_service_.get()); 196 csd_host_->set_safe_browsing_managers(ui_manager_.get(), 197 database_manager_.get()); 198 // We need to create this here since we don't call 199 // DidNavigateMainFramePostCommit in this test. 200 csd_host_->browse_info_.reset(new BrowseInfo); 201 202 // By default this is set to false. Turn it on as if we are in canary or 203 // dev channel 204 csd_host_->malware_report_enabled_ = true; 205 } 206 207 virtual void TearDown() { 208 // Delete the host object on the UI thread and release the 209 // SafeBrowsingService. 210 BrowserThread::DeleteSoon(BrowserThread::UI, FROM_HERE, 211 csd_host_.release()); 212 database_manager_ = NULL; 213 ui_manager_ = NULL; 214 base::RunLoop().RunUntilIdle(); 215 ChromeRenderViewHostTestHarness::TearDown(); 216 } 217 218 virtual content::BrowserContext* CreateBrowserContext() OVERRIDE { 219 // Set custom profile object so that we can mock calls to IsOffTheRecord. 220 // This needs to happen before we call the parent SetUp() function. We use 221 // a nice mock because other parts of the code are calling IsOffTheRecord. 222 mock_profile_ = new NiceMock<MockTestingProfile>(); 223 return mock_profile_; 224 } 225 226 void OnPhishingDetectionDone(const std::string& verdict_str) { 227 csd_host_->OnPhishingDetectionDone(verdict_str); 228 } 229 230 void UpdateIPUrlMap(const std::string& ip, const std::string& host) { 231 csd_host_->UpdateIPUrlMap(ip, host); 232 } 233 234 BrowseInfo* GetBrowseInfo() { 235 return csd_host_->browse_info_.get(); 236 } 237 238 void ExpectPreClassificationChecks(const GURL& url, 239 const bool* is_private, 240 const bool* is_incognito, 241 const bool* match_csd_whitelist, 242 const bool* get_valid_cached_result, 243 const bool* is_in_cache, 244 const bool* over_report_limit) { 245 if (is_private) { 246 EXPECT_CALL(*csd_service_, IsPrivateIPAddress(_)) 247 .WillOnce(Return(*is_private)); 248 } 249 if (is_incognito) { 250 EXPECT_CALL(*mock_profile_, IsOffTheRecord()) 251 .WillRepeatedly(Return(*is_incognito)); 252 } 253 if (match_csd_whitelist) { 254 EXPECT_CALL(*database_manager_.get(), MatchCsdWhitelistUrl(url)) 255 .WillOnce(Return(*match_csd_whitelist)); 256 } 257 if (get_valid_cached_result) { 258 EXPECT_CALL(*csd_service_, GetValidCachedResult(url, NotNull())) 259 .WillOnce(DoAll(SetArgumentPointee<1>(true), 260 Return(*get_valid_cached_result))); 261 } 262 if (is_in_cache) { 263 EXPECT_CALL(*csd_service_, IsInCache(url)).WillOnce(Return(*is_in_cache)); 264 } 265 if (over_report_limit) { 266 EXPECT_CALL(*csd_service_, OverPhishingReportLimit()) 267 .WillOnce(Return(*over_report_limit)); 268 } 269 } 270 271 void WaitAndCheckPreClassificationChecks() { 272 // Wait for CheckCsdWhitelist and CheckCache() to be called if at all. 273 base::RunLoop().RunUntilIdle(); 274 EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); 275 EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); 276 EXPECT_TRUE(Mock::VerifyAndClear(database_manager_.get())); 277 EXPECT_TRUE(Mock::VerifyAndClear(mock_profile_)); 278 } 279 280 void SetFeatureExtractor(BrowserFeatureExtractor* extractor) { 281 csd_host_->feature_extractor_.reset(extractor); 282 } 283 284 void SetRedirectChain(const std::vector<GURL>& redirect_chain) { 285 csd_host_->browse_info_->url_redirects = redirect_chain; 286 } 287 288 void SetUnsafeResourceToCurrent() { 289 UnsafeResource resource; 290 resource.url = GURL("http://www.malware.com/"); 291 resource.original_url = web_contents()->GetURL(); 292 resource.is_subresource = true; 293 resource.threat_type = SB_THREAT_TYPE_URL_MALWARE; 294 resource.callback = base::Bind(&EmptyUrlCheckCallback); 295 resource.render_process_host_id = web_contents()->GetRenderProcessHost()-> 296 GetID(); 297 resource.render_view_id = 298 web_contents()->GetRenderViewHost()->GetRoutingID(); 299 csd_host_->OnSafeBrowsingHit(resource); 300 resource.callback.Reset(); 301 ASSERT_TRUE(csd_host_->DidShowSBInterstitial()); 302 ASSERT_TRUE(csd_host_->unsafe_resource_.get()); 303 // Test that the resource above was copied. 304 EXPECT_EQ(resource.url, csd_host_->unsafe_resource_->url); 305 EXPECT_EQ(resource.original_url, csd_host_->unsafe_resource_->original_url); 306 EXPECT_EQ(resource.is_subresource, 307 csd_host_->unsafe_resource_->is_subresource); 308 EXPECT_EQ(resource.threat_type, csd_host_->unsafe_resource_->threat_type); 309 EXPECT_TRUE(csd_host_->unsafe_resource_->callback.is_null()); 310 EXPECT_EQ(resource.render_process_host_id, 311 csd_host_->unsafe_resource_->render_process_host_id); 312 EXPECT_EQ(resource.render_view_id, 313 csd_host_->unsafe_resource_->render_view_id); 314 } 315 316 protected: 317 scoped_ptr<ClientSideDetectionHost> csd_host_; 318 scoped_ptr<StrictMock<MockClientSideDetectionService> > csd_service_; 319 scoped_refptr<StrictMock<MockSafeBrowsingUIManager> > ui_manager_; 320 scoped_refptr<StrictMock<MockSafeBrowsingDatabaseManager> > database_manager_; 321 MockTestingProfile* mock_profile_; // We don't own this object 322 }; 323 324 325 TEST_F(ClientSideDetectionHostTest, OnPhishingDetectionDoneInvalidVerdict) { 326 // Case 0: renderer sends an invalid verdict string that we're unable to 327 // parse. 328 MockBrowserFeatureExtractor* mock_extractor = new MockBrowserFeatureExtractor( 329 web_contents(), 330 csd_service_.get()); 331 SetFeatureExtractor(mock_extractor); // The host class takes ownership. 332 EXPECT_CALL(*mock_extractor, ExtractFeatures(_, _, _)).Times(0); 333 OnPhishingDetectionDone("Invalid Protocol Buffer"); 334 EXPECT_TRUE(Mock::VerifyAndClear(mock_extractor)); 335 } 336 337 TEST_F(ClientSideDetectionHostTest, OnPhishingDetectionDoneNotPhishing) { 338 // Case 1: client thinks the page is phishing. The server does not agree. 339 // No interstitial is shown. 340 MockBrowserFeatureExtractor* mock_extractor = new MockBrowserFeatureExtractor( 341 web_contents(), 342 csd_service_.get()); 343 SetFeatureExtractor(mock_extractor); // The host class takes ownership. 344 345 ClientSideDetectionService::ClientReportPhishingRequestCallback cb; 346 ClientPhishingRequest verdict; 347 verdict.set_url("http://phishingurl.com/"); 348 verdict.set_client_score(1.0f); 349 verdict.set_is_phishing(true); 350 351 EXPECT_CALL(*mock_extractor, ExtractFeatures(_, _, _)) 352 .WillOnce(DoAll(DeleteArg<1>(), 353 InvokeCallbackArgument<2>(true, &verdict))); 354 EXPECT_CALL(*csd_service_, 355 SendClientReportPhishingRequest( 356 Pointee(PartiallyEqualVerdict(verdict)), _)) 357 .WillOnce(SaveArg<1>(&cb)); 358 OnPhishingDetectionDone(verdict.SerializeAsString()); 359 EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); 360 ASSERT_FALSE(cb.is_null()); 361 362 // Make sure DoDisplayBlockingPage is not going to be called. 363 EXPECT_CALL(*ui_manager_.get(), DoDisplayBlockingPage(_)).Times(0); 364 cb.Run(GURL(verdict.url()), false); 365 base::RunLoop().RunUntilIdle(); 366 EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); 367 } 368 369 TEST_F(ClientSideDetectionHostTest, OnPhishingDetectionDoneDisabled) { 370 // Case 2: client thinks the page is phishing and so does the server but 371 // showing the interstitial is disabled => no interstitial is shown. 372 MockBrowserFeatureExtractor* mock_extractor = new MockBrowserFeatureExtractor( 373 web_contents(), 374 csd_service_.get()); 375 SetFeatureExtractor(mock_extractor); // The host class takes ownership. 376 377 ClientSideDetectionService::ClientReportPhishingRequestCallback cb; 378 ClientPhishingRequest verdict; 379 verdict.set_url("http://phishingurl.com/"); 380 verdict.set_client_score(1.0f); 381 verdict.set_is_phishing(true); 382 383 EXPECT_CALL(*mock_extractor, ExtractFeatures(_, _, _)) 384 .WillOnce(DoAll(DeleteArg<1>(), 385 InvokeCallbackArgument<2>(true, &verdict))); 386 EXPECT_CALL(*csd_service_, 387 SendClientReportPhishingRequest( 388 Pointee(PartiallyEqualVerdict(verdict)), _)) 389 .WillOnce(SaveArg<1>(&cb)); 390 OnPhishingDetectionDone(verdict.SerializeAsString()); 391 EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); 392 ASSERT_FALSE(cb.is_null()); 393 394 // Make sure DoDisplayBlockingPage is not going to be called. 395 EXPECT_CALL(*ui_manager_.get(), DoDisplayBlockingPage(_)).Times(0); 396 cb.Run(GURL(verdict.url()), false); 397 base::RunLoop().RunUntilIdle(); 398 EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); 399 } 400 401 TEST_F(ClientSideDetectionHostTest, OnPhishingDetectionDoneShowInterstitial) { 402 // Case 3: client thinks the page is phishing and so does the server. 403 // We show an interstitial. 404 MockBrowserFeatureExtractor* mock_extractor = new MockBrowserFeatureExtractor( 405 web_contents(), 406 csd_service_.get()); 407 SetFeatureExtractor(mock_extractor); // The host class takes ownership. 408 409 ClientSideDetectionService::ClientReportPhishingRequestCallback cb; 410 GURL phishing_url("http://phishingurl.com/"); 411 ClientPhishingRequest verdict; 412 verdict.set_url(phishing_url.spec()); 413 verdict.set_client_score(1.0f); 414 verdict.set_is_phishing(true); 415 416 EXPECT_CALL(*mock_extractor, ExtractFeatures(_, _, _)) 417 .WillOnce(DoAll(DeleteArg<1>(), 418 InvokeCallbackArgument<2>(true, &verdict))); 419 EXPECT_CALL(*csd_service_, 420 SendClientReportPhishingRequest( 421 Pointee(PartiallyEqualVerdict(verdict)), _)) 422 .WillOnce(SaveArg<1>(&cb)); 423 OnPhishingDetectionDone(verdict.SerializeAsString()); 424 EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); 425 ASSERT_FALSE(cb.is_null()); 426 427 UnsafeResource resource; 428 EXPECT_CALL(*ui_manager_.get(), DoDisplayBlockingPage(_)) 429 .WillOnce(SaveArg<0>(&resource)); 430 cb.Run(phishing_url, true); 431 432 base::RunLoop().RunUntilIdle(); 433 EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); 434 EXPECT_EQ(phishing_url, resource.url); 435 EXPECT_EQ(phishing_url, resource.original_url); 436 EXPECT_FALSE(resource.is_subresource); 437 EXPECT_EQ(SB_THREAT_TYPE_CLIENT_SIDE_PHISHING_URL, resource.threat_type); 438 EXPECT_EQ(web_contents()->GetRenderProcessHost()->GetID(), 439 resource.render_process_host_id); 440 EXPECT_EQ(web_contents()->GetRenderViewHost()->GetRoutingID(), 441 resource.render_view_id); 442 443 // Make sure the client object will be deleted. 444 BrowserThread::PostTask( 445 BrowserThread::IO, 446 FROM_HERE, 447 base::Bind(&MockSafeBrowsingUIManager::InvokeOnBlockingPageComplete, 448 ui_manager_, resource.callback)); 449 } 450 451 TEST_F(ClientSideDetectionHostTest, OnPhishingDetectionDoneMultiplePings) { 452 // Case 4 & 5: client thinks a page is phishing then navigates to 453 // another page which is also considered phishing by the client 454 // before the server responds with a verdict. After a while the 455 // server responds for both requests with a phishing verdict. Only 456 // a single interstitial is shown for the second URL. 457 MockBrowserFeatureExtractor* mock_extractor = new MockBrowserFeatureExtractor( 458 web_contents(), 459 csd_service_.get()); 460 SetFeatureExtractor(mock_extractor); // The host class takes ownership. 461 462 ClientSideDetectionService::ClientReportPhishingRequestCallback cb; 463 GURL phishing_url("http://phishingurl.com/"); 464 ClientPhishingRequest verdict; 465 verdict.set_url(phishing_url.spec()); 466 verdict.set_client_score(1.0f); 467 verdict.set_is_phishing(true); 468 469 EXPECT_CALL(*mock_extractor, ExtractFeatures(_, _, _)) 470 .WillOnce(DoAll(DeleteArg<1>(), 471 InvokeCallbackArgument<2>(true, &verdict))); 472 EXPECT_CALL(*csd_service_, 473 SendClientReportPhishingRequest( 474 Pointee(PartiallyEqualVerdict(verdict)), _)) 475 .WillOnce(SaveArg<1>(&cb)); 476 OnPhishingDetectionDone(verdict.SerializeAsString()); 477 EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); 478 ASSERT_FALSE(cb.is_null()); 479 480 // Set this back to a normal browser feature extractor since we're using 481 // NavigateAndCommit() and it's easier to use the real thing than setting up 482 // mock expectations. 483 SetFeatureExtractor(new BrowserFeatureExtractor(web_contents(), 484 csd_service_.get())); 485 GURL other_phishing_url("http://other_phishing_url.com/bla"); 486 ExpectPreClassificationChecks(other_phishing_url, &kFalse, &kFalse, &kFalse, 487 &kFalse, &kFalse, &kFalse); 488 // We navigate away. The callback cb should be revoked. 489 NavigateAndCommit(other_phishing_url); 490 // Wait for the pre-classification checks to finish for other_phishing_url. 491 WaitAndCheckPreClassificationChecks(); 492 493 ClientSideDetectionService::ClientReportPhishingRequestCallback cb_other; 494 verdict.set_url(other_phishing_url.spec()); 495 verdict.set_client_score(0.8f); 496 EXPECT_CALL(*csd_service_, 497 SendClientReportPhishingRequest( 498 Pointee(PartiallyEqualVerdict(verdict)), _)) 499 .WillOnce(DoAll(DeleteArg<0>(), 500 SaveArg<1>(&cb_other), 501 QuitUIMessageLoop())); 502 std::vector<GURL> redirect_chain; 503 redirect_chain.push_back(other_phishing_url); 504 SetRedirectChain(redirect_chain); 505 OnPhishingDetectionDone(verdict.SerializeAsString()); 506 base::MessageLoop::current()->Run(); 507 EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); 508 ASSERT_FALSE(cb_other.is_null()); 509 510 // We expect that the interstitial is shown for the second phishing URL and 511 // not for the first phishing URL. 512 UnsafeResource resource; 513 EXPECT_CALL(*ui_manager_.get(), DoDisplayBlockingPage(_)) 514 .WillOnce(SaveArg<0>(&resource)); 515 516 cb.Run(phishing_url, true); // Should have no effect. 517 cb_other.Run(other_phishing_url, true); // Should show interstitial. 518 519 base::RunLoop().RunUntilIdle(); 520 EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); 521 EXPECT_EQ(other_phishing_url, resource.url); 522 EXPECT_EQ(other_phishing_url, resource.original_url); 523 EXPECT_FALSE(resource.is_subresource); 524 EXPECT_EQ(SB_THREAT_TYPE_CLIENT_SIDE_PHISHING_URL, resource.threat_type); 525 EXPECT_EQ(web_contents()->GetRenderProcessHost()->GetID(), 526 resource.render_process_host_id); 527 EXPECT_EQ(web_contents()->GetRenderViewHost()->GetRoutingID(), 528 resource.render_view_id); 529 530 // Make sure the client object will be deleted. 531 BrowserThread::PostTask( 532 BrowserThread::IO, 533 FROM_HERE, 534 base::Bind(&MockSafeBrowsingUIManager::InvokeOnBlockingPageComplete, 535 ui_manager_, resource.callback)); 536 } 537 538 TEST_F(ClientSideDetectionHostTest, 539 OnPhishingDetectionDoneVerdictNotPhishing) { 540 // Case 6: renderer sends a verdict string that isn't phishing. 541 MockBrowserFeatureExtractor* mock_extractor = new MockBrowserFeatureExtractor( 542 web_contents(), 543 csd_service_.get()); 544 SetFeatureExtractor(mock_extractor); // The host class takes ownership. 545 546 ClientPhishingRequest verdict; 547 verdict.set_url("http://not-phishing.com/"); 548 verdict.set_client_score(0.1f); 549 verdict.set_is_phishing(false); 550 551 EXPECT_CALL(*mock_extractor, ExtractFeatures(_, _, _)).Times(0); 552 OnPhishingDetectionDone(verdict.SerializeAsString()); 553 EXPECT_TRUE(Mock::VerifyAndClear(mock_extractor)); 554 } 555 556 TEST_F(ClientSideDetectionHostTest, 557 OnPhishingDetectionDoneVerdictNotPhishingButSBMatch) { 558 // Case 7: renderer sends a verdict string that isn't phishing but the URL 559 // was on the regular phishing or malware lists. 560 GURL url("http://not-phishing.com/"); 561 ClientPhishingRequest verdict; 562 verdict.set_url(url.spec()); 563 verdict.set_client_score(0.1f); 564 verdict.set_is_phishing(false); 565 566 // First we have to navigate to the URL to set the unique page ID. 567 ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, 568 &kFalse, &kFalse); 569 NavigateAndCommit(url); 570 WaitAndCheckPreClassificationChecks(); 571 SetUnsafeResourceToCurrent(); 572 573 EXPECT_CALL(*csd_service_, 574 SendClientReportPhishingRequest( 575 Pointee(PartiallyEqualVerdict(verdict)), CallbackIsNull())) 576 .WillOnce(DoAll(DeleteArg<0>(), QuitUIMessageLoop())); 577 std::vector<GURL> redirect_chain; 578 redirect_chain.push_back(url); 579 SetRedirectChain(redirect_chain); 580 OnPhishingDetectionDone(verdict.SerializeAsString()); 581 base::MessageLoop::current()->Run(); 582 EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); 583 } 584 585 TEST_F(ClientSideDetectionHostTest, UpdateIPUrlMap) { 586 BrowseInfo* browse_info = GetBrowseInfo(); 587 588 // Empty IP or host are skipped 589 UpdateIPUrlMap("250.10.10.10", std::string()); 590 ASSERT_EQ(0U, browse_info->ips.size()); 591 UpdateIPUrlMap(std::string(), "http://google.com/a"); 592 ASSERT_EQ(0U, browse_info->ips.size()); 593 UpdateIPUrlMap(std::string(), std::string()); 594 ASSERT_EQ(0U, browse_info->ips.size()); 595 596 std::set<std::string> expected_urls; 597 for (int i = 0; i < 20; i++) { 598 std::string url = base::StringPrintf("http://%d.com/", i); 599 expected_urls.insert(url); 600 UpdateIPUrlMap("250.10.10.10", url); 601 } 602 ASSERT_EQ(1U, browse_info->ips.size()); 603 ASSERT_EQ(20U, browse_info->ips["250.10.10.10"].size()); 604 EXPECT_EQ(expected_urls, browse_info->ips["250.10.10.10"]); 605 606 // Add more urls for this ip, it exceeds max limit and won't be added 607 UpdateIPUrlMap("250.10.10.10", "http://21.com/"); 608 ASSERT_EQ(1U, browse_info->ips.size()); 609 ASSERT_EQ(20U, browse_info->ips["250.10.10.10"].size()); 610 EXPECT_EQ(expected_urls, browse_info->ips["250.10.10.10"]); 611 612 // Add 199 more IPs 613 for (int i = 0; i < 199; i++) { 614 std::string ip = base::StringPrintf("%d.%d.%d.256", i, i, i); 615 expected_urls.clear(); 616 expected_urls.insert("test.com/"); 617 UpdateIPUrlMap(ip, "test.com/"); 618 ASSERT_EQ(1U, browse_info->ips[ip].size()); 619 EXPECT_EQ(expected_urls, browse_info->ips[ip]); 620 } 621 ASSERT_EQ(200U, browse_info->ips.size()); 622 623 // Exceeding max ip limit 200, these won't be added 624 UpdateIPUrlMap("250.250.250.250", "goo.com/"); 625 UpdateIPUrlMap("250.250.250.250", "bar.com/"); 626 UpdateIPUrlMap("250.250.0.250", "foo.com/"); 627 ASSERT_EQ(200U, browse_info->ips.size()); 628 629 // Add url to existing IPs succeed 630 UpdateIPUrlMap("100.100.100.256", "more.com/"); 631 ASSERT_EQ(2U, browse_info->ips["100.100.100.256"].size()); 632 expected_urls.clear(); 633 expected_urls.insert("test.com/"); 634 expected_urls.insert("more.com/"); 635 EXPECT_EQ(expected_urls, browse_info->ips["100.100.100.256"]); 636 } 637 638 TEST_F(ClientSideDetectionHostTest, 639 OnPhishingDetectionDoneVerdictNotPhishingNotMalwareIP) { 640 // Case 7: renderer sends a verdict string that isn't phishing and not matches 641 // malware bad IP list 642 MockBrowserFeatureExtractor* mock_extractor = new MockBrowserFeatureExtractor( 643 web_contents(), 644 csd_service_.get()); 645 SetFeatureExtractor(mock_extractor); // The host class takes ownership. 646 647 ClientPhishingRequest verdict; 648 verdict.set_url("http://not-phishing.com/"); 649 verdict.set_client_score(0.1f); 650 verdict.set_is_phishing(false); 651 652 ClientMalwareRequest malware_verdict; 653 malware_verdict.set_url("http://not-phishing.com/"); 654 655 EXPECT_CALL(*mock_extractor, ExtractMalwareFeatures(_, _)) 656 .WillOnce(SetArgumentPointee<1>(malware_verdict)); 657 EXPECT_CALL(*csd_service_, 658 SendClientReportMalwareRequest(_, _)).Times(0); 659 EXPECT_CALL(*mock_extractor, ExtractFeatures(_, _, _)).Times(0); 660 661 OnPhishingDetectionDone(verdict.SerializeAsString()); 662 EXPECT_TRUE(Mock::VerifyAndClear(mock_extractor)); 663 } 664 665 TEST_F(ClientSideDetectionHostTest, 666 OnPhishingDetectionDoneVerdictNotPhishingButMalwareIP) { 667 // Case 8: renderer sends a verdict string that isn't phishing but matches 668 // malware bad IP list 669 MockBrowserFeatureExtractor* mock_extractor = new MockBrowserFeatureExtractor( 670 web_contents(), 671 csd_service_.get()); 672 SetFeatureExtractor(mock_extractor); // The host class takes ownership. 673 674 ClientPhishingRequest verdict; 675 verdict.set_url("http://not-phishing.com/"); 676 verdict.set_client_score(0.1f); 677 verdict.set_is_phishing(false); 678 679 ClientMalwareRequest malware_verdict; 680 malware_verdict.set_url("http://not-phishing.com/"); 681 ClientMalwareRequest::Feature* feature = malware_verdict.add_feature_map(); 682 feature->set_name("malwareip1.2.3.4"); 683 feature->set_value(1.0); 684 feature->add_metainfo("badip.com"); 685 686 EXPECT_CALL(*mock_extractor, ExtractMalwareFeatures(_, _)) 687 .WillOnce(SetArgumentPointee<1>(malware_verdict)); 688 EXPECT_CALL(*csd_service_, 689 SendClientReportMalwareRequest( 690 Pointee(PartiallyEqualMalwareVerdict(malware_verdict)), _)) 691 .WillOnce(DeleteArg<0>()); 692 EXPECT_CALL(*mock_extractor, ExtractFeatures(_, _, _)).Times(0); 693 694 OnPhishingDetectionDone(verdict.SerializeAsString()); 695 EXPECT_TRUE(Mock::VerifyAndClear(mock_extractor)); 696 } 697 698 TEST_F(ClientSideDetectionHostTest, 699 OnPhishingDetectionDoneVerdictPhishingAndMalwareIP) { 700 // Case 9: renderer sends a verdict string that is phishing and matches 701 // malware bad IP list 702 MockBrowserFeatureExtractor* mock_extractor = new MockBrowserFeatureExtractor( 703 web_contents(), 704 csd_service_.get()); 705 SetFeatureExtractor(mock_extractor); // The host class takes ownership. 706 707 ClientSideDetectionService::ClientReportPhishingRequestCallback cb; 708 ClientPhishingRequest verdict; 709 verdict.set_url("http://not-phishing.com/"); 710 verdict.set_client_score(0.1f); 711 verdict.set_is_phishing(true); 712 713 ClientMalwareRequest malware_verdict; 714 malware_verdict.set_url("http://not-phishing.com/"); 715 ClientMalwareRequest::Feature* feature = malware_verdict.add_feature_map(); 716 feature->set_name("malwareip1.2.3.4"); 717 feature->set_value(1.0); 718 feature->add_metainfo("badip.com"); 719 720 EXPECT_CALL(*mock_extractor, ExtractMalwareFeatures(_, _)) 721 .WillOnce(SetArgumentPointee<1>(malware_verdict)); 722 EXPECT_CALL(*csd_service_, 723 SendClientReportMalwareRequest( 724 Pointee(PartiallyEqualMalwareVerdict(malware_verdict)), _)) 725 .WillOnce(DeleteArg<0>()); 726 727 EXPECT_CALL(*mock_extractor, ExtractFeatures(_, _, _)) 728 .WillOnce(DoAll(DeleteArg<1>(), 729 InvokeCallbackArgument<2>(true, &verdict))); 730 731 EXPECT_CALL(*csd_service_, 732 SendClientReportPhishingRequest( 733 Pointee(PartiallyEqualVerdict(verdict)), _)) 734 .WillOnce(SaveArg<1>(&cb)); 735 OnPhishingDetectionDone(verdict.SerializeAsString()); 736 EXPECT_TRUE(Mock::VerifyAndClear(mock_extractor)); 737 EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); 738 ASSERT_FALSE(cb.is_null()); 739 } 740 741 TEST_F(ClientSideDetectionHostTest, NavigationCancelsShouldClassifyUrl) { 742 // Test that canceling pending should classify requests works as expected. 743 744 GURL first_url("http://first.phishy.url.com"); 745 GURL second_url("http://second.url.com/"); 746 // The first few checks are done synchronously so check that they have been 747 // done for the first URL, while the second URL has all the checks done. We 748 // need to manually set up the IsPrivateIPAddress mock since if the same mock 749 // expectation is specified twice, gmock will only use the last instance of 750 // it, meaning the first will never be matched. 751 EXPECT_CALL(*csd_service_, IsPrivateIPAddress(_)) 752 .WillOnce(Return(false)) 753 .WillOnce(Return(false)); 754 ExpectPreClassificationChecks(first_url, NULL, &kFalse, &kFalse, NULL, 755 NULL, NULL); 756 ExpectPreClassificationChecks(second_url, NULL, &kFalse, &kFalse, &kFalse, 757 &kFalse, &kFalse); 758 759 NavigateAndCommit(first_url); 760 // Don't flush the message loop, as we want to navigate to a different 761 // url before the final pre-classification checks are run. 762 NavigateAndCommit(second_url); 763 WaitAndCheckPreClassificationChecks(); 764 } 765 766 TEST_F(ClientSideDetectionHostTest, ShouldClassifyUrl) { 767 // Navigate the tab to a page. We should see a StartPhishingDetection IPC. 768 GURL url("http://host.com/"); 769 ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, 770 &kFalse, &kFalse); 771 NavigateAndCommit(url); 772 WaitAndCheckPreClassificationChecks(); 773 774 const IPC::Message* msg = process()->sink().GetFirstMessageMatching( 775 SafeBrowsingMsg_StartPhishingDetection::ID); 776 ASSERT_TRUE(msg); 777 Tuple1<GURL> actual_url; 778 SafeBrowsingMsg_StartPhishingDetection::Read(msg, &actual_url); 779 EXPECT_EQ(url, actual_url.a); 780 EXPECT_EQ(rvh()->GetRoutingID(), msg->routing_id()); 781 process()->sink().ClearMessages(); 782 783 // Now try an in-page navigation. This should not trigger an IPC. 784 EXPECT_CALL(*csd_service_, IsPrivateIPAddress(_)).Times(0); 785 url = GURL("http://host.com/#foo"); 786 ExpectPreClassificationChecks(url, NULL, NULL, NULL, NULL, NULL, NULL); 787 NavigateAndCommit(url); 788 WaitAndCheckPreClassificationChecks(); 789 790 msg = process()->sink().GetFirstMessageMatching( 791 SafeBrowsingMsg_StartPhishingDetection::ID); 792 ASSERT_FALSE(msg); 793 794 // Check that XHTML is supported, in addition to the default HTML type. 795 // Note: for this test to work correctly, the new URL must be on the 796 // same domain as the previous URL, otherwise it will create a new 797 // RenderViewHost that won't have the mime type set. 798 url = GURL("http://host.com/xhtml"); 799 rvh_tester()->SetContentsMimeType("application/xhtml+xml"); 800 ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, 801 &kFalse, &kFalse); 802 NavigateAndCommit(url); 803 WaitAndCheckPreClassificationChecks(); 804 msg = process()->sink().GetFirstMessageMatching( 805 SafeBrowsingMsg_StartPhishingDetection::ID); 806 ASSERT_TRUE(msg); 807 SafeBrowsingMsg_StartPhishingDetection::Read(msg, &actual_url); 808 EXPECT_EQ(url, actual_url.a); 809 EXPECT_EQ(rvh()->GetRoutingID(), msg->routing_id()); 810 process()->sink().ClearMessages(); 811 812 // Navigate to a new host, which should cause another IPC. 813 url = GURL("http://host2.com/"); 814 ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, 815 &kFalse, &kFalse); 816 NavigateAndCommit(url); 817 WaitAndCheckPreClassificationChecks(); 818 msg = process()->sink().GetFirstMessageMatching( 819 SafeBrowsingMsg_StartPhishingDetection::ID); 820 ASSERT_TRUE(msg); 821 SafeBrowsingMsg_StartPhishingDetection::Read(msg, &actual_url); 822 EXPECT_EQ(url, actual_url.a); 823 EXPECT_EQ(rvh()->GetRoutingID(), msg->routing_id()); 824 process()->sink().ClearMessages(); 825 826 // If the mime type is not one that we support, no IPC should be triggered. 827 // Note: for this test to work correctly, the new URL must be on the 828 // same domain as the previous URL, otherwise it will create a new 829 // RenderViewHost that won't have the mime type set. 830 url = GURL("http://host2.com/image.jpg"); 831 rvh_tester()->SetContentsMimeType("image/jpeg"); 832 ExpectPreClassificationChecks(url, NULL, NULL, NULL, NULL, NULL, NULL); 833 NavigateAndCommit(url); 834 WaitAndCheckPreClassificationChecks(); 835 msg = process()->sink().GetFirstMessageMatching( 836 SafeBrowsingMsg_StartPhishingDetection::ID); 837 ASSERT_FALSE(msg); 838 839 // If IsPrivateIPAddress returns true, no IPC should be triggered. 840 url = GURL("http://host3.com/"); 841 ExpectPreClassificationChecks(url, &kTrue, NULL, NULL, NULL, NULL, NULL); 842 NavigateAndCommit(url); 843 WaitAndCheckPreClassificationChecks(); 844 msg = process()->sink().GetFirstMessageMatching( 845 SafeBrowsingMsg_StartPhishingDetection::ID); 846 ASSERT_FALSE(msg); 847 848 // If the tab is incognito there should be no IPC. Also, we shouldn't 849 // even check the csd-whitelist. 850 url = GURL("http://host4.com/"); 851 ExpectPreClassificationChecks(url, &kFalse, &kTrue, NULL, NULL, NULL, NULL); 852 NavigateAndCommit(url); 853 WaitAndCheckPreClassificationChecks(); 854 msg = process()->sink().GetFirstMessageMatching( 855 SafeBrowsingMsg_StartPhishingDetection::ID); 856 ASSERT_FALSE(msg); 857 858 // If the URL is on the csd whitelist, no IPC should be triggered. 859 url = GURL("http://host5.com/"); 860 ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kTrue, NULL, NULL, 861 NULL); 862 NavigateAndCommit(url); 863 WaitAndCheckPreClassificationChecks(); 864 msg = process()->sink().GetFirstMessageMatching( 865 SafeBrowsingMsg_StartPhishingDetection::ID); 866 ASSERT_FALSE(msg); 867 868 // If item is in the cache but it isn't valid, we will classify regardless 869 // of whether we are over the reporting limit. 870 url = GURL("http://host6.com/"); 871 ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, &kTrue, 872 NULL); 873 NavigateAndCommit(url); 874 WaitAndCheckPreClassificationChecks(); 875 msg = process()->sink().GetFirstMessageMatching( 876 SafeBrowsingMsg_StartPhishingDetection::ID); 877 ASSERT_TRUE(msg); 878 SafeBrowsingMsg_StartPhishingDetection::Read(msg, &actual_url); 879 EXPECT_EQ(url, actual_url.a); 880 EXPECT_EQ(rvh()->GetRoutingID(), msg->routing_id()); 881 process()->sink().ClearMessages(); 882 883 // If the url isn't in the cache and we are over the reporting limit, we 884 // don't do classification. 885 url = GURL("http://host7.com/"); 886 ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kFalse, 887 &kFalse, &kTrue); 888 NavigateAndCommit(url); 889 WaitAndCheckPreClassificationChecks(); 890 msg = process()->sink().GetFirstMessageMatching( 891 SafeBrowsingMsg_StartPhishingDetection::ID); 892 ASSERT_FALSE(msg); 893 894 // If result is cached, we will try and display the blocking page directly 895 // with no start classification message. 896 url = GURL("http://host8.com/"); 897 ExpectPreClassificationChecks(url, &kFalse, &kFalse, &kFalse, &kTrue, NULL, 898 NULL); 899 900 UnsafeResource resource; 901 EXPECT_CALL(*ui_manager_.get(), DoDisplayBlockingPage(_)) 902 .WillOnce(SaveArg<0>(&resource)); 903 904 NavigateAndCommit(url); 905 // Wait for CheckCsdWhitelist and CheckCache() to be called. 906 base::RunLoop().RunUntilIdle(); 907 // Now we check that all expected functions were indeed called on the two 908 // service objects. 909 EXPECT_TRUE(Mock::VerifyAndClear(csd_service_.get())); 910 EXPECT_TRUE(Mock::VerifyAndClear(ui_manager_.get())); 911 EXPECT_EQ(url, resource.url); 912 EXPECT_EQ(url, resource.original_url); 913 resource.callback.Reset(); 914 msg = process()->sink().GetFirstMessageMatching( 915 SafeBrowsingMsg_StartPhishingDetection::ID); 916 ASSERT_FALSE(msg); 917 } 918 919 } // namespace safe_browsing 920