lookalike_url_util_unittest.cc 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. // Copyright 2020 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "components/lookalikes/core/lookalike_url_util.h"
  5. #include "base/bind.h"
  6. #include "base/strings/utf_string_conversions.h"
  7. #include "components/lookalikes/core/features.h"
  8. #include "components/reputation/core/safety_tip_test_utils.h"
  9. #include "components/reputation/core/safety_tips_config.h"
  10. #include "components/version_info/channel.h"
  11. #include "testing/gtest/include/gtest/gtest.h"
  12. using version_info::Channel;
  13. namespace {
  14. // Tests lists for Combo Squatting. Some of these entries are intended to test
  15. // for various edge cases and aren't realistic for production.
  16. constexpr std::pair<const char*, const char*> kBrandNames[] = {
  17. {"google", "google"},
  18. {"youtube", "youtube"},
  19. {"sample", "sarnple"},
  20. {"example", "exarnple"},
  21. {"vices", "vices"}};
  22. const char* const kPopularKeywords[] = {"online", "login", "account",
  23. "arnple", "services", "test"};
  24. const ComboSquattingParams kComboSquattingParams{
  25. kBrandNames, std::size(kBrandNames), kPopularKeywords,
  26. std::size(kPopularKeywords)};
  27. } // namespace
  28. std::string TargetEmbeddingTypeToString(TargetEmbeddingType type) {
  29. switch (type) {
  30. case TargetEmbeddingType::kNone:
  31. return "kNone";
  32. case TargetEmbeddingType::kInterstitial:
  33. return "kInterstitial";
  34. case TargetEmbeddingType::kSafetyTip:
  35. return "kSafetyTip";
  36. }
  37. NOTREACHED();
  38. }
  39. TEST(LookalikeUrlUtilTest, IsEditDistanceAtMostOne) {
  40. const struct TestCase {
  41. const wchar_t* domain;
  42. const wchar_t* top_domain;
  43. bool expected;
  44. } kTestCases[] = {
  45. {L"", L"", true},
  46. {L"a", L"a", true},
  47. {L"a", L"", true},
  48. {L"", L"a", true},
  49. {L"", L"ab", false},
  50. {L"ab", L"", false},
  51. {L"ab", L"a", true},
  52. {L"a", L"ab", true},
  53. {L"ab", L"b", true},
  54. {L"b", L"ab", true},
  55. {L"ab", L"ab", true},
  56. {L"", L"ab", false},
  57. {L"ab", L"", false},
  58. {L"a", L"abc", false},
  59. {L"abc", L"a", false},
  60. {L"aba", L"ab", true},
  61. {L"ba", L"aba", true},
  62. {L"abc", L"ac", true},
  63. {L"ac", L"abc", true},
  64. // Same length.
  65. {L"xbc", L"ybc", true},
  66. {L"axc", L"ayc", true},
  67. {L"abx", L"aby", true},
  68. // Should also work for non-ASCII.
  69. {L"é", L"", true},
  70. {L"", L"é", true},
  71. {L"tést", L"test", true},
  72. {L"test", L"tést", true},
  73. {L"tés", L"test", false},
  74. {L"test", L"tés", false},
  75. // Real world test cases.
  76. {L"google.com", L"gooogle.com", true},
  77. {L"gogle.com", L"google.com", true},
  78. {L"googlé.com", L"google.com", true},
  79. {L"google.com", L"googlé.com", true},
  80. // Different by two characters.
  81. {L"google.com", L"goooglé.com", false},
  82. };
  83. for (const TestCase& test_case : kTestCases) {
  84. bool result =
  85. IsEditDistanceAtMostOne(base::WideToUTF16(test_case.domain),
  86. base::WideToUTF16(test_case.top_domain));
  87. EXPECT_EQ(test_case.expected, result)
  88. << "when comparing " << test_case.domain << " with "
  89. << test_case.top_domain;
  90. }
  91. }
  92. TEST(LookalikeUrlUtilTest, EditDistanceExcludesCommonFalsePositives) {
  93. const struct TestCase {
  94. const char* domain;
  95. const char* top_domain;
  96. bool is_likely_false_positive;
  97. } kTestCases[] = {
  98. // Most edit distance instances are not likely false positives.
  99. {"abcxd.com", "abcyd.com", false}, // Substitution
  100. {"abcxd.com", "abcxxd.com", false}, // Deletion
  101. {"abcxxd.com", "abcxd.com", false}, // Insertion
  102. // But we permit cases where the only difference is in the tld.
  103. {"abcde.com", "abcde.net", true},
  104. // We also permit matches that are only due to a numeric suffix,
  105. {"abcd1.com", "abcd2.com", true}, // Substitution
  106. {"abcde.com", "abcde1.com", true}, // Numeric deletion
  107. {"abcde1.com", "abcde.com", true}, // Numeric insertion
  108. {"abcd11.com", "abcd21.com", true}, // Not-final-digit substitution
  109. {"a.abcd1.com", "abcd2.com", true}, // Only relevant for eTLD+1.
  110. // ...and that change must be due to the numeric suffix.
  111. {"abcx1.com", "abcy1.com", false}, // Substitution before suffix
  112. {"abcd1.com", "abcde1.com", false}, // Deletion before suffix
  113. {"abcde1.com", "abcd1.com", false}, // Insertion before suffix
  114. {"abcdx.com", "abcdy.com", false}, // Non-numeric substitution at end
  115. // We also permit matches that are only due to a first-character change,
  116. {"xabcd.com", "yabcd.com", true}, // Substitution
  117. {"xabcde.com", "abcde.com", true}, // Insertion
  118. {"abcde.com", "xabcde.com", true}, // Deletion
  119. {"a.abcde.com", "xabcde.com", true}, // For eTLD+1
  120. // ...so long as that change is only on the first character, not later.
  121. {"abcde.com", "axbcde.com", false}, // Deletion
  122. {"axbcde.com", "abcde.com", false}, // Insertion
  123. {"axbcde.com", "aybcde.com", false}, // Substitution
  124. // We permit matches that only differ due to a single "-".
  125. {"-abcde.com", "abcde.com", true},
  126. {"ab-cde.com", "abcde.com", true},
  127. {"abcde-.com", "abcde.com", true},
  128. {"abcde.com", "-abcde.com", true},
  129. {"abcde.com", "ab-cde.com", true},
  130. {"abcde.com", "abcde-.com", true},
  131. };
  132. for (const TestCase& test_case : kTestCases) {
  133. auto navigated =
  134. GetDomainInfo(GURL(std::string(url::kHttpsScheme) +
  135. url::kStandardSchemeSeparator + test_case.domain));
  136. auto matched = GetDomainInfo(GURL(std::string(url::kHttpsScheme) +
  137. url::kStandardSchemeSeparator +
  138. test_case.top_domain));
  139. bool result = IsLikelyEditDistanceFalsePositive(navigated, matched);
  140. EXPECT_EQ(test_case.is_likely_false_positive, result)
  141. << "when comparing " << test_case.domain << " with "
  142. << test_case.top_domain;
  143. }
  144. }
  145. TEST(LookalikeUrlUtilTest, CharacterSwapExcludesCommonFalsePositives) {
  146. const struct TestCase {
  147. const char* domain;
  148. const char* top_domain;
  149. bool is_likely_false_positive;
  150. } kTestCases[] = {
  151. {"abcde.com", "abced.com", false},
  152. // Only differs by registry:
  153. {"abcde.sr", "abcde.rs", true},
  154. };
  155. for (const TestCase& test_case : kTestCases) {
  156. auto navigated =
  157. GetDomainInfo(GURL(std::string(url::kHttpsScheme) +
  158. url::kStandardSchemeSeparator + test_case.domain));
  159. auto matched = GetDomainInfo(GURL(std::string(url::kHttpsScheme) +
  160. url::kStandardSchemeSeparator +
  161. test_case.top_domain));
  162. bool result = IsLikelyCharacterSwapFalsePositive(navigated, matched);
  163. EXPECT_EQ(test_case.is_likely_false_positive, result)
  164. << "when comparing " << test_case.domain << " with "
  165. << test_case.top_domain;
  166. }
  167. }
  168. bool IsGoogleScholar(const std::string& hostname) {
  169. return hostname == "scholar.google.com";
  170. }
  171. struct TargetEmbeddingHeuristicTestCase {
  172. const std::string hostname;
  173. // Empty when there is no match.
  174. const std::string expected_safe_host;
  175. const TargetEmbeddingType expected_type;
  176. };
  177. TEST(LookalikeUrlUtilTest, ShouldBlockBySpoofCheckResult) {
  178. EXPECT_FALSE(ShouldBlockBySpoofCheckResult(
  179. GetDomainInfo(GURL("https://example.com"))));
  180. // ASCII short eTLD+1:
  181. EXPECT_FALSE(
  182. ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://e.com"))));
  183. EXPECT_FALSE(ShouldBlockBySpoofCheckResult(
  184. GetDomainInfo(GURL("https://subdomain.e.com"))));
  185. // Unicode single character e2LD:
  186. EXPECT_FALSE(
  187. ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://τ.com"))));
  188. EXPECT_FALSE(
  189. ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://test.τ.com"))));
  190. // Unicode single character e2LD with a unicode registry.
  191. EXPECT_FALSE(
  192. ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://τ.рф"))));
  193. EXPECT_FALSE(
  194. ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://test.τ.рф"))));
  195. // Non-unique hostname:
  196. EXPECT_FALSE(ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://τ"))));
  197. // Multi character e2LD with disallowed characters:
  198. EXPECT_TRUE(
  199. ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://ττ.com"))));
  200. EXPECT_TRUE(ShouldBlockBySpoofCheckResult(
  201. GetDomainInfo(GURL("https://test.ττ.com"))));
  202. EXPECT_TRUE(
  203. ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://ττ.рф"))));
  204. EXPECT_TRUE(
  205. ShouldBlockBySpoofCheckResult(GetDomainInfo(GURL("https://test.ττ.рф"))));
  206. }
  207. TEST(LookalikeUrlUtilTest, TargetEmbeddingTest) {
  208. const std::vector<DomainInfo> kEngagedSites = {
  209. GetDomainInfo(GURL("https://highengagement.com")),
  210. GetDomainInfo(GURL("https://highengagement.inthesubdomain.com")),
  211. GetDomainInfo(GURL("https://highengagement.co.uk")),
  212. GetDomainInfo(GURL("https://subdomain.highengagement.com")),
  213. GetDomainInfo(GURL("https://www.highengagementwithwww.com")),
  214. GetDomainInfo(GURL("https://subdomain.google.com")),
  215. };
  216. const std::vector<TargetEmbeddingHeuristicTestCase> kTestCases = {
  217. // The length of the url should not affect the outcome.
  218. {"this-is-a-very-long-url-but-it-should-not-affect-the-"
  219. "outcome-of-this-target-embedding-test-google.com-login.com",
  220. "google.com", TargetEmbeddingType::kInterstitial},
  221. {"google-com-this-is-a-very-long-url-but-it-should-not-affect-"
  222. "the-outcome-of-this-target-embedding-test-login.com",
  223. "google.com", TargetEmbeddingType::kInterstitial},
  224. {"this-is-a-very-long-url-but-it-should-not-affect-google-the-"
  225. "outcome-of-this-target-embedding-test.com-login.com",
  226. "", TargetEmbeddingType::kNone},
  227. {"google-this-is-a-very-long-url-but-it-should-not-affect-the-"
  228. "outcome-of-this-target-embedding-test.com-login.com",
  229. "", TargetEmbeddingType::kNone},
  230. // We need exact skeleton match for our domain so exclude edit-distance
  231. // matches.
  232. {"goog0le.com-login.com", "", TargetEmbeddingType::kNone},
  233. // Unicode characters should be handled
  234. {"googlé.com-login.com", "google.com",
  235. TargetEmbeddingType::kInterstitial},
  236. {"foo-googlé.com-bar.com", "google.com",
  237. TargetEmbeddingType::kInterstitial},
  238. // The basic states
  239. {"google.com.foo.com", "google.com", TargetEmbeddingType::kInterstitial},
  240. // - before the domain name should be ignored.
  241. {"foo-google.com-bar.com", "google.com",
  242. TargetEmbeddingType::kInterstitial},
  243. // The embedded target's TLD doesn't necessarily need to be followed by a
  244. // '-' and could be a subdomain by itself.
  245. {"foo-google.com.foo.com", "google.com",
  246. TargetEmbeddingType::kInterstitial},
  247. {"a.b.c.d.e.f.g.h.foo-google.com.foo.com", "google.com",
  248. TargetEmbeddingType::kInterstitial},
  249. {"a.b.c.d.e.f.g.h.google.com-foo.com", "google.com",
  250. TargetEmbeddingType::kInterstitial},
  251. {"1.2.3.4.5.6.google.com-foo.com", "google.com",
  252. TargetEmbeddingType::kInterstitial},
  253. // Target domain could be in the middle of subdomains.
  254. {"foo.google.com.foo.com", "google.com",
  255. TargetEmbeddingType::kInterstitial},
  256. // The target domain and its tld should be next to each other.
  257. {"foo-google.l.com-foo.com", "", TargetEmbeddingType::kNone},
  258. // Target domain might be separated with a dash instead of dot.
  259. {"foo.google-com-foo.com", "google.com",
  260. TargetEmbeddingType::kInterstitial},
  261. // Allowlisted domains should not trigger heuristic.
  262. {"scholar.google.com.foo.com", "", TargetEmbeddingType::kNone},
  263. {"scholar.google.com-google.com.foo.com", "google.com",
  264. TargetEmbeddingType::kInterstitial},
  265. {"google.com-scholar.google.com.foo.com", "google.com",
  266. TargetEmbeddingType::kInterstitial},
  267. {"foo.scholar.google.com.foo.com", "", TargetEmbeddingType::kNone},
  268. {"scholar.foo.google.com.foo.com", "google.com",
  269. TargetEmbeddingType::kInterstitial},
  270. // e2LDs should be longer than 3 characters.
  271. {"hp.com-foo.com", "", TargetEmbeddingType::kNone},
  272. // Targets with common words as e2LD are not considered embedded targets
  273. // either for all TLDs or another-TLD matching.
  274. {"foo.jobs.com-foo.com", "", TargetEmbeddingType::kNone},
  275. {"foo.office.com-foo.com", "office.com",
  276. TargetEmbeddingType::kInterstitial},
  277. {"foo.jobs.org-foo.com", "", TargetEmbeddingType::kNone},
  278. {"foo.office.org-foo.com", "", TargetEmbeddingType::kNone},
  279. // Common words (like 'jobs' are included in the big common word list.
  280. // Ensure that the supplemental kCommonWords list is also checked.
  281. {"foo.hoteles.com-foo.com", "", TargetEmbeddingType::kNone},
  282. // Targets could be embedded without their dots and dashes.
  283. {"googlecom-foo.com", "google.com", TargetEmbeddingType::kInterstitial},
  284. {"foo.googlecom-foo.com", "google.com",
  285. TargetEmbeddingType::kInterstitial},
  286. // But should not be detected if they're using a common word. weather.com
  287. // is on the top domain list, but 'weather' is a common word.
  288. {"weathercom-foo.com", "", TargetEmbeddingType::kNone},
  289. // And should also not be detected if they're too short. vk.com is on the
  290. // top domain list, but is shorter than kMinE2LDLengthForTargetEmbedding.
  291. {"vkcom-foo.com", "", TargetEmbeddingType::kNone},
  292. // Ensure legitimate domains don't trigger.
  293. {"foo.google.com", "", TargetEmbeddingType::kNone},
  294. {"foo.bar.google.com", "", TargetEmbeddingType::kNone},
  295. {"google.com", "", TargetEmbeddingType::kNone},
  296. {"google.co.uk", "", TargetEmbeddingType::kNone},
  297. {"google.randomreg-login.com", "", TargetEmbeddingType::kNone},
  298. {"com.foo.com", "", TargetEmbeddingType::kNone},
  299. // Multipart eTLDs should work.
  300. {"foo.google.co.uk.foo.com", "google.co.uk",
  301. TargetEmbeddingType::kInterstitial},
  302. {"foo.highengagement-co-uk.foo.com", "highengagement.co.uk",
  303. TargetEmbeddingType::kInterstitial},
  304. // Cross-TLD matches should not trigger, even when they're embedding
  305. // another domain, even when using a de-facto public eTLD.
  306. {"google.com.mx", "", TargetEmbeddingType::kNone}, // public
  307. {"google.com.de", "", TargetEmbeddingType::kNone}, // de-facto public
  308. // Engaged sites should trigger as specifically as possible, and should
  309. // trigger preferentially to top sites when possible.
  310. {"foo.highengagement.com.foo.com", "highengagement.com",
  311. TargetEmbeddingType::kInterstitial},
  312. {"foo.subdomain.highengagement.com.foo.com",
  313. "subdomain.highengagement.com", TargetEmbeddingType::kInterstitial},
  314. {"foo.subdomain.google.com.foo.com", "subdomain.google.com",
  315. TargetEmbeddingType::kInterstitial},
  316. // Skeleton matching should work against engaged sites at a eTLD+1 level,
  317. {"highengagement.inthesubdomain.com-foo.com",
  318. "highengagement.inthesubdomain.com", TargetEmbeddingType::kInterstitial},
  319. // but only if the bare eTLD+1, or www.[eTLD+1] has been engaged.
  320. {"subdomain.highéngagement.com-foo.com", "highengagement.com",
  321. TargetEmbeddingType::kInterstitial},
  322. {"subdomain.highéngagementwithwww.com-foo.com",
  323. "highengagementwithwww.com", TargetEmbeddingType::kInterstitial},
  324. {"other.inthésubdomain.com-foo.com", "", TargetEmbeddingType::kNone},
  325. // Ideally, we'd be able to combine subdomains and skeleton matching, but
  326. // our current algorithm can't detect that precisely.
  327. {"highengagement.inthésubdomain.com-foo.com", "",
  328. TargetEmbeddingType::kNone},
  329. // Domains should be allowed to embed themselves.
  330. {"highengagement.com.highengagement.com", "", TargetEmbeddingType::kNone},
  331. {"subdomain.highengagement.com.highengagement.com", "",
  332. TargetEmbeddingType::kNone},
  333. {"nothighengagement.highengagement.com.highengagement.com", "",
  334. TargetEmbeddingType::kNone},
  335. {"google.com.google.com", "", TargetEmbeddingType::kNone},
  336. {"www.google.com.google.com", "", TargetEmbeddingType::kNone},
  337. // Detect embeddings at the end of the domain, too, but as a Safety Tip.
  338. {"www-google.com", "google.com", TargetEmbeddingType::kSafetyTip},
  339. {"www-highengagement.com", "highengagement.com",
  340. TargetEmbeddingType::kSafetyTip},
  341. {"subdomain-highengagement.com", "subdomain.highengagement.com",
  342. TargetEmbeddingType::kSafetyTip},
  343. // If the match duplicates the TLD, it's not quite tail-embedding.
  344. {"google-com.com", "google.com", TargetEmbeddingType::kInterstitial},
  345. // If there are multiple options, it should choose the more severe one.
  346. {"google-com.google-com.com", "google.com",
  347. TargetEmbeddingType::kInterstitial},
  348. {"subdomain.google-com.google-com.com", "google.com",
  349. TargetEmbeddingType::kInterstitial},
  350. {"google.com-google.com-google.com", "google.com",
  351. TargetEmbeddingType::kInterstitial},
  352. // Ignore end-of-domain embeddings when they're also cross-TLD matches.
  353. {"google.com.mx", "", TargetEmbeddingType::kNone},
  354. // For a small set of high-value domains that are also common words (see
  355. // kDomainsPermittedInEndEmbeddings), we block all embeddings except those
  356. // at the very end of the domain (e.g. foo-{domain.com}). Ensure this
  357. // works for domains on the list, but not for others.
  358. {"office.com-foo.com", "office.com", TargetEmbeddingType::kInterstitial},
  359. {"example-office.com", "", TargetEmbeddingType::kNone},
  360. {"example-google.com", "google.com", TargetEmbeddingType::kSafetyTip},
  361. };
  362. reputation::InitializeBlankLookalikeAllowlistForTesting();
  363. auto* config_proto = reputation::GetSafetyTipsRemoteConfigProto();
  364. for (auto& test_case : kTestCases) {
  365. std::string safe_hostname;
  366. TargetEmbeddingType embedding_type = GetTargetEmbeddingType(
  367. test_case.hostname, kEngagedSites,
  368. base::BindRepeating(&IsGoogleScholar), config_proto, &safe_hostname);
  369. if (test_case.expected_type != TargetEmbeddingType::kNone) {
  370. EXPECT_EQ(safe_hostname, test_case.expected_safe_host)
  371. << test_case.hostname << " should trigger on "
  372. << test_case.expected_safe_host << ", but "
  373. << (safe_hostname.empty() ? "it didn't trigger at all."
  374. : "triggered on " + safe_hostname);
  375. EXPECT_EQ(embedding_type, test_case.expected_type)
  376. << test_case.hostname << " should trigger "
  377. << TargetEmbeddingTypeToString(test_case.expected_type) << " against "
  378. << test_case.expected_safe_host << " but it returned "
  379. << TargetEmbeddingTypeToString(embedding_type);
  380. } else {
  381. EXPECT_EQ(embedding_type, TargetEmbeddingType::kNone)
  382. << test_case.hostname << " unexpectedly triggered "
  383. << TargetEmbeddingTypeToString(embedding_type) << " against "
  384. << safe_hostname;
  385. }
  386. }
  387. }
  388. TEST(LookalikeUrlUtilTest, TargetEmbeddingIgnoresComponentWordlist) {
  389. const std::vector<DomainInfo> kEngagedSites = {
  390. GetDomainInfo(GURL("https://commonword.com")),
  391. GetDomainInfo(GURL("https://uncommonword.com")),
  392. };
  393. reputation::SetSafetyTipAllowlistPatterns({}, {}, {"commonword"});
  394. auto* config_proto = reputation::GetSafetyTipsRemoteConfigProto();
  395. TargetEmbeddingType embedding_type;
  396. std::string safe_hostname;
  397. // Engaged sites using uncommon words are still blocked.
  398. embedding_type = GetTargetEmbeddingType(
  399. "uncommonword.com.evil.com", kEngagedSites,
  400. base::BindRepeating(&IsGoogleScholar), config_proto, &safe_hostname);
  401. EXPECT_EQ(embedding_type, TargetEmbeddingType::kInterstitial);
  402. // But engaged sites using common words are not blocked.
  403. embedding_type = GetTargetEmbeddingType(
  404. "commonword.com.evil.com", kEngagedSites,
  405. base::BindRepeating(&IsGoogleScholar), config_proto, &safe_hostname);
  406. EXPECT_EQ(embedding_type, TargetEmbeddingType::kNone);
  407. }
  408. struct GetETLDPlusOneTestCase {
  409. const std::string hostname;
  410. const std::string expected_etldp1;
  411. };
  412. TEST(LookalikeUrlUtilTest, GetETLDPlusOneHandlesSpecialRegistries) {
  413. const std::vector<GetETLDPlusOneTestCase> kTestCases = {
  414. // Trivial test cases for public registries.
  415. {"google.com", "google.com"},
  416. {"www.google.com", "google.com"},
  417. {"www.google.co.uk", "google.co.uk"},
  418. // .com.de is a de-facto public registry.
  419. {"www.google.com.de", "google.com.de"},
  420. // .cloud.goog is a private registry.
  421. {"www.example.cloud.goog", "cloud.goog"},
  422. };
  423. for (auto& test_case : kTestCases) {
  424. EXPECT_EQ(GetETLDPlusOne(test_case.hostname), test_case.expected_etldp1);
  425. }
  426. }
  427. // Tests for the character swap heuristic.
  428. TEST(LookalikeUrlUtilTest, HasOneCharacterSwap) {
  429. const struct TestCase {
  430. const wchar_t* str1;
  431. const wchar_t* str2;
  432. bool expected;
  433. } kTestCases[] = {{L"", L"", false},
  434. {L"", L"a", false},
  435. {L"", L"ab", false},
  436. {L"a", L"ab", false},
  437. {L"a", L"ba", false},
  438. {L"abc.com", L"abc.com", false},
  439. {L"abc.com", L"abcd.com", false},
  440. {L"domain.com", L"nomaid.com", false},
  441. // Two swaps (ab to ba, ba to ab):
  442. {L"abba", L"baab", false},
  443. {L"ab", L"ba", true},
  444. {L"abba", L"baba", true},
  445. {L"abaaa", L"baaaa", true},
  446. {L"abcaa", L"bacaa", true},
  447. {L"aaaab", L"aaaba", true},
  448. {L"aacab", L"aacba", true},
  449. {L"aabaa", L"abaaa", true},
  450. {L"aabcc", L"abacc", true},
  451. {L"aabaa", L"aaaba", true},
  452. {L"ccbaa", L"ccaba", true},
  453. {L"domain.com", L"doamin.com", true},
  454. {L"gmail.com", L"gmailc.om", true},
  455. {L"gmailc.om", L"gmail.com", true}};
  456. for (const TestCase& test_case : kTestCases) {
  457. bool result = HasOneCharacterSwap(base::WideToUTF16(test_case.str1),
  458. base::WideToUTF16(test_case.str2));
  459. EXPECT_EQ(test_case.expected, result)
  460. << "when comparing " << test_case.str1 << " with " << test_case.str2;
  461. }
  462. }
  463. TEST(LookalikeUrlUtilTest, IsHeuristicEnabledForHostname) {
  464. reputation::SafetyTipsConfig proto;
  465. reputation::HeuristicLaunchConfig* config = proto.add_launch_config();
  466. config->set_heuristic(reputation::HeuristicLaunchConfig::
  467. HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES);
  468. // Minimum rollout percentages to enable a heuristic on each site on Stable
  469. // channel:
  470. // example1.com: 79%
  471. // example2.com: 16%
  472. // example3.com: 36%
  473. // Slowly ramp up the launch and cover more sites on Stable channel.
  474. config->set_launch_percentage(0);
  475. EXPECT_FALSE(IsHeuristicEnabledForHostname(
  476. &proto,
  477. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  478. "example1.com", Channel::STABLE));
  479. EXPECT_FALSE(IsHeuristicEnabledForHostname(
  480. &proto,
  481. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  482. "example2.com", Channel::STABLE));
  483. EXPECT_FALSE(IsHeuristicEnabledForHostname(
  484. &proto,
  485. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  486. "example3.com", Channel::STABLE));
  487. config->set_launch_percentage(25);
  488. EXPECT_FALSE(IsHeuristicEnabledForHostname(
  489. &proto,
  490. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  491. "example1.com", Channel::STABLE));
  492. EXPECT_TRUE(IsHeuristicEnabledForHostname(
  493. &proto,
  494. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  495. "example2.com", Channel::STABLE));
  496. EXPECT_FALSE(IsHeuristicEnabledForHostname(
  497. &proto,
  498. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  499. "example3.com", Channel::STABLE));
  500. config->set_launch_percentage(50);
  501. EXPECT_FALSE(IsHeuristicEnabledForHostname(
  502. &proto,
  503. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  504. "example1.com", Channel::STABLE));
  505. EXPECT_TRUE(IsHeuristicEnabledForHostname(
  506. &proto,
  507. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  508. "example2.com", Channel::STABLE));
  509. EXPECT_TRUE(IsHeuristicEnabledForHostname(
  510. &proto,
  511. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  512. "example3.com", Channel::STABLE));
  513. config->set_launch_percentage(100);
  514. EXPECT_TRUE(IsHeuristicEnabledForHostname(
  515. &proto,
  516. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  517. "example1.com", Channel::STABLE));
  518. EXPECT_TRUE(IsHeuristicEnabledForHostname(
  519. &proto,
  520. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  521. "example2.com", Channel::STABLE));
  522. EXPECT_TRUE(IsHeuristicEnabledForHostname(
  523. &proto,
  524. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  525. "example3.com", Channel::STABLE));
  526. // On Beta, launch is always at 50%.
  527. config->set_launch_percentage(0);
  528. EXPECT_FALSE(IsHeuristicEnabledForHostname(
  529. &proto,
  530. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  531. "example1.com", Channel::BETA));
  532. EXPECT_TRUE(IsHeuristicEnabledForHostname(
  533. &proto,
  534. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  535. "example2.com", Channel::BETA));
  536. EXPECT_TRUE(IsHeuristicEnabledForHostname(
  537. &proto,
  538. reputation::HeuristicLaunchConfig::HEURISTIC_CHARACTER_SWAP_ENGAGED_SITES,
  539. "example3.com", Channel::BETA));
  540. }
  541. class ComboSquattingTest : public testing::Test {
  542. protected:
  543. void SetUp() override {
  544. SetComboSquattingParamsForTesting(kComboSquattingParams);
  545. }
  546. void TearDown() override { ResetComboSquattingParamsForTesting(); }
  547. };
  548. // Test for Combo Squatting check of domains.
  549. TEST_F(ComboSquattingTest, IsComboSquatting) {
  550. const std::vector<DomainInfo> kEngagedSites = {
  551. // An engaged site which is not in the hard coded brand names.
  552. GetDomainInfo(GURL("https://engagedsite.com")),
  553. // An engaged site which is duplicate with a hard coded brand name.
  554. GetDomainInfo(GURL("https://subdomain.google.com")),
  555. // An engaged site with length less than threshold (4) for
  556. // consideration.
  557. GetDomainInfo(GURL("https://len.com")),
  558. // An engaged site with a registry other than com.
  559. GetDomainInfo(GURL("https://testcombo.org")),
  560. };
  561. const struct TestCase {
  562. const char* domain;
  563. const char* expected_suggested_domain;
  564. const ComboSquattingType expected_type;
  565. ;
  566. } kTestCases[] = {
  567. // Not Combo Squatting (CSQ).
  568. {"google.com", "", ComboSquattingType::kNone},
  569. {"youtube.ca", "", ComboSquattingType::kNone},
  570. // Not CSQ, contains subdomains.
  571. {"login.google.com", "", ComboSquattingType::kNone},
  572. // Not CSQ, non registrable domains.
  573. {"google-login.test", "", ComboSquattingType::kNone},
  574. // CSQ with "-".
  575. {"google-online.com", "google.com", ComboSquattingType::kHardCoded},
  576. // CSQ with more than one keyword (login, online) with "-".
  577. {"google-login-online.com", "google.com", ComboSquattingType::kHardCoded},
  578. // CSQ with one keyword (online) and one random word (one) with "-".
  579. {"one-sample-online.com", "sample.com", ComboSquattingType::kHardCoded},
  580. // Not CSQ, with a keyword (test) as TLD.
  581. {"www.example.test", "", ComboSquattingType::kNone},
  582. // CSQ with more than one brand (google, youtube) with "-".
  583. {"google-youtube-account.com", "google.com",
  584. ComboSquattingType::kHardCoded},
  585. // CSQ without separator.
  586. {"loginsample.com", "sample.com", ComboSquattingType::kHardCoded},
  587. // Not CSQ with a keyword (ample) inside brand name (sample).
  588. {"sample.com", "", ComboSquattingType::kNone},
  589. // Current version of the heuristic cannot flag this kind of CSQ
  590. // with a keyword (ample) inside brand name (sample) and as an added
  591. // keyword to the domain.
  592. {"sample-ample.com", "", ComboSquattingType::kNone},
  593. // CSQ with more than one keyword (account, online) without separator.
  594. {"accountexampleonline.com", "example.com",
  595. ComboSquattingType::kHardCoded},
  596. // CSQ with one keyword (login) and one random word (one) without "-".
  597. {"oneyoutubelogin.com", "youtube.com", ComboSquattingType::kHardCoded},
  598. // Not CSQ, google is a public TLD.
  599. {"online.google", "", ComboSquattingType::kNone},
  600. // Not CSQ, brand name (vice) is part of keyword (service).
  601. {"keyservices.com", "", ComboSquattingType::kNone},
  602. // CSQ, brand name (engagedsite) is from engaged sites list.
  603. {"engagedsite-login.com", "engagedsite.com",
  604. ComboSquattingType::kSiteEngagement},
  605. // Not CSQ, brand name (len) is from engaged sites list but it is short.
  606. {"len-online.com", "", ComboSquattingType::kNone},
  607. // CSQ, brand name (googlé) is one of the hard coded brand names and has
  608. // IDN spoofing as well.
  609. {"googlé-login.com", "google.com", ComboSquattingType::kHardCoded},
  610. // CSQ, brand name (engagedsité) is one of the brand names from engaged
  611. // sites and has IDN spoofing as well.
  612. {"engagedsité-online.com", "engagedsite.com",
  613. ComboSquattingType::kSiteEngagement},
  614. // CSQ, keyword (lógin) has IDN spoofing.
  615. {"google-lógin.com", "google.com", ComboSquattingType::kHardCoded},
  616. // CSQ, CSQ with more than one brand (googlé, youtubé) with "-" and IDN
  617. // spoofing.
  618. {"googlé-youtubé-account.com", "google.com",
  619. ComboSquattingType::kHardCoded},
  620. // Not CSQ.
  621. {"ónline.googlé", "", ComboSquattingType::kNone},
  622. // Not CSQ, it has IDN spoofing but brand name (vicé) is part of keyword
  623. // (servicé).
  624. {"keyservicés.com", "", ComboSquattingType::kNone},
  625. // CSQ without separator and with IDN spoofing in the keyword.
  626. {"lóginsample.com", "sample.com", ComboSquattingType::kHardCoded},
  627. // CSQ without separator and with IDN spoofing in the brand name.
  628. {"loginsamplé.com", "sample.com", ComboSquattingType::kHardCoded},
  629. // Not CSQ, skeleton of brand name (lén) is from engaged sites list but it
  630. // is short.
  631. {"lén-online.com", "", ComboSquattingType::kNone},
  632. // CSQ when domain and registry are in top domains.
  633. {"google-login.co.kr", "google.co.kr", ComboSquattingType::kHardCoded},
  634. // CSQ when brand name is in hard coded brand names, but domain and
  635. // registry are not in top domains.
  636. {"google-login.co.ir", "google.com", ComboSquattingType::kHardCoded},
  637. // CSQ when domain and registry are in engaged sites, with registry other
  638. // than com.
  639. {"testcomboonline.org", "testcombo.org",
  640. ComboSquattingType::kSiteEngagement},
  641. };
  642. for (const TestCase& test_case : kTestCases) {
  643. auto navigated =
  644. GetDomainInfo(GURL(std::string(url::kHttpsScheme) +
  645. url::kStandardSchemeSeparator + test_case.domain));
  646. std::string matched_domain;
  647. ComboSquattingType type =
  648. GetComboSquattingType(navigated, kEngagedSites, &matched_domain);
  649. EXPECT_EQ(std::string(test_case.expected_suggested_domain), matched_domain);
  650. EXPECT_EQ(test_case.expected_type, type);
  651. }
  652. }