Commit fd747c4d authored by Livvie Lin's avatar Livvie Lin Committed by Commit Bot

Add IDN spoof check heuristic to iOS lookalike interstitial

This CL also componentizes the IDN spoof checker heuristic.

Change-Id: I674775cbe8dd19a2ca02551d2a72b39c782ffb82
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2315082
Commit-Queue: Livvie Lin <livvielin@chromium.org>
Reviewed-by: default avatarMustafa Emre Acer <meacer@chromium.org>
Cr-Commit-Position: refs/heads/master@{#791323}
parent 96819cfe
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
#include "base/bind.h" #include "base/bind.h"
#include "base/callback.h" #include "base/callback.h"
#include "base/i18n/char_iterator.h"
#include "base/metrics/field_trial_params.h" #include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h" #include "base/metrics/histogram_macros.h"
#include "base/stl_util.h" #include "base/stl_util.h"
...@@ -64,44 +63,6 @@ size_t FindFirstCrossSiteURL(const std::vector<GURL>& redirect_chain) { ...@@ -64,44 +63,6 @@ size_t FindFirstCrossSiteURL(const std::vector<GURL>& redirect_chain) {
return 0; return 0;
} }
bool IsASCII(UChar32 codepoint) {
return !(codepoint & ~0x7F);
}
// Returns true if |codepoint| has emoji related properties.
bool IsEmojiRelatedCodepoint(UChar32 codepoint) {
return u_hasBinaryProperty(codepoint, UCHAR_EMOJI) ||
// Characters that have emoji presentation by default (e.g. hourglass)
u_hasBinaryProperty(codepoint, UCHAR_EMOJI_PRESENTATION) ||
// Characters displayed as country flags when used as a valid pair.
// E.g. Regional Indicator Symbol Letter B used once in a string
// is rendered as 🇧, used twice is rendered as the flag of Barbados
// (with country code BB). It's therefore possible to come up with
// a spoof using regional indicator characters as text, but these
// domain names will be readily punycoded and detecting pairs isn't
// easy so we keep the code simple here.
u_hasBinaryProperty(codepoint, UCHAR_REGIONAL_INDICATOR) ||
// Pictographs such as Black Cross On Shield (U+26E8).
u_hasBinaryProperty(codepoint, UCHAR_EXTENDED_PICTOGRAPHIC);
}
// Returns true if |text| contains only ASCII characters, pictographs
// or emojis. This check is only used to determine if a domain that already
// failed spoof checks should be blocked by an interstitial. Ideally, we would
// check this for non-ASCII scripts as well (e.g. Cyrillic + emoji), but such
// usage isn't common.
bool IsASCIIAndEmojiOnly(const base::StringPiece16& text) {
base::i18n::UTF16CharIterator iter(text.data(), text.length());
while (!iter.end()) {
const UChar32 codepoint = iter.get();
if (!IsASCII(codepoint) && !IsEmojiRelatedCodepoint(codepoint)) {
return false;
}
iter.Advance();
}
return true;
}
} // namespace } // namespace
bool IsSafeRedirect(const std::string& matching_domain, bool IsSafeRedirect(const std::string& matching_domain,
...@@ -318,33 +279,6 @@ void LookalikeUrlNavigationThrottle::PerformChecksDeferred( ...@@ -318,33 +279,6 @@ void LookalikeUrlNavigationThrottle::PerformChecksDeferred(
CancelDeferredNavigation(result); CancelDeferredNavigation(result);
} }
bool ShouldBlockBySpoofCheckResult(const DomainInfo& navigated_domain) {
// Here, only a subset of spoof checks that cause an IDN to fallback to
// punycode are configured to show an interstitial.
switch (navigated_domain.idn_result.spoof_check_result) {
case url_formatter::IDNSpoofChecker::Result::kNone:
case url_formatter::IDNSpoofChecker::Result::kSafe:
return false;
case url_formatter::IDNSpoofChecker::Result::kICUSpoofChecks:
// If the eTLD+1 contains only a mix of ASCII + Emoji, allow.
return !IsASCIIAndEmojiOnly(navigated_domain.idn_result.result);
case url_formatter::IDNSpoofChecker::Result::kDeviationCharacters:
// Failures because of deviation characters, especially ß, is common.
return false;
case url_formatter::IDNSpoofChecker::Result::kTLDSpecificCharacters:
case url_formatter::IDNSpoofChecker::Result::kUnsafeMiddleDot:
case url_formatter::IDNSpoofChecker::Result::kWholeScriptConfusable:
case url_formatter::IDNSpoofChecker::Result::kDigitLookalikes:
case url_formatter::IDNSpoofChecker::Result::
kNonAsciiLatinCharMixedWithNonLatin:
case url_formatter::IDNSpoofChecker::Result::kDangerousPattern:
return true;
}
}
ThrottleCheckResult LookalikeUrlNavigationThrottle::PerformChecks( ThrottleCheckResult LookalikeUrlNavigationThrottle::PerformChecks(
const GURL& url, const GURL& url,
const DomainInfo& navigated_domain, const DomainInfo& navigated_domain,
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "base/bind.h" #include "base/bind.h"
#include "base/callback.h" #include "base/callback.h"
#include "base/feature_list.h" #include "base/feature_list.h"
#include "base/i18n/char_iterator.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/memory/scoped_refptr.h" #include "base/memory/scoped_refptr.h"
#include "base/memory/singleton.h" #include "base/memory/singleton.h"
...@@ -636,3 +637,68 @@ TargetEmbeddingType GetTargetEmbeddingType( ...@@ -636,3 +637,68 @@ TargetEmbeddingType GetTargetEmbeddingType(
} }
return TargetEmbeddingType::kNone; return TargetEmbeddingType::kNone;
} }
bool IsASCII(UChar32 codepoint) {
return !(codepoint & ~0x7F);
}
// Returns true if |codepoint| has emoji related properties.
bool IsEmojiRelatedCodepoint(UChar32 codepoint) {
return u_hasBinaryProperty(codepoint, UCHAR_EMOJI) ||
// Characters that have emoji presentation by default (e.g. hourglass)
u_hasBinaryProperty(codepoint, UCHAR_EMOJI_PRESENTATION) ||
// Characters displayed as country flags when used as a valid pair.
// E.g. Regional Indicator Symbol Letter B used once in a string
// is rendered as 🇧, used twice is rendered as the flag of Barbados
// (with country code BB). It's therefore possible to come up with
// a spoof using regional indicator characters as text, but these
// domain names will be readily punycoded and detecting pairs isn't
// easy so we keep the code simple here.
u_hasBinaryProperty(codepoint, UCHAR_REGIONAL_INDICATOR) ||
// Pictographs such as Black Cross On Shield (U+26E8).
u_hasBinaryProperty(codepoint, UCHAR_EXTENDED_PICTOGRAPHIC);
}
// Returns true if |text| contains only ASCII characters, pictographs
// or emojis. This check is only used to determine if a domain that already
// failed spoof checks should be blocked by an interstitial. Ideally, we would
// check this for non-ASCII scripts as well (e.g. Cyrillic + emoji), but such
// usage isn't common.
bool IsASCIIAndEmojiOnly(const base::StringPiece16& text) {
base::i18n::UTF16CharIterator iter(text.data(), text.length());
while (!iter.end()) {
const UChar32 codepoint = iter.get();
if (!IsASCII(codepoint) && !IsEmojiRelatedCodepoint(codepoint)) {
return false;
}
iter.Advance();
}
return true;
}
bool ShouldBlockBySpoofCheckResult(const DomainInfo& navigated_domain) {
// Here, only a subset of spoof checks that cause an IDN to fallback to
// punycode are configured to show an interstitial.
switch (navigated_domain.idn_result.spoof_check_result) {
case url_formatter::IDNSpoofChecker::Result::kNone:
case url_formatter::IDNSpoofChecker::Result::kSafe:
return false;
case url_formatter::IDNSpoofChecker::Result::kICUSpoofChecks:
// If the eTLD+1 contains only a mix of ASCII + Emoji, allow.
return !IsASCIIAndEmojiOnly(navigated_domain.idn_result.result);
case url_formatter::IDNSpoofChecker::Result::kDeviationCharacters:
// Failures because of deviation characters, especially ß, is common.
return false;
case url_formatter::IDNSpoofChecker::Result::kTLDSpecificCharacters:
case url_formatter::IDNSpoofChecker::Result::kUnsafeMiddleDot:
case url_formatter::IDNSpoofChecker::Result::kWholeScriptConfusable:
case url_formatter::IDNSpoofChecker::Result::kDigitLookalikes:
case url_formatter::IDNSpoofChecker::Result::
kNonAsciiLatinCharMixedWithNonLatin:
case url_formatter::IDNSpoofChecker::Result::kDangerousPattern:
return true;
}
}
...@@ -186,4 +186,7 @@ TargetEmbeddingType GetTargetEmbeddingType( ...@@ -186,4 +186,7 @@ TargetEmbeddingType GetTargetEmbeddingType(
const LookalikeTargetAllowlistChecker& in_target_allowlist, const LookalikeTargetAllowlistChecker& in_target_allowlist,
std::string* safe_hostname); std::string* safe_hostname);
// Returns true if a navigation to an IDN should be blocked.
bool ShouldBlockBySpoofCheckResult(const DomainInfo& navigated_domain);
#endif // COMPONENTS_LOOKALIKES_CORE_LOOKALIKE_URL_UTIL_H_ #endif // COMPONENTS_LOOKALIKES_CORE_LOOKALIKE_URL_UTIL_H_
...@@ -21,6 +21,7 @@ source_set("lookalikes") { ...@@ -21,6 +21,7 @@ source_set("lookalikes") {
deps = [ deps = [
"//base", "//base",
"//components/lookalikes/core", "//components/lookalikes/core",
"//components/lookalikes/core:features",
"//components/security_interstitials/core", "//components/security_interstitials/core",
"//components/ukm/ios", "//components/ukm/ios",
"//components/url_formatter/spoof_checks/top_domains:common", "//components/url_formatter/spoof_checks/top_domains:common",
...@@ -43,6 +44,7 @@ source_set("unit_tests") { ...@@ -43,6 +44,7 @@ source_set("unit_tests") {
":lookalikes", ":lookalikes",
"//base/test:test_support", "//base/test:test_support",
"//components/lookalikes/core", "//components/lookalikes/core",
"//components/lookalikes/core:features",
"//components/security_interstitials/core", "//components/security_interstitials/core",
"//components/ukm:test_support", "//components/ukm:test_support",
"//components/ukm/ios", "//components/ukm/ios",
......
This directory contains shared iOS lookalike code.
End-to-end tests are located in ios/chrome/browser/web. The ShouldAllowResponse
tab helper code needs to be unit tested here in components, since
lookalike_url_egtest.mm uses a custom policy decider that overrides that method.
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
#import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.h" #import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.h"
#include "base/feature_list.h"
#include "components/lookalikes/core/features.h"
#include "components/lookalikes/core/lookalike_url_ui_util.h" #include "components/lookalikes/core/lookalike_url_ui_util.h"
#include "components/lookalikes/core/lookalike_url_util.h" #include "components/lookalikes/core/lookalike_url_util.h"
#include "components/ukm/ios/ukm_url_recorder.h" #include "components/ukm/ios/ukm_url_recorder.h"
...@@ -96,6 +98,15 @@ void LookalikeUrlTabHelper::ShouldAllowResponse( ...@@ -96,6 +98,15 @@ void LookalikeUrlTabHelper::ShouldAllowResponse(
}); });
if (!GetMatchingDomain(navigated_domain, engaged_sites, in_target_allowlist, if (!GetMatchingDomain(navigated_domain, engaged_sites, in_target_allowlist,
&matched_domain, &match_type)) { &matched_domain, &match_type)) {
if (base::FeatureList::IsEnabled(
lookalikes::features::kLookalikeInterstitialForPunycode) &&
ShouldBlockBySpoofCheckResult(navigated_domain)) {
match_type = LookalikeUrlMatchType::kFailedSpoofChecks;
RecordUMAFromMatchType(match_type);
std::move(callback).Run(CreateLookalikeErrorDecision());
return;
}
std::move(callback).Run(CreateAllowDecision()); std::move(callback).Run(CreateAllowDecision());
return; return;
} }
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
#import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.h" #import "ios/components/security_interstitials/lookalikes/lookalike_url_tab_helper.h"
#include "base/test/metrics/histogram_tester.h" #include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/lookalikes/core/features.h"
#include "ios/components/security_interstitials/lookalikes/lookalike_url_container.h" #include "ios/components/security_interstitials/lookalikes/lookalike_url_container.h"
#include "ios/components/security_interstitials/lookalikes/lookalike_url_tab_allow_list.h" #include "ios/components/security_interstitials/lookalikes/lookalike_url_tab_allow_list.h"
#import "ios/web/public/navigation/web_state_policy_decider.h" #import "ios/web/public/navigation/web_state_policy_decider.h"
...@@ -91,3 +93,21 @@ TEST_F(LookalikeUrlTabHelperTest, ShouldAllowResponse) { ...@@ -91,3 +93,21 @@ TEST_F(LookalikeUrlTabHelperTest, ShouldAllowResponse) {
histogram_tester_.ExpectTotalCount(lookalikes::kHistogramName, 1); histogram_tester_.ExpectTotalCount(lookalikes::kHistogramName, 1);
} }
// Tests that ShouldAllowResponse properly blocks lookalike navigations
// to IDNs when the feature is enabled.
TEST_F(LookalikeUrlTabHelperTest, ShouldAllowResponseForPunycode) {
GURL lookalike_url("https://ɴoτ-τoρ-ďoᛖaiɴ.com/");
base::test::ScopedFeatureList feature_list_disabled;
feature_list_disabled.InitAndDisableFeature(
lookalikes::features::kLookalikeInterstitialForPunycode);
EXPECT_TRUE(ShouldAllowResponseUrl(lookalike_url, /*main_frame=*/true)
.ShouldAllowNavigation());
base::test::ScopedFeatureList feature_list_enabled;
feature_list_enabled.InitAndEnableFeature(
lookalikes::features::kLookalikeInterstitialForPunycode);
EXPECT_FALSE(ShouldAllowResponseUrl(lookalike_url, /*main_frame=*/true)
.ShouldAllowNavigation());
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment