| // Copyright 2018 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 "base/bind.h" |
| #include "base/containers/contains.h" |
| #include "base/path_service.h" |
| #include "base/strings/pattern.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_clock.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/history/history_service_factory.h" |
| #include "chrome/browser/history/history_test_utils.h" |
| #include "chrome/browser/lookalikes/digital_asset_links_cross_validator.h" |
| #include "chrome/browser/lookalikes/lookalike_url_blocking_page.h" |
| #include "chrome/browser/lookalikes/lookalike_url_navigation_throttle.h" |
| #include "chrome/browser/lookalikes/lookalike_url_service.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/reputation/reputation_service.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_commands.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/webui_url_constants.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/lookalikes/core/features.h" |
| #include "components/lookalikes/core/lookalike_url_util.h" |
| #include "components/network_session_configurator/common/network_switches.h" |
| #include "components/omnibox/browser/location_bar_model.h" |
| #include "components/reputation/core/safety_tip_test_utils.h" |
| #include "components/reputation/core/safety_tips_config.h" |
| #include "components/security_interstitials/content/security_interstitial_page.h" |
| #include "components/security_interstitials/content/security_interstitial_tab_helper.h" |
| #include "components/security_interstitials/core/metrics_helper.h" |
| #include "components/site_engagement/content/site_engagement_score.h" |
| #include "components/site_engagement/content/site_engagement_service.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "components/url_formatter/spoof_checks/top_domains/test_top500_domains.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/content_paths.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/content_mock_cert_verifier.h" |
| #include "content/public/test/signed_exchange_browser_test_helper.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/url_loader_interceptor.h" |
| #include "net/cert/mock_cert_verifier.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_source.h" |
| #include "services/network/public/cpp/network_switches.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "ui/base/window_open_disposition.h" |
| |
| namespace { |
| |
| using security_interstitials::MetricsHelper; |
| using security_interstitials::SecurityInterstitialCommand; |
| using UkmEntry = ukm::builders::LookalikeUrl_NavigationSuggestion; |
| |
| // An engagement score above MEDIUM. |
| const int kHighEngagement = 20; |
| |
| // An engagement score below MEDIUM. |
| const int kLowEngagement = 1; |
| |
| // The UMA metric names registered by metrics_helper. |
| const char kInterstitialDecisionMetric[] = "interstitial.lookalike.decision"; |
| const char kInterstitialInteractionMetric[] = |
| "interstitial.lookalike.interaction"; |
| |
| const char kConsoleMessage[] = |
| "Chrome has determined that * could be fake or fraudulent*"; |
| |
| static std::unique_ptr<net::test_server::HttpResponse> |
| NetworkErrorResponseHandler(const net::test_server::HttpRequest& request) { |
| return std::unique_ptr<net::test_server::HttpResponse>( |
| new net::test_server::RawHttpResponse("", "")); |
| } |
| |
| security_interstitials::SecurityInterstitialPage* GetCurrentInterstitial( |
| content::WebContents* web_contents) { |
| security_interstitials::SecurityInterstitialTabHelper* helper = |
| security_interstitials::SecurityInterstitialTabHelper::FromWebContents( |
| web_contents); |
| if (!helper) { |
| return nullptr; |
| } |
| return helper->GetBlockingPageForCurrentlyCommittedNavigationForTesting(); |
| } |
| |
| security_interstitials::SecurityInterstitialPage::TypeID GetInterstitialType( |
| content::WebContents* web_contents) { |
| security_interstitials::SecurityInterstitialPage* page = |
| GetCurrentInterstitial(web_contents); |
| if (!page) { |
| return nullptr; |
| } |
| return page->GetTypeForTesting(); |
| } |
| |
| // Sets the absolute Site Engagement |score| for the testing origin. |
| void SetEngagementScore(Browser* browser, const GURL& url, double score) { |
| site_engagement::SiteEngagementService::Get(browser->profile()) |
| ->ResetBaseScoreForURL(url, score); |
| } |
| |
| bool IsUrlShowing(Browser* browser) { |
| return !browser->location_bar_model()->GetFormattedFullURL().empty(); |
| } |
| |
| // Navigate to |url| and wait for the load to complete before returning. |
| // Simulates a link click navigation. We don't use |
| // ui_test_utils::NavigateToURL(const GURL&) because it simulates the user |
| // typing the URL, causing the site to have a site engagement score of at |
| // least LOW. |
| void NavigateToURLSync(Browser* browser, const GURL& url) { |
| content::TestNavigationObserver navigation_observer( |
| browser->tab_strip_model()->GetActiveWebContents(), 1); |
| |
| NavigateParams params(browser, url, ui::PAGE_TRANSITION_LINK); |
| params.initiator_origin = url::Origin::Create(GURL("about:blank")); |
| params.disposition = WindowOpenDisposition::CURRENT_TAB; |
| params.is_renderer_initiated = true; |
| ui_test_utils::NavigateToURL(¶ms); |
| |
| navigation_observer.Wait(); |
| } |
| |
| // Load given URL and verify that it loaded an interstitial and hid the URL. |
| void LoadAndCheckInterstitialAt(Browser* browser, const GURL& url) { |
| content::WebContents* web_contents = |
| browser->tab_strip_model()->GetActiveWebContents(); |
| content::WebContentsConsoleObserver console_observer(web_contents); |
| console_observer.SetPattern(kConsoleMessage); |
| |
| EXPECT_EQ(nullptr, GetCurrentInterstitial(web_contents)); |
| |
| NavigateToURLSync(browser, url); |
| EXPECT_EQ(LookalikeUrlBlockingPage::kTypeForTesting, |
| GetInterstitialType(web_contents)); |
| EXPECT_FALSE(IsUrlShowing(browser)); |
| |
| console_observer.Wait(); |
| EXPECT_TRUE( |
| base::MatchPattern(console_observer.GetMessageAt(0u), kConsoleMessage)); |
| } |
| |
| void SendInterstitialCommand(content::WebContents* web_contents, |
| SecurityInterstitialCommand command) { |
| GetCurrentInterstitial(web_contents) |
| ->CommandReceived(base::NumberToString(command)); |
| } |
| |
| void SendInterstitialCommandSync(Browser* browser, |
| SecurityInterstitialCommand command, |
| bool punycode_interstitial = false) { |
| content::WebContents* web_contents = |
| browser->tab_strip_model()->GetActiveWebContents(); |
| |
| EXPECT_EQ(LookalikeUrlBlockingPage::kTypeForTesting, |
| GetInterstitialType(web_contents)); |
| |
| content::TestNavigationObserver navigation_observer(web_contents, 1); |
| SendInterstitialCommand(web_contents, command); |
| navigation_observer.Wait(); |
| |
| EXPECT_EQ(nullptr, GetCurrentInterstitial(web_contents)); |
| if (punycode_interstitial) { |
| // "Back to safety" button on the punycode interstitial goes to the New |
| // Tab Page which doesn't show the URL. |
| EXPECT_FALSE(IsUrlShowing(browser)); |
| } else { |
| EXPECT_TRUE(IsUrlShowing(browser)); |
| } |
| } |
| |
| // Verify that no interstitial is shown, regardless of feature state. |
| void TestInterstitialNotShown(Browser* browser, const GURL& navigated_url) { |
| content::WebContents* web_contents = |
| browser->tab_strip_model()->GetActiveWebContents(); |
| |
| NavigateToURLSync(browser, navigated_url); |
| EXPECT_EQ(nullptr, GetCurrentInterstitial(web_contents)); |
| |
| // Navigate to an empty page. This will happen after any |
| // LookalikeUrlService tasks, so will effectively wait for those tasks to |
| // finish. |
| NavigateToURLSync(browser, GURL("about:blank")); |
| EXPECT_EQ(nullptr, GetCurrentInterstitial(web_contents)); |
| } |
| |
| namespace test { |
| #include "components/url_formatter/spoof_checks/top_domains/browsertest_domains-trie-inc.cc" |
| } |
| |
| } // namespace |
| |
| class LookalikeUrlNavigationThrottleBrowserTest |
| : public InProcessBrowserTest, |
| public testing::WithParamInterface<std::tuple<bool, bool, bool>> { |
| protected: |
| void SetUp() override { |
| std::vector<base::test::ScopedFeatureList::FeatureAndParams> |
| enabled_features; |
| std::vector<base::Feature> disabled_features; |
| |
| if (target_embedding_enabled()) { |
| base::FieldTrialParams params; |
| enabled_features.emplace_back( |
| lookalikes::features::kDetectTargetEmbeddingLookalikes, params); |
| enabled_features.emplace_back(features::kSignedHTTPExchange, params); |
| } else { |
| disabled_features.push_back( |
| lookalikes::features::kDetectTargetEmbeddingLookalikes); |
| } |
| |
| if (punycode_interstitial_enabled()) { |
| enabled_features.emplace_back( |
| lookalikes::features::kLookalikeInterstitialForPunycode, |
| base::FieldTrialParams()); |
| } else { |
| disabled_features.push_back( |
| lookalikes::features::kLookalikeInterstitialForPunycode); |
| } |
| |
| if (digital_asset_links_enabled()) { |
| enabled_features.emplace_back( |
| lookalikes::features::kLookalikeDigitalAssetLinks, |
| base::FieldTrialParams()); |
| } else { |
| disabled_features.push_back( |
| lookalikes::features::kLookalikeDigitalAssetLinks); |
| } |
| feature_list_.InitWithFeaturesAndParameters(enabled_features, |
| disabled_features); |
| reputation::InitializeSafetyTipConfig(); |
| InProcessBrowserTest::SetUp(); |
| } |
| |
| bool target_embedding_enabled() const { return std::get<0>(GetParam()); } |
| bool punycode_interstitial_enabled() const { return std::get<1>(GetParam()); } |
| bool digital_asset_links_enabled() const { return std::get<2>(GetParam()); } |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>(); |
| |
| const base::Time kNow = base::Time::FromDoubleT(1000); |
| test_clock_.SetNow(kNow); |
| |
| LookalikeUrlService* lookalike_service = |
| LookalikeUrlService::Get(browser()->profile()); |
| lookalike_service->SetClockForTesting(&test_clock_); |
| |
| // Use test top domain lists instead of the actual list. |
| url_formatter::IDNSpoofChecker::HuffmanTrieParams trie_params{ |
| test::kTopDomainsHuffmanTree, sizeof(test::kTopDomainsHuffmanTree), |
| test::kTopDomainsTrie, test::kTopDomainsTrieBits, |
| test::kTopDomainsRootPosition}; |
| url_formatter::IDNSpoofChecker::SetTrieParamsForTesting(trie_params); |
| |
| // Use test top 500 domain skeletons instead of the actual list. |
| Top500DomainsParams top500_params{ |
| test_top500_domains::kTop500EditDistanceSkeletons, |
| test_top500_domains::kNumTop500EditDistanceSkeletons}; |
| SetTop500DomainsParamsForTesting(top500_params); |
| |
| // Use test keywords instead of the actual list. This isn't strictly |
| // necessary as this test doesn't use reputation service, but it's good |
| // practice. |
| ReputationService* rep_service = |
| ReputationService::Get(browser()->profile()); |
| rep_service->SetSensitiveKeywordsForTesting( |
| test_top500_domains::kTopKeywords, |
| test_top500_domains::kNumTopKeywords); |
| } |
| |
| void TearDownOnMainThread() override { |
| url_formatter::IDNSpoofChecker::RestoreTrieParamsForTesting(); |
| ResetTop500DomainsParamsForTesting(); |
| |
| ReputationService* rep_service = |
| ReputationService::Get(browser()->profile()); |
| rep_service->ResetSensitiveKeywordsForTesting(); |
| } |
| |
| GURL GetURL(const char* hostname) const { |
| return embedded_test_server()->GetURL(hostname, "/title1.html"); |
| } |
| |
| GURL GetURLWithoutPath(const char* hostname) const { |
| return GetURL(hostname).GetWithEmptyPath(); |
| } |
| |
| GURL GetLongRedirect(const char* via_hostname1, |
| const char* via_hostname2, |
| const char* dest_hostname) const { |
| GURL dest = GetURL(dest_hostname); |
| GURL mid = embedded_test_server()->GetURL( |
| via_hostname2, "/server-redirect?" + dest.spec()); |
| return embedded_test_server()->GetURL(via_hostname1, |
| "/server-redirect?" + mid.spec()); |
| } |
| |
| // Checks that UKM recorded an event for each URL in |navigated_urls| with the |
| // given metric value. |
| template <typename T> |
| void CheckUkm(const std::vector<GURL>& navigated_urls, |
| const std::string& metric_name, |
| T metric_value) { |
| auto entries = test_ukm_recorder()->GetEntriesByName(UkmEntry::kEntryName); |
| ASSERT_EQ(navigated_urls.size(), entries.size()); |
| int entry_count = 0; |
| for (const auto* const entry : entries) { |
| test_ukm_recorder()->ExpectEntrySourceHasUrl(entry, |
| navigated_urls[entry_count]); |
| test_ukm_recorder()->ExpectEntryMetric(entry, metric_name, |
| static_cast<int>(metric_value)); |
| entry_count++; |
| } |
| } |
| |
| // Checks that UKM did not record any lookalike URL metrics. |
| void CheckNoUkm() { |
| EXPECT_TRUE( |
| test_ukm_recorder()->GetEntriesByName(UkmEntry::kEntryName).empty()); |
| } |
| |
| // Tests that the histogram event |expected_event| is recorded, the |
| // interstitial is displayed and clicking the link on the interstitial works. |
| void TestMetricsRecordedAndInterstitialShown( |
| Browser* browser, |
| const base::HistogramTester& histograms, |
| const GURL& navigated_url, |
| const GURL& expected_suggested_url, |
| NavigationSuggestionEvent expected_event, |
| bool expect_signed_exchange = false) { |
| history::HistoryService* const history_service = |
| HistoryServiceFactory::GetForProfile( |
| browser->profile(), ServiceAccessType::EXPLICIT_ACCESS); |
| ui_test_utils::WaitForHistoryToLoad(history_service); |
| |
| LoadAndCheckInterstitialAt(browser, navigated_url); |
| |
| if (expect_signed_exchange) { |
| LookalikeUrlBlockingPage* interstitial = |
| static_cast<LookalikeUrlBlockingPage*>(GetCurrentInterstitial( |
| browser->tab_strip_model()->GetActiveWebContents())); |
| EXPECT_TRUE(interstitial->is_signed_exchange_for_testing()); |
| } |
| |
| SendInterstitialCommandSync(browser, |
| SecurityInterstitialCommand::CMD_DONT_PROCEED); |
| EXPECT_EQ(expected_suggested_url, |
| browser->tab_strip_model()->GetActiveWebContents()->GetURL()); |
| |
| // Clicking the link in the interstitial should also remove the original |
| // URL from history. |
| ui_test_utils::HistoryEnumerator enumerator(browser->profile()); |
| EXPECT_FALSE(base::Contains(enumerator.urls(), navigated_url)); |
| |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount(lookalikes::kHistogramName, expected_event, 1); |
| |
| histograms.ExpectTotalCount(kInterstitialDecisionMetric, 2); |
| histograms.ExpectBucketCount(kInterstitialDecisionMetric, |
| MetricsHelper::SHOW, 1); |
| histograms.ExpectBucketCount(kInterstitialDecisionMetric, |
| MetricsHelper::DONT_PROCEED, 1); |
| |
| histograms.ExpectTotalCount(kInterstitialInteractionMetric, 1); |
| histograms.ExpectBucketCount(kInterstitialInteractionMetric, |
| MetricsHelper::TOTAL_VISITS, 1); |
| } |
| |
| // Tests that the histogram event |expected_event| is recorded, the |
| // interstitial is displayed and clicking "Back to safety" on the interstitial |
| // works. |
| void TestPunycodeInterstitialShown(Browser* browser, |
| const GURL& navigated_url, |
| NavigationSuggestionEvent expected_event) { |
| base::HistogramTester histograms; |
| |
| history::HistoryService* const history_service = |
| HistoryServiceFactory::GetForProfile( |
| browser->profile(), ServiceAccessType::EXPLICIT_ACCESS); |
| ui_test_utils::WaitForHistoryToLoad(history_service); |
| |
| LoadAndCheckInterstitialAt(browser, navigated_url); |
| |
| // Clicking "Back to safety" should go to the new tab page. |
| SendInterstitialCommandSync(browser, |
| SecurityInterstitialCommand::CMD_DONT_PROCEED, |
| /*punycode_interstitial=*/true); |
| EXPECT_EQ(GURL(chrome::kChromeUINewTabURL), |
| browser->tab_strip_model()->GetActiveWebContents()->GetURL()); |
| |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount(lookalikes::kHistogramName, expected_event, 1); |
| |
| histograms.ExpectTotalCount(kInterstitialDecisionMetric, 2); |
| histograms.ExpectBucketCount(kInterstitialDecisionMetric, |
| MetricsHelper::SHOW, 1); |
| histograms.ExpectBucketCount(kInterstitialDecisionMetric, |
| MetricsHelper::DONT_PROCEED, 1); |
| |
| histograms.ExpectTotalCount(kInterstitialInteractionMetric, 1); |
| histograms.ExpectBucketCount(kInterstitialInteractionMetric, |
| MetricsHelper::TOTAL_VISITS, 1); |
| } |
| |
| // Tests that the histogram event |expected_event| is recorded, the |
| // interstitial is displayed and clicking through the interstitial works. |
| void TestHistogramEventsRecordedWhenInterstitialIgnored( |
| Browser* browser, |
| base::HistogramTester* histograms, |
| const GURL& navigated_url, |
| NavigationSuggestionEvent expected_event) { |
| |
| history::HistoryService* const history_service = |
| HistoryServiceFactory::GetForProfile( |
| browser->profile(), ServiceAccessType::EXPLICIT_ACCESS); |
| ui_test_utils::WaitForHistoryToLoad(history_service); |
| |
| LoadAndCheckInterstitialAt(browser, navigated_url); |
| |
| // Clicking the ignore button in the interstitial should remove the |
| // interstitial and navigate to the original URL. |
| SendInterstitialCommandSync(browser, |
| SecurityInterstitialCommand::CMD_PROCEED); |
| EXPECT_EQ(navigated_url, |
| browser->tab_strip_model()->GetActiveWebContents()->GetURL()); |
| |
| // Clicking the link should cause the original URL to appear in history. |
| ui_test_utils::HistoryEnumerator enumerator(browser->profile()); |
| EXPECT_TRUE(base::Contains(enumerator.urls(), navigated_url)); |
| |
| histograms->ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms->ExpectBucketCount(lookalikes::kHistogramName, expected_event, |
| 1); |
| |
| histograms->ExpectTotalCount(kInterstitialDecisionMetric, 2); |
| histograms->ExpectBucketCount(kInterstitialDecisionMetric, |
| MetricsHelper::SHOW, 1); |
| histograms->ExpectBucketCount(kInterstitialDecisionMetric, |
| MetricsHelper::PROCEED, 1); |
| |
| histograms->ExpectTotalCount(kInterstitialInteractionMetric, 1); |
| histograms->ExpectBucketCount(kInterstitialInteractionMetric, |
| MetricsHelper::TOTAL_VISITS, 1); |
| |
| TestInterstitialNotShown(browser, navigated_url); |
| } |
| |
| ukm::TestUkmRecorder* test_ukm_recorder() { return test_ukm_recorder_.get(); } |
| |
| base::SimpleTestClock* test_clock() { return &test_clock_; } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_; |
| base::SimpleTestClock test_clock_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| LookalikeUrlNavigationThrottleBrowserTest, |
| testing::Combine(testing::Bool() /* target_embedding_enabled */, |
| testing::Bool() /* punycode_interstitial_enabled */, |
| testing::Bool() /* digital_asset_links_enabled */)); |
| |
| // Navigating to a non-IDN shouldn't show an interstitial or record metrics. |
| // TODO(https://crbug.com1207573): re-enable when flakiness is fixed. |
| #if defined(OS_LINUX) || defined(OS_WIN) || defined(OS_MAC) |
| #define MAYBE_NonIdn_NoMatch DISABLED_NonIdn_NoMatch |
| #else |
| #define MAYBE_NonIdn_NoMatch NonIdn_NoMatch |
| #endif |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| MAYBE_NonIdn_NoMatch) { |
| TestInterstitialNotShown(browser(), GetURL("google.com")); |
| CheckNoUkm(); |
| } |
| |
| // Navigating to a domain whose visual representation does not look like a |
| // top domain shouldn't show an interstitial or record metrics. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| NonTopDomainIdn_NoInterstitial) { |
| TestInterstitialNotShown(browser(), GetURL("éxample.com")); |
| CheckNoUkm(); |
| } |
| |
| // If the user has engaged with the domain before, metrics shouldn't be recorded |
| // and the interstitial shouldn't be shown, even if the domain is visually |
| // similar to a top domain. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_TopDomain_EngagedSite_NoMatch) { |
| const GURL url = GetURL("googlé.com"); |
| SetEngagementScore(browser(), url, kHighEngagement); |
| TestInterstitialNotShown(browser(), url); |
| CheckNoUkm(); |
| } |
| |
| // Navigate to a domain whose visual representation looks like a top domain. |
| // This should record metrics. It should also show a lookalike warning |
| // interstitial if configured via a feature param. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_TopDomain_Match) { |
| const GURL kNavigatedUrl = GetURL("googlé.com"); |
| const GURL kExpectedSuggestedUrl = GetURLWithoutPath("google.com"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| |
| base::HistogramTester histograms; |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kExpectedSuggestedUrl, |
| NavigationSuggestionEvent::kMatchSkeletonTop500); |
| |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kSkeletonMatchTop500); |
| CheckUkm({kNavigatedUrl}, "TriggeredByInitialUrl", false); |
| } |
| |
| // Navigate to a domain that would trigger the warning, but doesn't because it |
| // fails-safe when the allowlist isn't available. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| NoMatchOnAllowlistMissing) { |
| const GURL kNavigatedUrl = GetURL("googlé.com"); |
| |
| // Clear out any existing proto. |
| reputation::SetSafetyTipsRemoteConfigProto(nullptr); |
| |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| CheckNoUkm(); |
| } |
| |
| // Embedding a top domain should show an interstitial when enabled. If disabled |
| // this would trigger safety tips when target embedding feature parameter is |
| // enabled for safety tips. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| TargetEmbedding_TopDomain_Match) { |
| const GURL kNavigatedUrl = GetURL("google.com-test.com"); |
| const GURL kExpectedSuggestedUrl = GetURLWithoutPath("google.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| base::HistogramTester histograms; |
| |
| // |TestMetricsRecordedAndInterstitialShown| assumes everything should be |
| // recorded if target embedding is not disabled. But only for target embedding |
| // checks, if TargetEmbedding is not explicitly enabled, it should be treated |
| // just like it is disabled. So we make sure an interstitial is not shown if |
| // target embedding is not enabled. And defer to |
| // |TestMetricsRecordedAndInterstitialShown| otherwise. |
| if (!target_embedding_enabled()) { |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount( |
| lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchTargetEmbedding, 1); |
| } else { |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kExpectedSuggestedUrl, |
| NavigationSuggestionEvent::kMatchTargetEmbedding); |
| } |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kTargetEmbedding); |
| CheckUkm({kNavigatedUrl}, "TriggeredByInitialUrl", false); |
| } |
| |
| // Same as TargetEmbedding_TopDomain_Match, but has a redirect where the first |
| // and last URLs are both target embedding matches. Should only record |
| // metrics for the first URL. Regression test for crbug.com/1136296. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| TargetEmbedding_TopDomain_Redirect_Match) { |
| const GURL kNavigatedUrl = GetLongRedirect("google.com-test.com", "site.test", |
| "youtube.com-test.com"); |
| // UKM will record the final URL of the redirect: |
| const GURL kLastUrl = GetURL("youtube.com-test.com"); |
| const GURL kExpectedSuggestedUrl = GetURLWithoutPath("google.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| base::HistogramTester histograms; |
| |
| // |TestMetricsRecordedAndInterstitialShown| assumes everything should be |
| // recorded if target embedding is not disabled. But only for target embedding |
| // checks, if TargetEmbedding is not explicitly enabled, it should be treated |
| // just like it is disabled. So we make sure an interstitial is not shown if |
| // target embedding is not enabled. And defer to |
| // |TestMetricsRecordedAndInterstitialShown| otherwise. |
| if (!target_embedding_enabled()) { |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount( |
| lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchTargetEmbedding, 1); |
| } else { |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kExpectedSuggestedUrl, |
| NavigationSuggestionEvent::kMatchTargetEmbedding); |
| } |
| CheckUkm({kLastUrl}, "MatchType", LookalikeUrlMatchType::kTargetEmbedding); |
| CheckUkm({kLastUrl}, "TriggeredByInitialUrl", true); |
| } |
| |
| // Target embedding should not trigger on allowlisted embedder domains. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| TargetEmbedding_EmbedderAllowlist) { |
| const GURL kNavigatedUrl = GetURL("google.com.allowlisted.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| reputation::SetSafetyTipAllowlistPatterns({"allowlisted.com/"}, {}, {}); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| CheckNoUkm(); |
| } |
| |
| // Target embedding should not trigger on allowlisted target domains. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| TargetEmbedding_TargetAllowlist) { |
| const GURL kNavigatedUrl = GetURL("foo.scholar.google.com.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| reputation::SetSafetyTipAllowlistPatterns({}, {"scholar\\.google\\.com"}, {}); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| CheckNoUkm(); |
| } |
| |
| // Target embedding shouldn't trigger on component-delivered common words. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| TargetEmbedding_ComponentCommonWords) { |
| const GURL kNavigatedUrl = GetURL("google.com.example.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| reputation::SetSafetyTipAllowlistPatterns({}, {}, {"google"}); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| CheckNoUkm(); |
| } |
| |
| // Navigate to a domain target embedding a domain with no separators, but that |
| // matches the target allowlist. Regression test for crbug.com/1127450. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| TargetEmbedding_TargetAllowlistWithNoSeparators) { |
| const GURL kNavigatedUrl = GetURL("googlecom.example.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| reputation::SetSafetyTipAllowlistPatterns({}, {"google\\.com"}, {}); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| CheckNoUkm(); |
| } |
| |
| // Similar to Idn_TopDomain_Match but the domain is not in top 500. Should not |
| // show an interstitial, but should still record metrics. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_TopDomain_Match_Not500) { |
| const GURL kNavigatedUrl = GetURL("googlé.sk"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| |
| base::HistogramTester histograms; |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount(lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchSkeletonTop5k, |
| 1); |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kSkeletonMatchTop5k); |
| } |
| |
| // Same as Idn_TopDomain_Match, but this time the domain contains characters |
| // from different scripts, failing the checks in IDN spoof checker before |
| // reaching the top domain check. In this case, the end result is the same, but |
| // the reason we fall back to punycode is different. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_TopDomainMixedScript_Match) { |
| const GURL kNavigatedUrl = GetURL("аррӏе.com"); |
| const GURL kExpectedSuggestedUrl = GetURLWithoutPath("apple.com"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| |
| base::HistogramTester histograms; |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kExpectedSuggestedUrl, |
| NavigationSuggestionEvent::kMatchSkeletonTop500); |
| |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kSkeletonMatchTop500); |
| } |
| |
| // The navigated domain will fall back to punycode because it fails standard |
| // ICU spoof checks in IDN spoof checker. If the feature flag to show |
| // interstitials for punycode fallback is enabled, this will show a punycode |
| // interstitial. Otherwise, the navigation won't be blocked. |
| // but there won't be an interstitial because the domain |
| // doesn't match a top domain. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Punycode_NoSuggestedUrl_NoInterstitial) { |
| const GURL kNavigatedUrl = GetURL("ɴoτ-τoρ-ďoᛖaiɴ.com"); |
| |
| if (!punycode_interstitial_enabled()) { |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| } else { |
| TestPunycodeInterstitialShown( |
| browser(), kNavigatedUrl, |
| NavigationSuggestionEvent::kFailedSpoofChecks); |
| } |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kFailedSpoofChecks); |
| } |
| |
| // The navigated domain will fall back to punycode because it fails spoof checks |
| // in IDN spoof checker. The heuristic that changes this domain to punycode |
| // (latin middle dot) is configured to show a punycode interstitial. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Punycode_NoSuggestedUrl_Interstitial) { |
| if (!punycode_interstitial_enabled()) { |
| return; |
| } |
| // Navigate to a domain that doesn't trigger target embedding: |
| const GURL kNavigatedUrl = GetURL("example·com.com"); |
| TestPunycodeInterstitialShown(browser(), kNavigatedUrl, |
| NavigationSuggestionEvent::kFailedSpoofChecks); |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kFailedSpoofChecks); |
| } |
| |
| // The navigated domain will fall back to punycode because it fails spoof checks |
| // in IDN spoof checker. The heuristic that changes this domain to punycode |
| // (latin middle dot) is configured to show a punycode interstitial. The domain |
| // is also caught by the target embedding heuristic. Target embedding should |
| // take priority. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| PunycodeAndTargetEmbedding_NoSuggestedUrl_Interstitial) { |
| if (!(target_embedding_enabled() && punycode_interstitial_enabled())) { |
| return; |
| } |
| // Navigate to a domain that triggers target embedding: |
| const GURL kNavigatedUrl = GetURL("google·com.com"); |
| const GURL kExpectedSuggestedUrl = GetURLWithoutPath("google.com"); |
| base::HistogramTester histograms; |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kExpectedSuggestedUrl, |
| NavigationSuggestionEvent::kMatchTargetEmbedding); |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kTargetEmbedding); |
| } |
| |
| // The navigated domain itself is a top domain or a subdomain of a top domain. |
| // Should not record metrics. The top domain list doesn't contain any IDN, so |
| // this only tests the case where the subdomains are IDNs. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| TopDomainIdnSubdomain_NoMatch) { |
| TestInterstitialNotShown(browser(), GetURL("tést.google.com")); |
| CheckNoUkm(); |
| |
| // blogspot.com is a private registry, so the eTLD+1 of "tést.blogspot.com" is |
| // itself, instead of just "blogspot.com". This is different than |
| // tést.google.com whose eTLD+1 is google.com, and it should be handled |
| // correctly. |
| TestInterstitialNotShown(browser(), GetURL("tést.blogspot.com")); |
| CheckNoUkm(); |
| } |
| |
| // Schemes other than HTTP and HTTPS should be ignored. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| TopDomainChromeUrl_NoMatch) { |
| TestInterstitialNotShown(browser(), GURL("chrome://googlé.com")); |
| CheckNoUkm(); |
| } |
| |
| // Navigate to a domain within an edit distance of 1 to an engaged domain. |
| // This should record metrics, but should not show a lookalike warning |
| // interstitial yet. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| EditDistance_EngagedDomain_Match) { |
| base::HistogramTester histograms; |
| SetEngagementScore(browser(), GURL("https://test-site.com"), kHighEngagement); |
| |
| // The skeleton of this domain is one 1 edit away from the skeleton of |
| // test-site.com. |
| const GURL kNavigatedUrl = GetURL("best-sité.com"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| // Advance clock to force a fetch of new engaged sites list. |
| test_clock()->Advance(base::Hours(1)); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount( |
| lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchEditDistanceSiteEngagement, 1); |
| |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kEditDistanceSiteEngagement); |
| } |
| |
| // Navigate to a domain with a character swap of 1 to an engaged domain. |
| // This should record metrics, but should not show a lookalike warning |
| // interstitial yet. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| CharacterSwap_EngagedDomain_SkeletonMatch) { |
| base::HistogramTester histograms; |
| SetEngagementScore(browser(), GURL("https://character-swap.com"), |
| kHighEngagement); |
| |
| // The skeleton of this domain is one character swap from the skeleton of |
| // character-swap.com. |
| const GURL kNavigatedUrl = GetURL("character-wsap.com"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| // Advance clock to force a fetch of new engaged sites list. |
| test_clock()->Advance(base::Hours(1)); |
| |
| content::WebContents* tab = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| content::WebContentsConsoleObserver console_observer(tab); |
| console_observer.SetPattern("Chrome has determined that*character-wsap.com*"); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| console_observer.Wait(); |
| |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount( |
| lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchCharacterSwapSiteEngagement, 1); |
| |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kCharacterSwapSiteEngagement); |
| } |
| |
| // Same as CharacterSwap_EngagedDomain_SkeletonMatch, but this time |
| // the match is on the actual hostnames and not skeletons. Note that |
| // the skeletons of example.com and éxaplme.com don't have exactly |
| // one character swap (exarnple.com and exaprnle.com) |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| CharacterSwap_EngagedDomain_HostnameMatch) { |
| base::HistogramTester histograms; |
| SetEngagementScore(browser(), GURL("https://example.com"), kHighEngagement); |
| const GURL kNavigatedUrl = GetURL("éxapmle.com"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| // Advance clock to force a fetch of new engaged sites list. |
| test_clock()->Advance(base::Hours(1)); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount( |
| lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchCharacterSwapSiteEngagement, 1); |
| |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kCharacterSwapSiteEngagement); |
| } |
| |
| // Navigate to a domain within an edit distance of 1 to a top domain. |
| // This should record metrics, but should not show a lookalike warning |
| // interstitial yet. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| EditDistance_TopDomain_Match) { |
| base::HistogramTester histograms; |
| |
| // The skeleton of this domain, gooogle.corn, is one 1 edit away from |
| // google.corn, the skeleton of google.com. |
| const GURL kNavigatedUrl = GetURL("goooglé.com"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount(lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchEditDistance, |
| 1); |
| |
| CheckUkm({kNavigatedUrl}, "MatchType", LookalikeUrlMatchType::kEditDistance); |
| } |
| |
| // Navigate to a domain within a character swap of 1 to a top domain. |
| // This should record metrics, but should not show a lookalike warning |
| // interstitial yet. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| CharacterSwap_TopDomain_Match) { |
| base::HistogramTester histograms; |
| const GURL kNavigatedUrl = GetURL("goolge.com"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount( |
| lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchCharacterSwapTop500, 1); |
| |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kCharacterSwapTop500); |
| } |
| |
| // Navigate to a domain within a character swap of 1 to a top domain, |
| // but the character swap is at the registry. This should not be flagged |
| // as a character swap match. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| CharacterSwap_TopDomainWithDifferentRegistry_NoMatch) { |
| base::HistogramTester histograms; |
| // google.sr is within one character swap of google.rs which is a top domain. |
| const GURL kNavigatedUrl = GetURL("google.sr"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 0); |
| CheckNoUkm(); |
| } |
| |
| // Navigate to a domain within an edit distance of 1 to a top domain, but that |
| // matches the allowlist. This should neither record metrics nor show an |
| // interstitial. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| EditDistance_TopDomain_Target_Allowlist) { |
| base::HistogramTester histograms; |
| reputation::SetSafetyTipAllowlistPatterns({}, {"google\\.com"}, {}); |
| |
| // The skeleton of this domain, gooogle.corn, is one 1 edit away from |
| // google.corn, the skeleton of google.com. |
| const GURL kNavigatedUrl = GetURL("goooglé.com"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 0); |
| CheckNoUkm(); |
| } |
| |
| // Navigate to a domain within an edit distance of 1 to an engaged domain, but |
| // that matches the allowlist. This should neither record metrics nor show an |
| // interstitial. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| EditDistance_EngagedDomain_Target_Allowlist) { |
| base::HistogramTester histograms; |
| SetEngagementScore(browser(), GURL("https://test-site.com"), kHighEngagement); |
| reputation::SetSafetyTipAllowlistPatterns({}, {"test-site\\.com"}, {}); |
| |
| // The skeleton of this domain is one 1 edit away from the skeleton of |
| // test-site.com. |
| const GURL kNavigatedUrl = GetURL("best-sité.com"); |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| // Advance clock to force a fetch of new engaged sites list. |
| test_clock()->Advance(base::Hours(1)); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 0); |
| CheckNoUkm(); |
| } |
| |
| // Tests negative examples for the edit distance. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| EditDistance_TopDomain_NoMatch) { |
| // Matches google.com.tr but only differs in registry. |
| TestInterstitialNotShown(browser(), GetURL("google.com.tw")); |
| CheckNoUkm(); |
| |
| // Matches academia.edu but is a top domain itself. |
| TestInterstitialNotShown(browser(), GetURL("academic.ru")); |
| CheckNoUkm(); |
| |
| // Matches ask.com but is too short. |
| TestInterstitialNotShown(browser(), GetURL("bsk.com")); |
| CheckNoUkm(); |
| } |
| |
| // Tests negative examples for the edit distance with engaged sites. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| EditDistance_SiteEngagement_NoMatch) { |
| SetEngagementScore(browser(), GURL("https://test-site.com.tr"), |
| kHighEngagement); |
| SetEngagementScore(browser(), GURL("https://1234.com"), kHighEngagement); |
| SetEngagementScore(browser(), GURL("https://gooogle.com"), kHighEngagement); |
| // Advance clock to force a fetch of new engaged sites list. |
| test_clock()->Advance(base::Hours(1)); |
| |
| // Matches test-site.com.tr but only differs in registry. |
| TestInterstitialNotShown(browser(), GetURL("test-site.com.tw")); |
| CheckNoUkm(); |
| |
| // Matches gooogle.com but is a top domain itself. |
| TestInterstitialNotShown(browser(), GetURL("google.com")); |
| CheckNoUkm(); |
| |
| // Matches 1234.com but is too short. |
| TestInterstitialNotShown(browser(), GetURL("123.com")); |
| CheckNoUkm(); |
| } |
| |
| // Test that the heuristics are not triggered with net errors. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| NetError_SiteEngagement_Interstitial) { |
| // Create a test server that returns invalid responses. |
| net::EmbeddedTestServer custom_test_server; |
| custom_test_server.RegisterRequestHandler( |
| base::BindRepeating(&NetworkErrorResponseHandler)); |
| ASSERT_TRUE(custom_test_server.Start()); |
| |
| SetEngagementScore(browser(), GURL("http://site1.com"), kHighEngagement); |
| // Advance clock to force a fetch of new engaged sites list. |
| test_clock()->Advance(base::Hours(1)); |
| |
| TestInterstitialNotShown( |
| browser(), custom_test_server.GetURL("sité1.com", "/title1.html")); |
| } |
| |
| // Same as NetError_SiteEngagement_Interstitial, but triggered by a top domain. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| NetError_TopDomain_Interstitial) { |
| // Create a test server that returns invalid responses. |
| net::EmbeddedTestServer custom_test_server; |
| custom_test_server.RegisterRequestHandler( |
| base::BindRepeating(&NetworkErrorResponseHandler)); |
| ASSERT_TRUE(custom_test_server.Start()); |
| TestInterstitialNotShown(browser(), |
| custom_test_server.GetURL("googlé.com", "/")); |
| } |
| |
| // TODO(https://crbug.com/1122078): Enable test when MacOS flake is fixed. |
| // TODO(https://crbug.com/1106402): Enable test when Win/Linux flake is fixed. |
| #if defined(OS_MAC) || defined(OS_WIN) || defined(OS_LINUX) |
| #define MAYBE_Idn_SiteEngagement_Match DISABLED_Idn_SiteEngagement_Match |
| #else |
| #define MAYBE_Idn_SiteEngagement_Match Idn_SiteEngagement_Match |
| #endif |
| |
| // Navigate to a domain whose visual representation looks like a domain with a |
| // site engagement score above a certain threshold. This should record metrics. |
| // It should also show lookalike warning interstitial if configured via |
| // a feature param. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| MAYBE_Idn_SiteEngagement_Match) { |
| const char* const kEngagedSites[] = { |
| "http://site1.com", "http://www.site2.com", "http://sité3.com", |
| "http://www.sité4.com"}; |
| |
| for (const char* const kSite : kEngagedSites) { |
| SetEngagementScore(browser(), GURL(kSite), kHighEngagement); |
| } |
| |
| // The domains here should not be private domains (e.g. site.test), otherwise |
| // they might test the wrong thing. Also note that site5.com is in the top |
| // domain list, so it shouldn't be used here. |
| const struct SiteEngagementTestCase { |
| const char* const navigated; |
| const char* const suggested; |
| } kSiteEngagementTestCases[] = { |
| {"sité1.com", "site1.com"}, |
| {"mail.www.sité1.com", "site1.com"}, |
| // Same as above two but ending with dots. |
| {"sité1.com.", "site1.com"}, |
| {"mail.www.sité1.com.", "site1.com"}, |
| |
| // These should match since the comparison uses eTLD+1s. |
| {"sité2.com", "site2.com"}, |
| {"mail.sité2.com", "site2.com"}, |
| |
| {"síté3.com", "sité3.com"}, |
| {"mail.síté3.com", "sité3.com"}, |
| |
| {"síté4.com", "sité4.com"}, |
| {"mail.síté4.com", "sité4.com"}, |
| }; |
| |
| std::vector<GURL> ukm_urls; |
| for (const auto& test_case : kSiteEngagementTestCases) { |
| const GURL kNavigatedUrl = GetURL(test_case.navigated); |
| const GURL kExpectedSuggestedUrl = GetURLWithoutPath(test_case.suggested); |
| |
| // Even if the navigated site has a low engagement score, it should be |
| // considered for lookalike suggestions. |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| // Advance the clock to force LookalikeUrlService to fetch a new engaged |
| // site list. |
| test_clock()->Advance(base::Hours(1)); |
| |
| base::HistogramTester histograms; |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kExpectedSuggestedUrl, |
| NavigationSuggestionEvent::kMatchSiteEngagement); |
| |
| ukm_urls.push_back(kNavigatedUrl); |
| CheckUkm(ukm_urls, "MatchType", LookalikeUrlMatchType::kSiteEngagement); |
| } |
| } |
| |
| // The site redirects to the matched site, this should not show |
| // an interstitial. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_SiteEngagement_SafeRedirect) { |
| const GURL kExpectedSuggestedUrl = GetURLWithoutPath("site1.com"); |
| const GURL kNavigatedUrl = embedded_test_server()->GetURL( |
| "sité1.com", "/server-redirect?" + kExpectedSuggestedUrl.spec()); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| SetEngagementScore(browser(), kExpectedSuggestedUrl, kHighEngagement); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| } |
| |
| // The site redirects to the matched site, but the redirect chain has more than |
| // two redirects. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_SiteEngagement_MidRedirectSpoofsIgnored) { |
| const GURL kFinalUrl = GetURLWithoutPath("site1.com"); |
| const GURL kMidUrl = embedded_test_server()->GetURL( |
| "sité1.com", "/server-redirect?" + kFinalUrl.spec()); |
| const GURL kNavigatedUrl = embedded_test_server()->GetURL( |
| "other-site.test", "/server-redirect?" + kMidUrl.spec()); |
| |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| SetEngagementScore(browser(), kFinalUrl, kHighEngagement); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| CheckNoUkm(); |
| } |
| |
| // The site is allowed by the component updater. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| AllowedByComponentUpdater) { |
| reputation::SetSafetyTipAllowlistPatterns( |
| {"xn--googl-fsa.com/", // googlé.com in punycode |
| "site.test/", "another-site.test/"}, |
| {}, {}); |
| TestInterstitialNotShown(browser(), GetURL("googlé.com")); |
| CheckNoUkm(); |
| |
| // Try a non-HTTP URL. Shouldn't crash. |
| TestInterstitialNotShown(browser(), GURL("data:text/html, test")); |
| CheckNoUkm(); |
| } |
| |
| // The site is allowed by enterprise policy. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| AllowedByPolicy) { |
| const GURL kNavigatedUrl = GetURL("xn--googl-fsa.com"); |
| SetEnterpriseAllowlistForTesting(browser()->profile()->GetPrefs(), |
| {"xn--googl-fsa.com"}); |
| |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| CheckNoUkm(); |
| } |
| |
| // Tests negative examples for all heuristics. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| NonUniqueDomains_NoMatch) { |
| // Unknown registry. |
| TestInterstitialNotShown(browser(), GetURL("google.cóm")); |
| CheckNoUkm(); |
| |
| // Engaged site is localhost, navigated site has unknown registry. This |
| // is intended to test that nonunique domains in the engaged site list is |
| // filtered out. However, it doesn't quite test that: We'll bail out early |
| // because the navigated site has unknown registry (and not because there is |
| // no engaged nonunique site). |
| SetEngagementScore(browser(), GURL("http://localhost6.localhost"), |
| kHighEngagement); |
| test_clock()->Advance(base::Hours(1)); |
| // The skeleton of this URL is localhost6.localpost which is at one edit |
| // distance from localhost6.localhost. We use localpost here to prevent an |
| // early return in LookalikeUrlNavigationThrottle::HandleThrottleRequest(). |
| TestInterstitialNotShown(browser(), GURL("http://localhóst6.localpost")); |
| CheckNoUkm(); |
| } |
| |
| // Navigate to a domain whose visual representation looks both like a domain |
| // with a site engagement score and also a top domain. This should record |
| // metrics for a site engagement match because of the order of checks. It should |
| // also show lookalike warning interstitial if configured via a feature param. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_SiteEngagementAndTopDomain_Match) { |
| const GURL kNavigatedUrl = GetURL("googlé.com"); |
| const GURL kExpectedSuggestedUrl = GetURLWithoutPath("google.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| SetEngagementScore(browser(), kExpectedSuggestedUrl, kHighEngagement); |
| |
| // Advance the clock to force LookalikeUrlService to fetch a new engaged |
| // site list. |
| test_clock()->Advance(base::Hours(1)); |
| |
| base::HistogramTester histograms; |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kExpectedSuggestedUrl, |
| NavigationSuggestionEvent::kMatchSiteEngagement); |
| |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kSiteEngagement); |
| } |
| |
| // Similar to Idn_SiteEngagement_Match, but tests a single domain. Also checks |
| // that the list of engaged sites in incognito and the main profile don't affect |
| // each other. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_SiteEngagement_Match_Incognito) { |
| const GURL kNavigatedUrl = GetURL("sité1.com"); |
| const GURL kEngagedUrl = GetURLWithoutPath("site1.com"); |
| |
| // Set high engagement scores in the main profile and low engagement scores |
| // in incognito. Main profile should record metrics, incognito shouldn't. |
| Browser* incognito = CreateIncognitoBrowser(); |
| LookalikeUrlService::Get(incognito->profile()) |
| ->SetClockForTesting(test_clock()); |
| SetEngagementScore(browser(), kEngagedUrl, kHighEngagement); |
| SetEngagementScore(incognito, kEngagedUrl, kLowEngagement); |
| |
| std::vector<GURL> ukm_urls; |
| // Main profile should record metrics because there are engaged sites. |
| { |
| // Advance the clock to force LookalikeUrlService to fetch a new engaged |
| // site list. |
| test_clock()->Advance(base::Hours(1)); |
| base::HistogramTester histograms; |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kEngagedUrl, |
| NavigationSuggestionEvent::kMatchSiteEngagement); |
| |
| ukm_urls.push_back(kNavigatedUrl); |
| CheckUkm(ukm_urls, "MatchType", LookalikeUrlMatchType::kSiteEngagement); |
| } |
| |
| // Incognito shouldn't record metrics because there are no engaged sites. |
| { |
| base::HistogramTester histograms; |
| test_clock()->Advance(base::Hours(1)); |
| TestInterstitialNotShown(incognito, kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 0); |
| } |
| |
| // Now reverse the scores: Set low engagement in the main profile and high |
| // engagement in incognito. |
| SetEngagementScore(browser(), kEngagedUrl, kLowEngagement); |
| SetEngagementScore(incognito, kEngagedUrl, kHighEngagement); |
| |
| // Incognito should start recording metrics and main profile should stop. |
| { |
| test_clock()->Advance(base::Hours(1)); |
| |
| base::HistogramTester histograms; |
| TestMetricsRecordedAndInterstitialShown( |
| incognito, histograms, kNavigatedUrl, kEngagedUrl, |
| NavigationSuggestionEvent::kMatchSiteEngagement); |
| ukm_urls.push_back(kNavigatedUrl); |
| CheckUkm(ukm_urls, "MatchType", LookalikeUrlMatchType::kSiteEngagement); |
| } |
| |
| // Main profile shouldn't record metrics because there are no engaged sites. |
| { |
| base::HistogramTester histograms; |
| test_clock()->Advance(base::Hours(1)); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 0); |
| } |
| } |
| |
| // Test that navigations to a site with a high engagement score shouldn't |
| // record metrics or show interstitial. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_SiteEngagement_Match_IgnoreHighlyEngagedSite) { |
| base::HistogramTester histograms; |
| SetEngagementScore(browser(), GURL("http://site-not-in-top-domain-list.com"), |
| kHighEngagement); |
| const GURL high_engagement_url = GetURL("síte-not-ín-top-domaín-líst.com"); |
| SetEngagementScore(browser(), high_engagement_url, kHighEngagement); |
| TestInterstitialNotShown(browser(), high_engagement_url); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 0); |
| } |
| |
| // Test that an engaged site with a scheme other than HTTP or HTTPS should be |
| // ignored. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Idn_SiteEngagement_IgnoreChromeUrl) { |
| base::HistogramTester histograms; |
| SetEngagementScore(browser(), |
| GURL("chrome://site-not-in-top-domain-list.com"), |
| kHighEngagement); |
| const GURL low_engagement_url("http://síte-not-ín-top-domaín-líst.com"); |
| SetEngagementScore(browser(), low_engagement_url, kLowEngagement); |
| TestInterstitialNotShown(browser(), low_engagement_url); |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 0); |
| } |
| |
| // IDNs with a single label should be properly handled. There are two cases |
| // where this might occur: |
| // 1. The navigated URL is an IDN with a single label. |
| // 2. One of the engaged sites is an IDN with a single label. |
| // Neither of these should cause a crash. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| IdnWithSingleLabelShouldNotCauseACrash) { |
| base::HistogramTester histograms; |
| |
| // Case 1: Navigating to an IDN with a single label shouldn't cause a crash. |
| TestInterstitialNotShown(browser(), GetURL("é")); |
| |
| // Case 2: An IDN with a single label with a site engagement score shouldn't |
| // cause a crash. |
| SetEngagementScore(browser(), GURL("http://tést"), kHighEngagement); |
| TestInterstitialNotShown(browser(), GetURL("tést.com")); |
| |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 0); |
| CheckNoUkm(); |
| } |
| |
| // Ensure that dismissing the interstitial works, and the result is remembered |
| // in the current tab. This should record metrics on the first visit, but not |
| // the second. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Interstitial_Dismiss) { |
| base::HistogramTester histograms; |
| |
| const GURL kNavigatedUrl = GetURL("sité1.com"); |
| const GURL kEngagedUrl = GetURL("site1.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| SetEngagementScore(browser(), kEngagedUrl, kHighEngagement); |
| |
| TestHistogramEventsRecordedWhenInterstitialIgnored( |
| browser(), &histograms, kNavigatedUrl, |
| NavigationSuggestionEvent::kMatchSiteEngagement); |
| |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| LookalikeUrlMatchType::kSiteEngagement); |
| } |
| |
| // Navigate to lookalike domains that redirect to benign domains and ensure that |
| // we display an interstitial along the way. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| Interstitial_CapturesRedirects) { |
| { |
| // Verify it works when the lookalike domain is the first in the chain |
| const GURL kNavigatedUrl = |
| GetLongRedirect("googlé.com", "example.net", "example.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| LoadAndCheckInterstitialAt(browser(), kNavigatedUrl); |
| } |
| |
| // LoadAndCheckInterstitialAt assumes there's not an interstitial already |
| // showing (since otherwise it can't be sure that the navigation caused it). |
| NavigateToURLSync(browser(), GetURL("example.com")); |
| |
| { |
| // ...but not when it's in the middle of the chain |
| const GURL kNavigatedUrl = |
| GetLongRedirect("example.net", "googlé.com", "example.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| } |
| |
| NavigateToURLSync(browser(), GetURL("example.com")); |
| |
| { |
| // ...but definitely when it's last in the chain. |
| const GURL kNavigatedUrl = |
| GetLongRedirect("example.net", "example.com", "googlé.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| LoadAndCheckInterstitialAt(browser(), kNavigatedUrl); |
| } |
| } |
| |
| // Verify that a warning, when ignored, applies to the entire eTLD+1, not just |
| // the navigated origin. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| AllowlistAppliesToETLDPlusOne) { |
| { |
| const GURL kNavigatedUrl = GetURL("sub1.googlé.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| LoadAndCheckInterstitialAt(browser(), kNavigatedUrl); |
| SendInterstitialCommandSync(browser(), |
| SecurityInterstitialCommand::CMD_PROCEED); |
| } |
| |
| // TestInterstitialNotShown assumes there's not an interstitial already |
| // showing (since otherwise it can't be sure that the navigation caused it). |
| NavigateToURLSync(browser(), GetURL("example.com")); |
| |
| { |
| const GURL kNavigatedUrl = GetURL("sub2.googlé.com"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| } |
| |
| // We respect private registries for this manual allowlisting so that |
| // different (independent) subdomains each show their own warning. |
| if (!target_embedding_enabled()) { |
| // Since subdomains are only used for target embedding, if that's not |
| // enabled, we can bail out now. |
| return; |
| } |
| NavigateToURLSync(browser(), GetURL("example.com")); |
| { |
| // Note: This uses blogspot.cv because blogspot.com is a top domain, and top |
| // domains don't show warnings. |
| const GURL kNavigatedUrl = GetURL("google-com.blogspot.cv"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| LoadAndCheckInterstitialAt(browser(), kNavigatedUrl); |
| SendInterstitialCommandSync(browser(), |
| SecurityInterstitialCommand::CMD_PROCEED); |
| } |
| NavigateToURLSync(browser(), GetURL("example.com")); |
| { |
| const GURL kNavigatedUrl = GetURL("google-com-unrelated.blogspot.cv"); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| LoadAndCheckInterstitialAt(browser(), kNavigatedUrl); |
| } |
| } |
| |
| // Verify that the user action in UKM is recorded even when we navigate away |
| // from the interstitial without interacting with it. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| UkmRecordedAfterNavigateAway) { |
| const GURL navigated_url = GetURL("googlé.com"); |
| const GURL subsequent_url = GetURL("example.com"); |
| |
| LoadAndCheckInterstitialAt(browser(), navigated_url); |
| NavigateToURLSync(browser(), subsequent_url); |
| CheckUkm({navigated_url}, "UserAction", |
| LookalikeUrlBlockingPageUserAction::kCloseOrBack); |
| } |
| |
| // Verify that the user action in UKM is recorded properly when the user accepts |
| // the navigation suggestion. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| UkmRecordedAfterSuggestionAccepted) { |
| const GURL navigated_url = GetURL("googlé.com"); |
| |
| LoadAndCheckInterstitialAt(browser(), navigated_url); |
| SendInterstitialCommandSync(browser(), |
| SecurityInterstitialCommand::CMD_DONT_PROCEED); |
| CheckUkm({navigated_url}, "UserAction", |
| LookalikeUrlBlockingPageUserAction::kAcceptSuggestion); |
| } |
| |
| // Verify that the user action in UKM is recorded properly when the user ignores |
| // the navigation suggestion. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| UkmRecordedAfterSuggestionIgnored) { |
| const GURL navigated_url = GetURL("googlé.com"); |
| |
| LoadAndCheckInterstitialAt(browser(), navigated_url); |
| SendInterstitialCommandSync(browser(), |
| SecurityInterstitialCommand::CMD_PROCEED); |
| CheckUkm({navigated_url}, "UserAction", |
| LookalikeUrlBlockingPageUserAction::kClickThrough); |
| } |
| |
| // Verify that the URL shows normally on pages after a lookalike interstitial. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| UrlShownAfterInterstitial) { |
| LoadAndCheckInterstitialAt(browser(), GetURL("googlé.com")); |
| |
| // URL should be showing again when we navigate to a normal URL |
| NavigateToURLSync(browser(), GetURL("example.com")); |
| EXPECT_TRUE(IsUrlShowing(browser())); |
| } |
| |
| // Verify that bypassing warnings in the main profile does not affect incognito. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| MainProfileDoesNotAffectIncognito) { |
| const GURL kNavigatedUrl = GetURL("googlé.com"); |
| |
| // Set low engagement scores in the main profile and in incognito. |
| Browser* incognito = CreateIncognitoBrowser(); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| SetEngagementScore(incognito, kNavigatedUrl, kLowEngagement); |
| |
| LoadAndCheckInterstitialAt(browser(), kNavigatedUrl); |
| // PROCEEDing will disable the interstitial on subsequent navigations |
| SendInterstitialCommandSync(browser(), |
| SecurityInterstitialCommand::CMD_PROCEED); |
| |
| LoadAndCheckInterstitialAt(incognito, kNavigatedUrl); |
| } |
| |
| // Verify that bypassing warnings in incognito does not affect the main profile. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| IncognitoDoesNotAffectMainProfile) { |
| const GURL kNavigatedUrl = GetURL("sité1.com"); |
| const GURL kEngagedUrl = GetURL("site1.com"); |
| |
| // Set engagement scores in the main profile and in incognito. |
| Browser* incognito = CreateIncognitoBrowser(); |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| SetEngagementScore(incognito, kNavigatedUrl, kLowEngagement); |
| SetEngagementScore(browser(), kEngagedUrl, kHighEngagement); |
| SetEngagementScore(incognito, kEngagedUrl, kHighEngagement); |
| |
| LoadAndCheckInterstitialAt(incognito, kNavigatedUrl); |
| // PROCEEDing will disable the interstitial on subsequent navigations |
| SendInterstitialCommandSync(incognito, |
| SecurityInterstitialCommand::CMD_PROCEED); |
| |
| LoadAndCheckInterstitialAt(browser(), kNavigatedUrl); |
| } |
| |
| // Verify reloading the page does not result in dismissing an interstitial. |
| // Regression test for crbug/941886. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleBrowserTest, |
| RefreshDoesntDismiss) { |
| // Verify it works when the lookalike domain is the first in the chain. |
| const GURL kNavigatedUrl = |
| GetLongRedirect("googlé.com", "example.net", "example.com"); |
| |
| SetEngagementScore(browser(), kNavigatedUrl, kLowEngagement); |
| LoadAndCheckInterstitialAt(browser(), kNavigatedUrl); |
| |
| content::WebContents* web_contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Reload the interstitial twice. Should still work. |
| for (size_t i = 0; i < 2; i++) { |
| content::TestNavigationObserver navigation_observer(web_contents); |
| chrome::Reload(browser(), WindowOpenDisposition::CURRENT_TAB); |
| navigation_observer.Wait(); |
| |
| EXPECT_EQ(LookalikeUrlBlockingPage::kTypeForTesting, |
| GetInterstitialType(web_contents)); |
| EXPECT_FALSE(IsUrlShowing(browser())); |
| } |
| |
| // Go to the affected site directly. This should not result in an |
| // interstitial. |
| TestInterstitialNotShown(browser(), |
| embedded_test_server()->GetURL("example.net", "/")); |
| } |
| |
| scoped_refptr<net::X509Certificate> LoadCertificate() { |
| constexpr char kCertFileName[] = "prime256v1-sha256-google-com.public.pem"; |
| |
| base::ScopedAllowBlockingForTesting allow_io; |
| base::FilePath dir_path; |
| base::PathService::Get(content::DIR_TEST_DATA, &dir_path); |
| dir_path = dir_path.Append(FILE_PATH_LITERAL("sxg")); |
| |
| return net::CreateCertificateChainFromFile( |
| dir_path, kCertFileName, net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE); |
| } |
| |
| // Tests for Signed Exchanges. |
| class LookalikeUrlNavigationThrottleSignedExchangeBrowserTest |
| : public LookalikeUrlNavigationThrottleBrowserTest { |
| public: |
| LookalikeUrlNavigationThrottleSignedExchangeBrowserTest() { |
| net::EmbeddedTestServer::RegisterTestCerts(); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| // HTTPS server only serves a valid cert for localhost, so this is needed |
| // to load pages from other hosts without an error. |
| command_line->AppendSwitch(switches::kIgnoreCertificateErrors); |
| mock_cert_verifier_.SetUpCommandLine(command_line); |
| } |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); |
| } |
| |
| void TearDownInProcessBrowserTestFixture() override { |
| mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); |
| } |
| |
| void SetUp() override { |
| sxg_test_helper_.SetUp(); |
| LookalikeUrlNavigationThrottleBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| https_server_.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("content/test/data"))); |
| https_server_.ServeFilesFromSourceDirectory("content/test/data"); |
| https_server_.RegisterRequestMonitor(base::BindRepeating( |
| &LookalikeUrlNavigationThrottleSignedExchangeBrowserTest:: |
| MonitorRequest, |
| base::Unretained(this))); |
| ASSERT_TRUE(https_server_.Start()); |
| |
| LookalikeUrlNavigationThrottleBrowserTest::SetUpOnMainThread(); |
| } |
| |
| void TearDownOnMainThread() override { |
| sxg_test_helper_.TearDownOnMainThread(); |
| } |
| |
| bool HadSignedExchangeInAcceptHeader(const GURL& url) const { |
| const auto it = url_accept_header_map_.find(url); |
| if (it == url_accept_header_map_.end()) |
| return false; |
| return it->second.find("application/signed-exchange") != std::string::npos; |
| } |
| |
| void InstallMockCert() { |
| sxg_test_helper_.InstallMockCert(mock_cert_verifier_.mock_cert_verifier()); |
| |
| // Make the MockCertVerifier treat the certificate |
| // "prime256v1-sha256-google-com.public.pem" as valid for |
| // "google-com.example.org". |
| scoped_refptr<net::X509Certificate> original_cert = LoadCertificate(); |
| net::CertVerifyResult dummy_result; |
| dummy_result.verified_cert = original_cert; |
| dummy_result.cert_status = net::OK; |
| dummy_result.ocsp_result.response_status = net::OCSPVerifyResult::PROVIDED; |
| dummy_result.ocsp_result.revocation_status = |
| net::OCSPRevocationStatus::GOOD; |
| mock_cert_verifier_.mock_cert_verifier()->AddResultForCertAndHost( |
| original_cert, "google-com.example.org", dummy_result, net::OK); |
| } |
| |
| void InstallMockCertChainInterceptor() { |
| sxg_test_helper_.InstallMockCertChainInterceptor(); |
| sxg_test_helper_.InstallUrlInterceptor( |
| GURL("https://google-com.example.org/cert.msg"), |
| "content/test/data/sxg/google-com.example.org.public.pem.cbor"); |
| } |
| |
| protected: |
| net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; |
| content::SignedExchangeBrowserTestHelper sxg_test_helper_; |
| content::ContentMockCertVerifier mock_cert_verifier_; |
| |
| private: |
| void MonitorRequest(const net::test_server::HttpRequest& request) { |
| const auto it = request.headers.find("Accept"); |
| if (it == request.headers.end()) |
| return; |
| url_accept_header_map_[request.base_url.Resolve(request.relative_url)] = |
| it->second; |
| } |
| |
| std::map<GURL, std::string> url_accept_header_map_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| LookalikeUrlNavigationThrottleSignedExchangeBrowserTest, |
| testing::Combine(testing::Bool() /* target_embedding_enabled */, |
| testing::Bool() /* punycode_interstitial_enabled */, |
| testing::Bool() /* digital_asset_links_enabled */)); |
| |
| // Navigates to a 127.0.0.1 URL that serves a signed exchange for |
| // google-com.example.org. This navigation should be blocked by the target |
| // embedding interstitial. We only test target embedding here because we can |
| // test it with a subdomain of example.org (which is the domain used by SGX test |
| // code). Testing an ETLD+1 such as googlé.com would require generating a custom |
| // cert. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleSignedExchangeBrowserTest, |
| InnerUrlIsLookalike_ShouldBlock) { |
| if (!target_embedding_enabled()) { |
| return; |
| } |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| |
| sxg_test_helper_.InstallUrlInterceptor( |
| GURL("https://google-com.example.org/test/"), |
| "content/test/data/sxg/fallback.html"); |
| const GURL kNavigatedUrl = |
| https_server_.GetURL("/sxg/google-com.example.org_test.sxg"); |
| const GURL kExpectedSuggestedUrl("https://google.com"); |
| |
| base::HistogramTester histograms; |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kExpectedSuggestedUrl, |
| NavigationSuggestionEvent::kMatchTargetEmbedding, |
| true /* expect_signed_exchange */); |
| |
| // Check that the SXG file was handled as a Signed Exchange. |
| ASSERT_TRUE(HadSignedExchangeInAcceptHeader(kNavigatedUrl)); |
| } |
| |
| // Navigates to a lookalike URL (google-com.test.com) that serves a signed |
| // exchange for test.example.org. This should not be blocked. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleSignedExchangeBrowserTest, |
| OuterUrlIsLookalike_ShouldNotBlock) { |
| if (!target_embedding_enabled()) { |
| return; |
| } |
| |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| |
| const GURL kSgxTargetUrl("https://test.example.org/test/"); |
| sxg_test_helper_.InstallUrlInterceptor(kSgxTargetUrl, |
| "content/test/data/sxg/fallback.html"); |
| const GURL kNavigatedUrl = https_server_.GetURL( |
| "google-com.test.com", "/sxg/test.example.org_test.sxg"); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| |
| // Check that the SXG file was handled as a Signed Exchange. |
| // MonitorRequest() sees kNavigatedUrl with an IP address instead of |
| // domain name, so check it instead. |
| const GURL kResolvedNavigatedUrl = |
| https_server_.GetURL("/sxg/test.example.org_test.sxg"); |
| ASSERT_TRUE(HadSignedExchangeInAcceptHeader(kResolvedNavigatedUrl)); |
| } |
| |
| // Navigates to a lookalike URL (google-com.test.com) that serves a signed |
| // exchange for test.example.org. This should not be blocked. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleSignedExchangeBrowserTest, |
| OuterUrlIsLookalikeButNotSignedExchange_ShouldNotBlock) { |
| if (!target_embedding_enabled()) { |
| return; |
| } |
| |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| |
| const GURL kSgxTargetUrl("https://test.example.org/test/"); |
| sxg_test_helper_.InstallUrlInterceptor(kSgxTargetUrl, |
| "content/test/data/sxg/fallback.html"); |
| const GURL kSgxCacheUrl = https_server_.GetURL( |
| "google-com.test.com", "/sxg/test.example.org_test.sxg"); |
| const GURL kNavigatedUrl = embedded_test_server()->GetURL( |
| "apple-com.site.test", "/server-redirect?" + kSgxCacheUrl.spec()); |
| |
| TestInterstitialNotShown(browser(), kNavigatedUrl); |
| |
| // Check that the SXG file was handled as a Signed Exchange. |
| // MonitorRequest() sees kNavigatedUrl with an IP address instead of |
| // domain name, so check it instead. |
| const GURL kResolvedNavigatedUrl = |
| https_server_.GetURL("/sxg/test.example.org_test.sxg"); |
| ASSERT_TRUE(HadSignedExchangeInAcceptHeader(kResolvedNavigatedUrl)); |
| } |
| |
| // Navigates to a lookalike URL (google-com.test.com) that serves a signed |
| // exchange for google-com.example.org. |
| // Both the outer URL (i.e. cache) and the inner URL are lookalikes so this |
| // should be blocked. |
| IN_PROC_BROWSER_TEST_P(LookalikeUrlNavigationThrottleSignedExchangeBrowserTest, |
| InnerAndOuterUrlsAreLookalikes_ShouldBlock) { |
| if (!target_embedding_enabled()) { |
| return; |
| } |
| InstallMockCert(); |
| InstallMockCertChainInterceptor(); |
| |
| sxg_test_helper_.InstallUrlInterceptor( |
| GURL("https://google-com.example.org/test/"), |
| "content/test/data/sxg/fallback.html"); |
| const GURL kNavigatedUrl = https_server_.GetURL( |
| "google-com.test.com", "/sxg/google-com.example.org_test.sxg"); |
| const GURL kExpectedSuggestedUrl("https://google.com"); |
| |
| base::HistogramTester histograms; |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, kExpectedSuggestedUrl, |
| NavigationSuggestionEvent::kMatchTargetEmbedding, |
| true /* expect_signed_exchange */); |
| |
| // Check that the SXG file was handled as a Signed Exchange. |
| // MonitorRequest() sees kNavigatedUrl with an IP address instead of |
| // domain name, so check it instead. |
| const GURL kResolvedNavigatedUrl = |
| https_server_.GetURL("/sxg/google-com.example.org_test.sxg"); |
| ASSERT_TRUE(HadSignedExchangeInAcceptHeader(kResolvedNavigatedUrl)); |
| } |
| |
| // TODO(meacer): Add a test for a failed SGX response. It should be treated |
| // as a normal redirect. In fact, InnerAndOuterUrlsLookalikes_ShouldBlock |
| // is actually testing this right now, fix it. |
| |
| // Tests for Digital Asset Links. |
| class LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest |
| : public LookalikeUrlNavigationThrottleBrowserTest { |
| public: |
| struct TestSite { |
| std::string hostname; |
| std::string manifest; |
| bool slow_load = false; |
| }; |
| |
| enum SlowLoad { kNone, kLookalike, kTarget }; |
| // Sets up the site |lookalike_hostname| to serve a manifest with contents |
| // |lookalike_manifest|, and the site |target_hostname| to serve a manifest |
| // with contents |target_manifest| and navigates to |lookalike_hostname|. |
| // Expects an interstitial with the suggested hostname |
| // |expected_suggested_hostname|. |
| void TestExpectInterstitial(const base::HistogramTester& histograms, |
| const char* lookalike_hostname, |
| const std::string& lookalike_manifest, |
| const char* target_hostname, |
| const std::string& target_manifest, |
| const char* expected_suggested_hostname, |
| bool is_target_embedding, |
| SlowLoad slow_load = SlowLoad::kNone) { |
| const GURL kNavigatedUrl = MakeURL(lookalike_hostname); |
| const std::vector<TestSite> sites{ |
| {lookalike_hostname, lookalike_manifest, |
| slow_load == SlowLoad::kLookalike}, |
| {target_hostname, target_manifest, slow_load == SlowLoad::kTarget}, |
| }; |
| NavigationSuggestionEvent expected_event = |
| is_target_embedding ? NavigationSuggestionEvent::kMatchTargetEmbedding |
| : NavigationSuggestionEvent::kMatchSkeletonTop500; |
| SetUpManifests(sites); |
| TestMetricsRecordedAndInterstitialShown( |
| browser(), histograms, kNavigatedUrl, |
| MakeURL(expected_suggested_hostname), expected_event); |
| CheckUkm({kNavigatedUrl}, "MatchType", |
| is_target_embedding ? LookalikeUrlMatchType::kTargetEmbedding |
| : LookalikeUrlMatchType::kSkeletonMatchTop500); |
| // Ensure that there was indeed a lookalike match. |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount(lookalikes::kHistogramName, expected_event, 1); |
| } |
| |
| // Sets up the site |lookalike_hostname| to serve a manifest with contents |
| // |lookalike_manifest|, and the site |target_hostname| to serve a manifest |
| // with contents |target_manifest| and navigates to |lookalike_hostname|. |
| // Expects no interstitial. |
| void TestNoInterstitial(const base::HistogramTester& histograms, |
| const char* lookalike_hostname, |
| const std::string& lookalike_manifest, |
| const char* target_hostname, |
| const std::string& target_manifest) { |
| const std::vector<TestSite> sites{ |
| {lookalike_hostname, lookalike_manifest}, |
| {target_hostname, target_manifest}, |
| }; |
| SetUpManifests(sites); |
| |
| TestInterstitialNotShown(browser(), // histograms, |
| MakeURL(lookalike_hostname)); |
| CheckNoUkm(); |
| // Ensure that there was indeed a lookalike match. |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount( |
| lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchSkeletonTop500, 1); |
| } |
| |
| void SetUpManifests(const std::vector<TestSite>& sites) { |
| url_loader_interceptor_ = |
| std::make_unique<content::URLLoaderInterceptor>(base::BindRepeating( |
| &LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest:: |
| OnIntercept, |
| base::Unretained(this), sites)); |
| } |
| |
| void TearDownOnMainThread() override { url_loader_interceptor_.reset(); } |
| |
| bool OnIntercept(const std::vector<TestSite>& sites, |
| content::URLLoaderInterceptor::RequestParams* params) { |
| for (const TestSite& site : sites) { |
| if (params->url_request.url == MakeManifestURL(site.hostname)) { |
| if (site.slow_load) { |
| test_clock()->Advance(base::Minutes(15)); |
| } |
| if (!site.manifest.empty()) { |
| // Serve manifest contents: |
| std::string headers = |
| "HTTP/1.1 200 OK\nContent-Type: application/json; " |
| "charset=utf-8\n"; |
| content::URLLoaderInterceptor::WriteResponse(headers, site.manifest, |
| params->client.get()); |
| } else { |
| // Serve error: |
| params->client->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_CONNECTION_RESET)); |
| } |
| return true; |
| } |
| if (params->url_request.url == MakeURL(site.hostname)) { |
| content::URLLoaderInterceptor::WriteResponse( |
| "HTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\n", |
| "<html>Test page</html>", params->client.get()); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| static GURL MakeManifestURL(const std::string& hostname) { |
| return GURL("https://" + hostname + "/.well-known/assetlinks.json"); |
| } |
| |
| static GURL MakeURL(const std::string& hostname) { |
| return GURL("https://" + hostname); |
| } |
| |
| static std::string MakeManifestWithTarget(const char* target_domain, |
| bool invalid = false) { |
| const char* const format = R"([{ |
| "relation": ["%s"], |
| "target": { |
| "namespace": "web", |
| "site": "https://%s" |
| } |
| }] |
| )"; |
| // Go through MakeURL to convert target_domain to punycode. |
| return base::StringPrintf(format, |
| (invalid ? "junkvalue" : "lookalikes/allowlist"), |
| MakeURL(target_domain).host().c_str()); |
| } |
| |
| private: |
| std::unique_ptr<content::URLLoaderInterceptor> url_loader_interceptor_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| testing::Combine(testing::Bool() /* target_embedding_enabled */, |
| testing::Bool() /* punycode_interstitial_enabled */, |
| testing::Values(true) /* digital_asset_links_enabled */)); |
| |
| // Neither site serves a manifest. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| NoAssetLinks_ShowInterstitial) { |
| base::HistogramTester histograms; |
| TestExpectInterstitial(histograms, "googlé.com", std::string(), "google.com", |
| std::string(), |
| /*expected_suggested_hostname=*/"google.com", |
| /*is_target_embedding=*/false); |
| |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kLookalikeManifestFailed, 1); |
| } |
| |
| // Both lookalike and target sites serve valid asset link manifests pointing to |
| // each other. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| ValidAssetLinks_IgnoreInterstitial) { |
| base::HistogramTester histograms; |
| |
| TestNoInterstitial(histograms, "googlé.com", |
| MakeManifestWithTarget("google.com"), "google.com", |
| MakeManifestWithTarget("googlé.com")); |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kValidationSucceeded, 1); |
| |
| // Try again. The first try should have added the lookalike site to the |
| // allowlist so we shouldn't fetch the manifests again. |
| TestInterstitialNotShown(browser(), MakeURL("googlé.com")); |
| CheckNoUkm(); |
| // Ensure that there was indeed a lookalike match. |
| histograms.ExpectTotalCount(lookalikes::kHistogramName, 1); |
| histograms.ExpectBucketCount(lookalikes::kHistogramName, |
| NavigationSuggestionEvent::kMatchSkeletonTop500, |
| 1); |
| // Validator histogram should remain unchanged. |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kValidationSucceeded, 1); |
| } |
| |
| // Similar to ValidAssetLinks_IgnoreInterstitial, but the lookalike manifest |
| // fetch times out. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| ValidAssetLinks_LookalikeTimeout_ShowInterstitial) { |
| base::HistogramTester histograms; |
| |
| TestExpectInterstitial(histograms, "googlé.com", |
| MakeManifestWithTarget("google.com"), "google.com", |
| MakeManifestWithTarget("googlé.com"), |
| /*expected_suggested_hostname=*/"google.com", |
| /*is_target_embedding=*/false, SlowLoad::kLookalike); |
| |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kLookalikeManifestTimedOut, 1); |
| } |
| |
| // Similar to ValidAssetLinks_IgnoreInterstitial, but the target manifest fetch |
| // times out. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| ValidAssetLinks_TargetTimeout_ShowInterstitial) { |
| base::HistogramTester histograms; |
| |
| TestExpectInterstitial(histograms, "googlé.com", |
| MakeManifestWithTarget("google.com"), "google.com", |
| MakeManifestWithTarget("googlé.com"), |
| /*expected_suggested_hostname=*/"google.com", |
| /*is_target_embedding=*/false, SlowLoad::kTarget); |
| |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kTargetManifestTimedOut, 1); |
| } |
| |
| // Both lookalike and target sites serve asset links. Lookalike site's manifest |
| // has an entry for the target, but the target site's manifest doesn't have one |
| // for the lookalike. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| ValidAssetLinks_TargetManifestNotMatching_ShowInterstitial) { |
| base::HistogramTester histograms; |
| TestExpectInterstitial(histograms, "góógle.com", |
| MakeManifestWithTarget("google.com"), "google.com", |
| MakeManifestWithTarget("site.test"), |
| /*expected_suggested_hostname=*/"google.com", |
| /*is_target_embedding=*/false); |
| |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kTargetManifestFailed, 1); |
| } |
| |
| // Both lookalike and target sites serve asset links. Lookalike site's manifest |
| // has an entry for the target, but the target site's manifest doesn't have |
| // valid content. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| ValidAssetLinks_TargetManifestInvalid_ShowInterstitial) { |
| base::HistogramTester histograms; |
| TestExpectInterstitial(histograms, "góógle.com", |
| MakeManifestWithTarget("google.com"), "google.com", |
| MakeManifestWithTarget("góógle.com", /*invalid=*/true), |
| /*expected_suggested_hostname=*/"google.com", |
| /*is_target_embedding=*/false); |
| |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kTargetManifestFailed, 1); |
| } |
| |
| // Both lookalike and target sites are subdomains and serve valid asset links. |
| // However, lookalike heuristics match to eTLD+1s, so the lookalike's manifest |
| // will fail to validate because it points to the full URL instead. |
| // TODO(meacer): Support this use case. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| ValidSubdomainAssetLinks_NoMatch_ShowInterstitial) { |
| base::HistogramTester histograms; |
| TestExpectInterstitial( |
| histograms, "docs.góógle.com", MakeManifestWithTarget("docs.google.com"), |
| "docs.google.com", MakeManifestWithTarget("docs.góógle.com"), |
| /*expected_suggested_hostname=*/"google.com", |
| /*is_target_embedding=*/false); |
| |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kLookalikeManifestFailed, 1); |
| } |
| |
| // Both lookalike and target sites are subdomains and serve valid asset links. |
| // This time, the lookalike's manifest points to the eTLD+1 of the matching |
| // domain so the validation is successful. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| ValidSubdomainAssetLinks_IgnoreInterstitial) { |
| base::HistogramTester histograms; |
| TestNoInterstitial(histograms, "docs.góógle.com", |
| MakeManifestWithTarget("google.com"), "google.com", |
| MakeManifestWithTarget("docs.góógle.com")); |
| |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kValidationSucceeded, 1); |
| } |
| |
| // Similar to ValidSubdomainAssetLinks_IgnoreInterstitial with a few |
| // differences: |
| // - Lookalike is a subdomain but target is an eTLD+1 and lookalike's manifest |
| // points to this eTLD+1. |
| // - This time, the target's manifest points to the eTLD+1 of the lookalike |
| // instead of its fully qualified domain name. |
| // Validation is still successful despite the fact that the target manifest |
| // doesn't list the FQDN of the lookalike. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| ValidSubdomainAssetLinks_TargetDoesntListLookalikeFQDN_IgnoreInterstitial) { |
| base::HistogramTester histograms; |
| TestNoInterstitial( |
| histograms, |
| /*lookalike_hostname=*/"docs.góógle.com", |
| /*lookalike_manifest=*/MakeManifestWithTarget("google.com"), |
| /*target_hostname=*/"google.com", |
| /*target_manifest=*/MakeManifestWithTarget("góógle.com")); |
| |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kValidationSucceeded, 1); |
| } |
| |
| // Similar to ValidSubdomainAssetLinks_NoMatch_ShowInterstitial but the target |
| // is a subdomain. In some cases the target embedding heuristic can suggest |
| // subdomains instead of eTLD+1s. This doesn't show an interstitial. |
| // TODO(crbug.com/1174862): This currently isn't testing the right thing because |
| // Chrome suggests targetembedding.com instead of test.targetembedding.com. |
| IN_PROC_BROWSER_TEST_P( |
| LookalikeUrlNavigationThrottleDigitalAssetLinksBrowserTest, |
| ValidSubdomainAssetLinks_TargetIsSubdomain_ShowInterstitial) { |
| if (!target_embedding_enabled()) { |
| return; |
| } |
| |
| const char* kSuggestedSite = "targetembedding.com"; |
| SetEngagementScore(browser(), GetURLWithoutPath(kSuggestedSite), |
| kHighEngagement); |
| |
| base::HistogramTester histograms; |
| TestExpectInterstitial( |
| histograms, |
| /*lookalike_hostname=*/"test-targetembedding-com.com", |
| /*lookalike_manifest=*/MakeManifestWithTarget("test.targetembedding.com"), |
| /*target_hostname=*/"test.targetembedding.com", |
| /*target_manifest=*/ |
| MakeManifestWithTarget("test-targetembedding-com.com"), |
| /*expected_suggested_hostname=*/kSuggestedSite, |
| /*is_target_embedding=*/true); |
| |
| histograms.ExpectTotalCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, 2); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kStarted, 1); |
| histograms.ExpectBucketCount( |
| DigitalAssetLinkCrossValidator::kEventHistogramName, |
| DigitalAssetLinkCrossValidator::Event::kLookalikeManifestFailed, 1); |
| } |