| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/lookalikes/core/lookalike_url_util.h" |
| |
| #include "base/functional/bind.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/lookalikes/core/safety_tip_test_utils.h" |
| #include "components/lookalikes/core/safety_tips_config.h" |
| #include "components/url_formatter/spoof_checks/common_words/common_words_test-inc.cc" |
| #include "components/url_formatter/spoof_checks/common_words/common_words_util.h" |
| #include "components/url_formatter/spoof_checks/idn_spoof_checker.h" |
| #include "components/url_formatter/spoof_checks/top_domains/test_domains_trie.h" |
| #include "components/url_formatter/spoof_checks/top_domains/test_top_bucket_domains.h" |
| #include "components/version_info/channel.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using lookalikes::ComboSquattingParams; |
| using lookalikes::ComboSquattingType; |
| using lookalikes::DomainInfo; |
| using lookalikes::GetDomainInfo; |
| using lookalikes::IsHeuristicEnabledForHostname; |
| using lookalikes::LookalikeUrlMatchType; |
| using lookalikes::TargetEmbeddingType; |
| using version_info::Channel; |
| |
| namespace { |
| // Tests lists for Combo Squatting. Some of these entries are intended to test |
| // for various edge cases and aren't realistic for production. |
| constexpr std::string_view kBrandNames[][2] = {{"google", "google"}, |
| {"youtube", "youtube"}, |
| {"sample", "sarnple"}, |
| {"example", "exarnple"}, |
| {"vices", "vices"}}; |
| constexpr std::string_view kPopularKeywords[] = { |
| "online", "login", "account", "arnple", "services", "test", "security"}; |
| const ComboSquattingParams kComboSquattingParams{kBrandNames, kPopularKeywords}; |
| |
| } // namespace |
| |
| std::string TargetEmbeddingTypeToString(TargetEmbeddingType type) { |
| switch (type) { |
| case TargetEmbeddingType::kNone: |
| return "kNone"; |
| case TargetEmbeddingType::kInterstitial: |
| return "kInterstitial"; |
| case TargetEmbeddingType::kSafetyTip: |
| return "kSafetyTip"; |
| } |
| NOTREACHED(); |
| } |
| |
| // These tests do not use the production top domain list. This is to avoid |
| // having to adjust the tests when the top domain list is updated. Instead, |
| // these tests use the data in `test_domains.list` and `common_words_test.gpref` |
| // files. |
| class LookalikeUrlUtilTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| // Use test top domain lists instead of the actual list. |
| url_formatter::IDNSpoofChecker::HuffmanTrieParams trie_params{ |
| url_formatter::kTestTopDomainsHuffmanTree, |
| url_formatter::kTestTopDomainsTrie, |
| url_formatter::kTestTopDomainsTrieBits, |
| url_formatter::kTestTopDomainsRootPosition}; |
| url_formatter::IDNSpoofChecker::SetTrieParamsForTesting(trie_params); |
| |
| // Use test top bucket domain skeletons instead of the actual list. |
| lookalikes::TopBucketDomainsParams top_bucket_params{ |
| test_top_bucket_domains::kTopBucketEditDistanceSkeletons, |
| test_top_bucket_domains::kNumTopBucketEditDistanceSkeletons}; |
| lookalikes::SetTopBucketDomainsParamsForTesting(top_bucket_params); |
| |
| url_formatter::common_words::SetCommonWordDAFSAForTesting(test::kDafsa); |
| } |
| |
| void TearDown() override { |
| url_formatter::common_words::ResetCommonWordDAFSAForTesting(); |
| lookalikes::ResetTopBucketDomainsParamsForTesting(); |
| url_formatter::IDNSpoofChecker::RestoreTrieParamsForTesting(); |
| } |
| }; |
| |
| TEST_F(LookalikeUrlUtilTest, IsEditDistanceAtMostOne) { |
| const struct TestCase { |
| const wchar_t* domain; |
| const wchar_t* top_domain; |
| bool expected; |
| } kTestCases[] = { |
| {L"", L"", true}, |
| {L"a", L"a", true}, |
| {L"a", L"", true}, |
| {L"", L"a", true}, |
| |
| {L"", L"ab", false}, |
| {L"ab", L"", false}, |
| |
| {L"ab", L"a", true}, |
| {L"a", L"ab", true}, |
| {L"ab", L"b", true}, |
| {L"b", L"ab", true}, |
| {L"ab", L"ab", true}, |
| |
| {L"", L"ab", false}, |
| {L"ab", L"", false}, |
| {L"a", L"abc", false}, |
| {L"abc", L"a", false}, |
| |
| {L"aba", L"ab", true}, |
| {L"ba", L"aba", true}, |
| {L"abc", L"ac", true}, |
| {L"ac", L"abc", true}, |
| |
| // Same length. |
| {L"xbc", L"ybc", true}, |
| {L"axc", L"ayc", true}, |
| {L"abx", L"aby", true}, |
| |
| // Should also work for non-ASCII. |
| {L"é", L"", true}, |
| {L"", L"é", true}, |
| {L"tést", L"test", true}, |
| {L"test", L"tést", true}, |
| {L"tés", L"test", false}, |
| {L"test", L"tés", false}, |
| |
| // Real world test cases. |
| {L"google.com", L"gooogle.com", true}, |
| {L"gogle.com", L"google.com", true}, |
| {L"googlé.com", L"google.com", true}, |
| {L"google.com", L"googlé.com", true}, |
| // Different by two characters. |
| {L"google.com", L"goooglé.com", false}, |
| }; |
| for (const TestCase& test_case : kTestCases) { |
| bool result = lookalikes::IsEditDistanceAtMostOne( |
| base::WideToUTF16(test_case.domain), |
| base::WideToUTF16(test_case.top_domain)); |
| EXPECT_EQ(test_case.expected, result) |
| << "when comparing " << test_case.domain << " with " |
| << test_case.top_domain; |
| } |
| } |
| |
| TEST_F(LookalikeUrlUtilTest, EditDistanceExcludesCommonFalsePositives) { |
| const struct TestCase { |
| const char* domain; |
| const char* top_domain; |
| bool is_likely_false_positive; |
| } kTestCases[] = { |
| // Most edit distance instances are not likely false positives. |
| {"abcxd.com", "abcyd.com", false}, // Substitution |
| {"abcxd.com", "abcxxd.com", false}, // Deletion |
| {"abcxxd.com", "abcxd.com", false}, // Insertion |
| |
| // But we permit cases where the only difference is in the tld. |
| {"abcde.com", "abcde.net", true}, |
| |
| // We also permit matches that are only due to a numeric suffix, |
| {"abcd1.com", "abcd2.com", true}, // Substitution |
| {"abcde.com", "abcde1.com", true}, // Numeric deletion |
| {"abcde1.com", "abcde.com", true}, // Numeric insertion |
| {"abcd11.com", "abcd21.com", true}, // Not-final-digit substitution |
| {"a.abcd1.com", "abcd2.com", true}, // Only relevant for eTLD+1. |
| // ...and that change must be due to the numeric suffix. |
| {"abcx1.com", "abcy1.com", false}, // Substitution before suffix |
| {"abcd1.com", "abcde1.com", false}, // Deletion before suffix |
| {"abcde1.com", "abcd1.com", false}, // Insertion before suffix |
| {"abcdx.com", "abcdy.com", false}, // Non-numeric substitution at end |
| |
| // We also permit matches that are only due to a first-character change, |
| {"xabcd.com", "yabcd.com", true}, // Substitution |
| {"xabcde.com", "abcde.com", true}, // Insertion |
| {"abcde.com", "xabcde.com", true}, // Deletion |
| {"a.abcde.com", "xabcde.com", true}, // For eTLD+1 |
| // ...so long as that change is only on the first character, not later. |
| {"abcde.com", "axbcde.com", false}, // Deletion |
| {"axbcde.com", "abcde.com", false}, // Insertion |
| {"axbcde.com", "aybcde.com", false}, // Substitution |
| |
| // We permit matches that only differ due to a single "-". |
| {"-abcde.com", "abcde.com", true}, |
| {"ab-cde.com", "abcde.com", true}, |
| {"abcde-.com", "abcde.com", true}, |
| {"abcde.com", "-abcde.com", true}, |
| {"abcde.com", "ab-cde.com", true}, |
| {"abcde.com", "abcde-.com", true}, |
| }; |
| for (const TestCase& test_case : kTestCases) { |
| auto navigated = |
| GetDomainInfo(GURL(std::string(url::kHttpsScheme) + |
| url::kStandardSchemeSeparator + test_case.domain)); |
| auto matched = GetDomainInfo(GURL(std::string(url::kHttpsScheme) + |
| url::kStandardSchemeSeparator + |
| test_case.top_domain)); |
| bool result = IsLikelyEditDistanceFalsePositive(navigated, matched); |
| EXPECT_EQ(test_case.is_likely_false_positive, result) |
| << "when comparing " << test_case.domain << " with " |
| << test_case.top_domain; |
| } |
| } |
| |
| TEST_F(LookalikeUrlUtilTest, CharacterSwapExcludesCommonFalsePositives) { |
| const struct TestCase { |
| const char* domain; |
| const char* top_domain; |
| bool is_likely_false_positive; |
| } kTestCases[] = { |
| {"abcde.com", "abced.com", false}, |
| // Only differs by registry: |
| {"abcde.sr", "abcde.rs", true}, |
| }; |
| for (const TestCase& test_case : kTestCases) { |
| auto navigated = |
| GetDomainInfo(GURL(std::string(url::kHttpsScheme) + |
| url::kStandardSchemeSeparator + test_case.domain)); |
| auto matched = GetDomainInfo(GURL(std::string(url::kHttpsScheme) + |
| url::kStandardSchemeSeparator + |
| test_case.top_domain)); |
| bool result = IsLikelyCharacterSwapFalsePositive(navigated, matched); |
| EXPECT_EQ(test_case.is_likely_false_positive, result) |
| << "when comparing " << test_case.domain << " with " |
| << test_case.top_domain; |
| } |
| } |
| |
| bool IsGoogleScholar(const std::string& hostname) { |
| return hostname == "scholar.google.com"; |
| } |
| |
| struct TargetEmbeddingHeuristicTestCase { |
| const std::string hostname; |
| // Empty when there is no match. |
| const std::string expected_safe_host; |
| const TargetEmbeddingType expected_type; |
| }; |
| |
| TEST_F(LookalikeUrlUtilTest, ShouldBlockBySpoofCheckResult) { |
| EXPECT_FALSE(ShouldBlockBySpoofCheckResult( |
| GetDomainInfo(GURL("https://example.com")))); |
| // ASCII short eTLD+1: |
| EXPECT_FALSE( |
| ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://e.com")))); |
| EXPECT_FALSE(ShouldBlockBySpoofCheckResult( |
| GetDomainInfo(GURL("https://subdomain.e.com")))); |
| // Unicode single character e2LD: |
| EXPECT_FALSE( |
| ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://τ.com")))); |
| EXPECT_FALSE( |
| ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://test.τ.com")))); |
| // Unicode single character e2LD with a unicode registry. |
| EXPECT_FALSE( |
| ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://τ.рф")))); |
| EXPECT_FALSE( |
| ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://test.τ.рф")))); |
| // Non-unique hostname: |
| EXPECT_FALSE(ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://τ")))); |
| |
| // Multi character e2LD with disallowed characters: |
| EXPECT_TRUE( |
| ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://ττ.com")))); |
| EXPECT_TRUE(ShouldBlockBySpoofCheckResult( |
| GetDomainInfo(GURL("https://test.ττ.com")))); |
| EXPECT_TRUE( |
| ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://ττ.рф")))); |
| EXPECT_TRUE( |
| ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://test.ττ.рф")))); |
| } |
| |
| TEST_F(LookalikeUrlUtilTest, TargetEmbeddingTest) { |
| const std::vector<DomainInfo> kEngagedSites = { |
| GetDomainInfo(GURL("https://highengagement.com")), |
| GetDomainInfo(GURL("https://highengagement.inthesubdomain.com")), |
| GetDomainInfo(GURL("https://highengagement.co.uk")), |
| GetDomainInfo(GURL("https://subdomain.highengagement.com")), |
| GetDomainInfo(GURL("https://www.highengagementwithwww.com")), |
| GetDomainInfo(GURL("https://subdomain.google.com")), |
| }; |
| const std::vector<TargetEmbeddingHeuristicTestCase> kTestCases = { |
| // The length of the url should not affect the outcome. |
| {"this-is-a-very-long-url-but-it-should-not-affect-the-" |
| "outcome-of-this-target-embedding-test-google.com-login.com", |
| "google.com", TargetEmbeddingType::kInterstitial}, |
| {"google-com-this-is-a-very-long-url-but-it-should-not-affect-" |
| "the-outcome-of-this-target-embedding-test-login.com", |
| "google.com", TargetEmbeddingType::kInterstitial}, |
| {"this-is-a-very-long-url-but-it-should-not-affect-google-the-" |
| "outcome-of-this-target-embedding-test.com-login.com", |
| "", TargetEmbeddingType::kNone}, |
| {"google-this-is-a-very-long-url-but-it-should-not-affect-the-" |
| "outcome-of-this-target-embedding-test.com-login.com", |
| "", TargetEmbeddingType::kNone}, |
| |
| // We need exact skeleton match for our domain so exclude edit-distance |
| // matches. |
| {"goog0le.com-login.com", "", TargetEmbeddingType::kNone}, |
| |
| // Unicode characters should be handled |
| {"googlé.com-login.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"foo-googlé.com-bar.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| |
| // The basic states |
| {"google.com.foo.com", "google.com", TargetEmbeddingType::kInterstitial}, |
| // - before the domain name should be ignored. |
| {"foo-google.com-bar.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| // The embedded target's TLD doesn't necessarily need to be followed by a |
| // '-' and could be a subdomain by itself. |
| {"foo-google.com.foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"a.b.c.d.e.f.g.h.foo-google.com.foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"a.b.c.d.e.f.g.h.google.com-foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"1.2.3.4.5.6.google.com-foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| // Target domain could be in the middle of subdomains. |
| {"foo.google.com.foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| // The target domain and its tld should be next to each other. |
| {"foo-google.l.com-foo.com", "", TargetEmbeddingType::kNone}, |
| // Target domain might be separated with a dash instead of dot. |
| {"foo.google-com-foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| |
| // Allowlisted domains should not trigger heuristic. |
| {"scholar.google.com.foo.com", "", TargetEmbeddingType::kNone}, |
| {"scholar.google.com-google.com.foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"google.com-scholar.google.com.foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"foo.scholar.google.com.foo.com", "", TargetEmbeddingType::kNone}, |
| {"scholar.foo.google.com.foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| |
| // e2LDs should be longer than 3 characters. |
| {"hp.com-foo.com", "", TargetEmbeddingType::kNone}, |
| |
| // Targets with common words as e2LD are not considered embedded targets |
| // either for all TLDs or another-TLD matching. |
| {"foo.jobs.com-foo.com", "", TargetEmbeddingType::kNone}, |
| {"foo.office.com-foo.com", "office.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"foo.jobs.org-foo.com", "", TargetEmbeddingType::kNone}, |
| {"foo.office.org-foo.com", "", TargetEmbeddingType::kNone}, |
| // Common words (like 'jobs' are included in the big common word list. |
| // Ensure that the supplemental kCommonWords list is also checked. |
| {"foo.hoteles.com-foo.com", "", TargetEmbeddingType::kNone}, |
| |
| // Targets could be embedded without their dots and dashes. |
| {"googlecom-foo.com", "google.com", TargetEmbeddingType::kInterstitial}, |
| {"foo.googlecom-foo.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| // But should not be detected if they're using a common word. weather.com |
| // is on the top domain list, but 'weather' is a common word. |
| {"weathercom-foo.com", "", TargetEmbeddingType::kNone}, |
| // And should also not be detected if they're too short. vk.com is on the |
| // top domain list, but is shorter than kMinE2LDLengthForTargetEmbedding. |
| {"vkcom-foo.com", "", TargetEmbeddingType::kNone}, |
| |
| // Ensure legitimate domains don't trigger. |
| {"foo.google.com", "", TargetEmbeddingType::kNone}, |
| {"foo.bar.google.com", "", TargetEmbeddingType::kNone}, |
| {"google.com", "", TargetEmbeddingType::kNone}, |
| {"google.co.uk", "", TargetEmbeddingType::kNone}, |
| {"google.randomreg-login.com", "", TargetEmbeddingType::kNone}, |
| {"com.foo.com", "", TargetEmbeddingType::kNone}, |
| |
| // Multipart eTLDs should work. |
| {"foo.google.co.uk.foo.com", "google.co.uk", |
| TargetEmbeddingType::kInterstitial}, |
| {"foo.highengagement-co-uk.foo.com", "highengagement.co.uk", |
| TargetEmbeddingType::kInterstitial}, |
| |
| // Cross-TLD matches should not trigger, even when they're embedding |
| // another domain, even when using a de-facto public eTLD. |
| {"google.com.mx", "", TargetEmbeddingType::kNone}, // public |
| {"google.com.de", "", TargetEmbeddingType::kNone}, // de-facto public |
| |
| // Engaged sites should trigger as specifically as possible, and should |
| // trigger preferentially to top sites when possible. |
| {"foo.highengagement.com.foo.com", "highengagement.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"foo.subdomain.highengagement.com.foo.com", |
| "subdomain.highengagement.com", TargetEmbeddingType::kInterstitial}, |
| {"foo.subdomain.google.com.foo.com", "subdomain.google.com", |
| TargetEmbeddingType::kInterstitial}, |
| |
| // Skeleton matching should work against engaged sites at a eTLD+1 level, |
| {"highengagement.inthesubdomain.com-foo.com", |
| "highengagement.inthesubdomain.com", TargetEmbeddingType::kInterstitial}, |
| // but only if the bare eTLD+1, or www.[eTLD+1] has been engaged. |
| {"subdomain.highéngagement.com-foo.com", "highengagement.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"subdomain.highéngagementwithwww.com-foo.com", |
| "highengagementwithwww.com", TargetEmbeddingType::kInterstitial}, |
| {"other.inthésubdomain.com-foo.com", "", TargetEmbeddingType::kNone}, |
| // Ideally, we'd be able to combine subdomains and skeleton matching, but |
| // our current algorithm can't detect that precisely. |
| {"highengagement.inthésubdomain.com-foo.com", "", |
| TargetEmbeddingType::kNone}, |
| |
| // Domains should be allowed to embed themselves. |
| {"highengagement.com.highengagement.com", "", TargetEmbeddingType::kNone}, |
| {"subdomain.highengagement.com.highengagement.com", "", |
| TargetEmbeddingType::kNone}, |
| {"nothighengagement.highengagement.com.highengagement.com", "", |
| TargetEmbeddingType::kNone}, |
| {"google.com.google.com", "", TargetEmbeddingType::kNone}, |
| {"www.google.com.google.com", "", TargetEmbeddingType::kNone}, |
| |
| // Detect embeddings at the end of the domain, too, but as a Safety Tip. |
| {"www-google.com", "google.com", TargetEmbeddingType::kSafetyTip}, |
| {"www-highengagement.com", "highengagement.com", |
| TargetEmbeddingType::kSafetyTip}, |
| {"subdomain-highengagement.com", "subdomain.highengagement.com", |
| TargetEmbeddingType::kSafetyTip}, |
| // If the match duplicates the TLD, it's not quite tail-embedding. |
| {"google-com.com", "google.com", TargetEmbeddingType::kInterstitial}, |
| // If there are multiple options, it should choose the more severe one. |
| {"google-com.google-com.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"subdomain.google-com.google-com.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| {"google.com-google.com-google.com", "google.com", |
| TargetEmbeddingType::kInterstitial}, |
| |
| // Ignore end-of-domain embeddings when they're also cross-TLD matches. |
| {"google.com.mx", "", TargetEmbeddingType::kNone}, |
| |
| // For a small set of high-value domains that are also common words (see |
| // kDomainsPermittedInEndEmbeddings), we block all embeddings except those |
| // at the very end of the domain (e.g. foo-{domain.com}). Ensure this |
| // works for domains on the list, but not for others. |
| {"office.com-foo.com", "office.com", TargetEmbeddingType::kInterstitial}, |
| {"example-office.com", "", TargetEmbeddingType::kNone}, |
| {"example-google.com", "google.com", TargetEmbeddingType::kSafetyTip}, |
| }; |
| |
| lookalikes::InitializeBlankLookalikeAllowlistForTesting(); |
| auto* config_proto = lookalikes::GetSafetyTipsRemoteConfigProto(); |
| |
| for (auto& test_case : kTestCases) { |
| std::string safe_hostname; |
| TargetEmbeddingType embedding_type = GetTargetEmbeddingType( |
| test_case.hostname, kEngagedSites, |
| base::BindRepeating(&IsGoogleScholar), config_proto, &safe_hostname); |
| if (test_case.expected_type != TargetEmbeddingType::kNone) { |
| EXPECT_EQ(safe_hostname, test_case.expected_safe_host) |
| << test_case.hostname << " should trigger on " |
| << test_case.expected_safe_host << ", but " |
| << (safe_hostname.empty() ? "it didn't trigger at all." |
| : "triggered on " + safe_hostname); |
| EXPECT_EQ(embedding_type, test_case.expected_type) |
| << test_case.hostname << " should trigger " |
| << TargetEmbeddingTypeToString(test_case.expected_type) << " against " |
| << test_case.expected_safe_host << " but it returned " |
| << TargetEmbeddingTypeToString(embedding_type); |
| } else { |
| EXPECT_EQ(embedding_type, TargetEmbeddingType::kNone) |
| << test_case.hostname << " unexpectedly triggered " |
| << TargetEmbeddingTypeToString(embedding_type) << " against " |
| << safe_hostname; |
| } |
| } |
| } |
| |
| TEST_F(LookalikeUrlUtilTest, TargetEmbeddingIgnoresComponentWordlist) { |
| const std::vector<DomainInfo> kEngagedSites = { |
| GetDomainInfo(GURL("https://commonword.com")), |
| GetDomainInfo(GURL("https://uncommonword.com")), |
| }; |
| |
| lookalikes::SetSafetyTipAllowlistPatterns({}, {}, {"commonword"}); |
| auto* config_proto = lookalikes::GetSafetyTipsRemoteConfigProto(); |
| TargetEmbeddingType embedding_type; |
| std::string safe_hostname; |
| |
| // Engaged sites using uncommon words are still blocked. |
| embedding_type = GetTargetEmbeddingType( |
| "uncommonword.com.evil.com", kEngagedSites, |
| base::BindRepeating(&IsGoogleScholar), config_proto, &safe_hostname); |
| EXPECT_EQ(embedding_type, TargetEmbeddingType::kInterstitial); |
| |
| // But engaged sites using common words are not blocked. |
| embedding_type = GetTargetEmbeddingType( |
| "commonword.com.evil.com", kEngagedSites, |
| base::BindRepeating(&IsGoogleScholar), config_proto, &safe_hostname); |
| EXPECT_EQ(embedding_type, TargetEmbeddingType::kNone); |
| } |
| |
| TEST_F(LookalikeUrlUtilTest, GetETLDPlusOneHandlesSpecialRegistries) { |
| const struct GetETLDPlusOneTestCase { |
| const std::string hostname; |
| const std::string expected_etldp1; |
| } kTestCases[] = { |
| // Trivial test cases for public registries. |
| {"google.com", "google.com"}, |
| {"www.google.com", "google.com"}, |
| {"www.google.co.uk", "google.co.uk"}, |
| |
| // .com.de is a de-facto public registry. |
| {"www.google.com.de", "google.com.de"}, |
| // Regression test for crbug.com/351775838: |
| {"com.de", ""}, |
| |
| // .cloud.goog is a private registry. |
| {"www.example.cloud.goog", "cloud.goog"}, |
| {"cloud.goog", "cloud.goog"}, |
| }; |
| |
| for (auto& test_case : kTestCases) { |
| EXPECT_EQ(lookalikes::GetETLDPlusOne(test_case.hostname), |
| test_case.expected_etldp1); |
| } |
| } |
| |
| // Tests for the character swap heuristic. |
| TEST_F(LookalikeUrlUtilTest, HasOneCharacterSwap) { |
| const struct TestCase { |
| const wchar_t* str1; |
| const wchar_t* str2; |
| bool expected; |
| } kTestCases[] = {{L"", L"", false}, |
| {L"", L"a", false}, |
| {L"", L"ab", false}, |
| {L"a", L"ab", false}, |
| {L"a", L"ba", false}, |
| {L"abc.com", L"abc.com", false}, |
| {L"abc.com", L"abcd.com", false}, |
| {L"domain.com", L"nomaid.com", false}, |
| // Two swaps (ab to ba, ba to ab): |
| {L"abba", L"baab", false}, |
| |
| {L"ab", L"ba", true}, |
| {L"abba", L"baba", true}, |
| |
| {L"abaaa", L"baaaa", true}, |
| {L"abcaa", L"bacaa", true}, |
| |
| {L"aaaab", L"aaaba", true}, |
| {L"aacab", L"aacba", true}, |
| |
| {L"aabaa", L"abaaa", true}, |
| {L"aabcc", L"abacc", true}, |
| |
| {L"aabaa", L"aaaba", true}, |
| {L"ccbaa", L"ccaba", true}, |
| |
| {L"domain.com", L"doamin.com", true}, |
| {L"gmail.com", L"gmailc.om", true}, |
| {L"gmailc.om", L"gmail.com", true}}; |
| for (const TestCase& test_case : kTestCases) { |
| bool result = lookalikes::HasOneCharacterSwap( |
| base::WideToUTF16(test_case.str1), base::WideToUTF16(test_case.str2)); |
| EXPECT_EQ(test_case.expected, result) |
| << "when comparing " << test_case.str1 << " with " << test_case.str2; |
| } |
| } |
| |
| TEST_F(LookalikeUrlUtilTest, GetSuggestedURL) { |
| const struct TestCase { |
| const LookalikeUrlMatchType match_type; |
| const GURL navigated_url; |
| const std::string matched_hostname; |
| const GURL expected_suggested_url; |
| } kTestCases[] = { |
| // Certain heuristics such as top domain matches should use https for |
| // the suggested URL. |
| {LookalikeUrlMatchType::kSkeletonMatchTop500, |
| GURL("http://docs.googlé.com"), "google.com", |
| GURL("https://google.com")}, |
| // But not for non-default ports: |
| {LookalikeUrlMatchType::kSkeletonMatchTop500, |
| GURL("http://docs.googlé.com:8080"), "google.com", |
| GURL("http://google.com:8080")}, |
| // Site engagement should use http for the suggested URL. |
| {LookalikeUrlMatchType::kSkeletonMatchSiteEngagement, |
| GURL("http://docs.googlé.com"), "google.com", GURL("http://google.com")}, |
| |
| // Same tests with the matched hostname having a subdomain. |
| {LookalikeUrlMatchType::kSkeletonMatchTop500, |
| GURL("http://docs.googlé.com"), "docs.google.com", |
| GURL("https://google.com")}, |
| {LookalikeUrlMatchType::kSkeletonMatchTop500, |
| GURL("http://docs.googlé.com:8080"), "docs.google.com", |
| GURL("http://google.com:8080")}, |
| {LookalikeUrlMatchType::kSkeletonMatchSiteEngagement, |
| GURL("http://docs.googlé.com"), "docs.google.com", |
| GURL("http://google.com")}, |
| |
| // Same tests with neither the matched hostname or navigated domain having |
| // a subdomain. |
| {LookalikeUrlMatchType::kSkeletonMatchTop500, GURL("http://googlé.com"), |
| "docs.google.com", GURL("https://google.com")}, |
| {LookalikeUrlMatchType::kSkeletonMatchTop500, |
| GURL("http://googlé.com:8080"), "docs.google.com", |
| GURL("http://google.com:8080")}, |
| {LookalikeUrlMatchType::kSkeletonMatchSiteEngagement, |
| GURL("http://googlé.com"), "docs.google.com", GURL("http://google.com")}, |
| }; |
| |
| for (const TestCase& test_case : kTestCases) { |
| GURL suggested_url = |
| GetSuggestedURL(test_case.match_type, test_case.navigated_url, |
| test_case.matched_hostname); |
| EXPECT_EQ(test_case.expected_suggested_url, suggested_url); |
| } |
| } |
| |
| TEST_F(LookalikeUrlUtilTest, IsHeuristicEnabledForHostname) { |
| reputation::SafetyTipsConfig proto; |
| reputation::HeuristicLaunchConfig* config = proto.add_launch_config(); |
| config->set_heuristic(reputation::HeuristicLaunchConfig:: |
| HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES); |
| |
| // Minimum rollout percentages to enable a heuristic on each site on Stable |
| // channel: |
| // example1.com: 79% |
| // example2.com: 16% |
| // example3.com: 36% |
| |
| // Slowly ramp up the launch and cover more sites on Stable channel. |
| config->set_launch_percentage(0); |
| EXPECT_FALSE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example1.com", Channel::STABLE)); |
| EXPECT_FALSE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example2.com", Channel::STABLE)); |
| EXPECT_FALSE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example3.com", Channel::STABLE)); |
| |
| config->set_launch_percentage(25); |
| EXPECT_FALSE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example1.com", Channel::STABLE)); |
| EXPECT_TRUE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example2.com", Channel::STABLE)); |
| EXPECT_FALSE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example3.com", Channel::STABLE)); |
| |
| config->set_launch_percentage(50); |
| EXPECT_FALSE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example1.com", Channel::STABLE)); |
| EXPECT_TRUE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example2.com", Channel::STABLE)); |
| EXPECT_TRUE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example3.com", Channel::STABLE)); |
| |
| config->set_launch_percentage(100); |
| EXPECT_TRUE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example1.com", Channel::STABLE)); |
| EXPECT_TRUE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example2.com", Channel::STABLE)); |
| EXPECT_TRUE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example3.com", Channel::STABLE)); |
| |
| // On Beta, launch is always at 50%. |
| config->set_launch_percentage(0); |
| EXPECT_FALSE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example1.com", Channel::BETA)); |
| EXPECT_TRUE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example2.com", Channel::BETA)); |
| EXPECT_TRUE(IsHeuristicEnabledForHostname( |
| &proto, |
| reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES, |
| "example3.com", Channel::BETA)); |
| } |
| |
| class ComboSquattingTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| lookalikes::SetComboSquattingParamsForTesting(kComboSquattingParams); |
| } |
| void TearDown() override { |
| lookalikes::ResetComboSquattingParamsForTesting(); |
| } |
| }; |
| |
| // Test for Combo Squatting check of domains. |
| TEST_F(ComboSquattingTest, IsComboSquatting) { |
| const std::vector<DomainInfo> kEngagedSites = { |
| // An engaged site which is not in the hard coded brand names. |
| GetDomainInfo(GURL("https://engagedsite.com")), |
| // An engaged site which is duplicate with a hard coded brand name. |
| GetDomainInfo(GURL("https://subdomain.google.com")), |
| // An engaged site with length less than threshold (4) for |
| // consideration. |
| GetDomainInfo(GURL("https://len.com")), |
| // An engaged site with a registry other than com. |
| GetDomainInfo(GURL("https://testcombo.org")), |
| // Test case for overlapping brand name and keyword (highs + |
| // security = highsecurity). Single letter overlap. |
| GetDomainInfo(GURL("https://highs.com")), |
| }; |
| const struct TestCase { |
| const char* domain; |
| const char* expected_suggested_domain; |
| const ComboSquattingType expected_type; |
| } kTestCases[] = { |
| // Not Combo Squatting (CSQ). |
| {"google.com", "", ComboSquattingType::kNone}, |
| {"youtube.ca", "", ComboSquattingType::kNone}, |
| |
| // Not CSQ, contains subdomains. |
| {"login.google.com", "", ComboSquattingType::kNone}, |
| |
| // Not CSQ, non registrable domains. |
| {"google-login.test", "", ComboSquattingType::kNone}, |
| |
| // CSQ with "-". |
| {"google-online.com", "google.com", ComboSquattingType::kHardCoded}, |
| |
| // CSQ with more than one keyword (login, online) with "-". |
| {"google-login-online.com", "google.com", ComboSquattingType::kHardCoded}, |
| |
| // CSQ with one keyword (online) and one random word (one) with "-". |
| {"one-sample-online.com", "sample.com", ComboSquattingType::kHardCoded}, |
| |
| // Not CSQ, with a keyword (test) as TLD. |
| {"www.example.test", "", ComboSquattingType::kNone}, |
| |
| // CSQ with more than one brand (google, youtube) with "-". |
| {"google-youtube-account.com", "google.com", |
| ComboSquattingType::kHardCoded}, |
| |
| // CSQ without separator. |
| {"loginsample.com", "sample.com", ComboSquattingType::kHardCoded}, |
| |
| // Not CSQ with a keyword (ample) inside brand name (sample). |
| {"sample.com", "", ComboSquattingType::kNone}, |
| |
| // Current version of the heuristic cannot flag this kind of CSQ |
| // with a keyword (ample) inside brand name (sample) and as an added |
| // keyword to the domain. |
| {"sample-ample.com", "", ComboSquattingType::kNone}, |
| |
| // CSQ with more than one keyword (account, online) without separator. |
| {"accountexampleonline.com", "example.com", |
| ComboSquattingType::kHardCoded}, |
| |
| // CSQ with one keyword (login) and one random word (one) without "-". |
| {"oneyoutubelogin.com", "youtube.com", ComboSquattingType::kHardCoded}, |
| |
| // Not CSQ, google is a public TLD. |
| {"online.google", "", ComboSquattingType::kNone}, |
| |
| // Not CSQ, brand name (vice) is part of keyword (service). |
| {"keyservices.com", "", ComboSquattingType::kNone}, |
| |
| // CSQ, brand name (engagedsite) is from engaged sites list. |
| {"engagedsite-login.com", "engagedsite.com", |
| ComboSquattingType::kSiteEngagement}, |
| |
| // Not CSQ, brand name (len) is from engaged sites list but it is short. |
| {"len-online.com", "", ComboSquattingType::kNone}, |
| |
| // CSQ, brand name (googlé) is one of the hard coded brand names and has |
| // IDN spoofing as well. |
| {"googlé-login.com", "google.com", ComboSquattingType::kHardCoded}, |
| |
| // CSQ, brand name (engagedsité) is one of the brand names from engaged |
| // sites and has IDN spoofing as well. |
| {"engagedsité-online.com", "engagedsite.com", |
| ComboSquattingType::kSiteEngagement}, |
| |
| // CSQ, keyword (lógin) has IDN spoofing. |
| {"google-lógin.com", "google.com", ComboSquattingType::kHardCoded}, |
| |
| // CSQ, CSQ with more than one brand (googlé, youtubé) with "-" and IDN |
| // spoofing. |
| {"googlé-youtubé-account.com", "google.com", |
| ComboSquattingType::kHardCoded}, |
| |
| // Not CSQ. |
| {"ónline.googlé", "", ComboSquattingType::kNone}, |
| |
| // Not CSQ, it has IDN spoofing but brand name (vicé) is part of keyword |
| // (servicé). |
| {"keyservicés.com", "", ComboSquattingType::kNone}, |
| |
| // CSQ without separator and with IDN spoofing in the keyword. |
| {"lóginsample.com", "sample.com", ComboSquattingType::kHardCoded}, |
| |
| // CSQ without separator and with IDN spoofing in the brand name. |
| {"loginsamplé.com", "sample.com", ComboSquattingType::kHardCoded}, |
| |
| // Not CSQ, skeleton of brand name (lén) is from engaged sites list but it |
| // is short. |
| {"lén-online.com", "", ComboSquattingType::kNone}, |
| |
| // CSQ when domain and registry are in top domains. |
| {"google-login.co.kr", "google.co.kr", ComboSquattingType::kHardCoded}, |
| |
| // CSQ when brand name is in hard coded brand names, but domain and |
| // registry are not in top domains. |
| {"google-login.co.ir", "google.com", ComboSquattingType::kHardCoded}, |
| |
| // CSQ when domain and registry are in engaged sites, with registry other |
| // than com. |
| {"testcomboonline.org", "testcombo.org", |
| ComboSquattingType::kSiteEngagement}, |
| |
| // If the brand name (highsec) and keyword (security) overlap, ignore. |
| {"highsecurity.com", "", ComboSquattingType::kNone}, |
| }; |
| for (const TestCase& test_case : kTestCases) { |
| auto navigated = |
| GetDomainInfo(GURL(std::string(url::kHttpsScheme) + |
| url::kStandardSchemeSeparator + test_case.domain)); |
| std::string matched_domain; |
| ComboSquattingType type = |
| GetComboSquattingType(navigated, kEngagedSites, &matched_domain); |
| EXPECT_EQ(std::string(test_case.expected_suggested_domain), matched_domain); |
| EXPECT_EQ(test_case.expected_type, type); |
| } |
| } |