blob: 868886266f18accd1451f1fbfb078a651755747e [file]
// Copyright 2019 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/safety_tips_config.h"
#include <algorithm>
#include "base/no_destructor.h"
#include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
#include "third_party/re2/src/re2/re2.h"
#include "url/gurl.h"
using safe_browsing::V4ProtocolManagerUtil;
namespace lookalikes {
namespace {
class SafetyTipsConfigSingleton {
public:
void SetProto(std::unique_ptr<reputation::SafetyTipsConfig> proto) {
proto_ = std::move(proto);
}
reputation::SafetyTipsConfig* GetProto() const { return proto_.get(); }
static SafetyTipsConfigSingleton& GetInstance() {
static base::NoDestructor<SafetyTipsConfigSingleton> instance;
return *instance;
}
private:
std::unique_ptr<reputation::SafetyTipsConfig> proto_;
};
// Given a URL, generates all possible variant URLs to check the blocklist for.
// This is conceptually almost identical to safe_browsing::UrlToFullHashes, but
// without the hashing step.
//
// Note: Blocking "a.b/c/" does NOT block http://a.b/c without the trailing /.
void UrlToSafetyTipPatterns(const GURL& url,
std::vector<std::string>* patterns) {
std::string canon_host;
std::string canon_path;
std::string canon_query;
V4ProtocolManagerUtil::CanonicalizeUrl(url, &canon_host, &canon_path,
&canon_query);
std::vector<std::string> hosts;
if (url.HostIsIPAddress()) {
hosts.push_back(url.GetHost());
} else {
V4ProtocolManagerUtil::GenerateHostVariantsToCheck(canon_host, &hosts);
}
std::vector<std::string> paths;
V4ProtocolManagerUtil::GeneratePathVariantsToCheck(canon_path, canon_query,
&paths);
for (const std::string& host : hosts) {
for (const std::string& path : paths) {
DCHECK(path.length() == 0 || path[0] == '/');
patterns->push_back(host + path);
}
}
}
// Return whether |canonical_url| is a member of the designated cohort.
bool IsUrlAllowedByCohort(const reputation::SafetyTipsConfig* proto,
const GURL& canonical_url,
unsigned cohort_index) {
DCHECK(proto);
DCHECK(canonical_url.is_valid());
// Ensure that the cohort index is valid before using it. If it isn't valid,
// we just pretend the cohort didn't include the canonical URL.
if (cohort_index >= static_cast<unsigned>(proto->cohort_size())) {
return false;
}
const auto& cohort = proto->cohort(cohort_index);
// For each possible URL pattern, see if any of the indicated allowed_index or
// canonical_index entries correspond to a matching pattern since both sets of
// indices are considered valid spoof targets.
std::vector<std::string> patterns;
UrlToSafetyTipPatterns(canonical_url, &patterns);
for (const auto& search_pattern : patterns) {
for (const unsigned allowed_index : cohort.allowed_index()) {
// Skip over invalid indices.
if (allowed_index >=
static_cast<unsigned>(proto->allowed_pattern_size())) {
continue;
}
const auto& pattern = proto->allowed_pattern(allowed_index).pattern();
if (pattern == search_pattern) {
return true;
}
}
for (const unsigned canonical_index : cohort.canonical_index()) {
// Skip over invalid indices.
if (canonical_index >=
static_cast<unsigned>(proto->canonical_pattern_size())) {
continue;
}
const auto& pattern = proto->canonical_pattern(canonical_index).pattern();
if (pattern == search_pattern) {
return true;
}
}
}
return false;
}
} // namespace
void SetSafetyTipsRemoteConfigProto(
std::unique_ptr<reputation::SafetyTipsConfig> proto) {
SafetyTipsConfigSingleton::GetInstance().SetProto(std::move(proto));
}
const reputation::SafetyTipsConfig* GetSafetyTipsRemoteConfigProto() {
return SafetyTipsConfigSingleton::GetInstance().GetProto();
}
bool IsUrlAllowlistedBySafetyTipsComponent(
const reputation::SafetyTipsConfig* proto,
const GURL& visited_url,
const GURL& canonical_url) {
DCHECK(proto);
DCHECK(visited_url.is_valid());
std::vector<std::string> patterns;
UrlToSafetyTipPatterns(visited_url, &patterns);
auto allowed_patterns = proto->allowed_pattern();
for (const auto& pattern : patterns) {
reputation::UrlPattern search_target;
search_target.set_pattern(pattern);
auto maybe_before = std::lower_bound(
allowed_patterns.begin(), allowed_patterns.end(), search_target,
[](const reputation::UrlPattern& a, const reputation::UrlPattern& b)
-> bool { return a.pattern() < b.pattern(); });
if (maybe_before != allowed_patterns.end() &&
pattern == maybe_before->pattern()) {
// If no cohorts are given, it's a universal allowlist entry.
if (maybe_before->cohort_index_size() == 0) {
return true;
}
for (const unsigned cohort_index : maybe_before->cohort_index()) {
if (IsUrlAllowedByCohort(proto, canonical_url, cohort_index)) {
return true;
}
}
}
}
return false;
}
bool IsTargetHostAllowlistedBySafetyTipsComponent(
const reputation::SafetyTipsConfig* proto,
const std::string& hostname) {
DCHECK(!hostname.empty());
if (proto == nullptr) {
return false;
}
for (const auto& host_pattern : proto->allowed_target_pattern()) {
if (!host_pattern.has_regex()) {
continue;
}
DCHECK(!host_pattern.regex().empty());
const re2::RE2 regex(host_pattern.regex());
DCHECK(regex.ok());
if (re2::RE2::FullMatch(hostname, regex)) {
return true;
}
}
return false;
}
bool IsCommonWordInConfigProto(const reputation::SafetyTipsConfig* proto,
const std::string& word) {
// proto is nullptr when running in non-Lookalike tests.
if (proto == nullptr) {
return false;
}
auto common_words = proto->common_word();
DCHECK(std::ranges::is_sorted(common_words.begin(), common_words.end()));
auto lower = std::lower_bound(
common_words.begin(), common_words.end(), word,
[](const std::string& a, const std::string& b) -> bool { return a < b; });
return lower != common_words.end() && word == *lower;
}
} // namespace lookalikes