blob: 492261350208662ea4555282b1b58442070a2ec4 [file]
// 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/safe_browsing/content/browser/client_side_detection_host.h"
#include <algorithm>
#include <memory>
#include <optional>
#include <utility>
#include "base/check_op.h"
#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner_helpers.h"
#include "base/task/thread_pool.h"
#include "base/time/default_tick_clock.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "base/uuid.h"
#include "components/autofill/content/browser/content_autofill_client.h"
#include "components/autofill/core/browser/field_types.h"
#include "components/autofill/core/browser/foundations/autofill_client.h"
#include "components/autofill/core/browser/foundations/scoped_autofill_managers_observation.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/history_types.h"
#include "components/permissions/permission_request_manager.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/content/browser/base_ui_manager.h"
#include "components/safe_browsing/content/browser/client_side_detection_feature_cache.h"
#include "components/safe_browsing/content/browser/client_side_detection_service.h"
#include "components/safe_browsing/content/browser/content_unsafe_resource_util.h"
#include "components/safe_browsing/content/common/safe_browsing.mojom.h"
#include "components/safe_browsing/core/browser/credit_card_form_event.h"
#include "components/safe_browsing/core/browser/db/allowlist_checker_client.h"
#include "components/safe_browsing/core/browser/db/database_manager.h"
#include "components/safe_browsing/core/browser/intelligent_scan_delegate.h"
#include "components/safe_browsing/core/browser/safe_browsing_token_fetcher.h"
#include "components/safe_browsing/core/browser/sync/sync_utils.h"
#include "components/safe_browsing/core/browser/verdict_cache_manager.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/proto/csd.pb.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/safe_browsing/core/common/safebrowsing_switches.h"
#include "components/safe_browsing/core/common/utils.h"
#include "components/security_interstitials/core/unsafe_resource_locator.h"
#include "components/url_formatter/url_fixer.h"
#include "components/zoom/zoom_controller.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "mojo/public/cpp/base/proto_wrapper.h"
#include "net/base/ip_endpoint.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/url_util.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/mojom/loader/referrer.mojom.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"
#include "ui/base/page_transition_types.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "url/gurl.h"
#include "url/origin.h"
#if BUILDFLAG(IS_ANDROID)
#include "ui/android/view_android.h"
#endif
using content::BrowserThread;
using content::WebContents;
namespace safe_browsing {
namespace {
// How long to wait to run the user report callback.
const int kUserReportCallbackTimer = 30;
bool HasDebugFeatureDirectory() {
return base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kCsdDebugFeatureDirectoryFlag);
}
bool ShouldSkipCSDAllowlist() {
return base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kSkipCSDAllowlistOnPreclassification);
}
safe_browsing::mojom::ClientSideDetectionType GetClientSideDetectionMojomType(
ClientSideDetectionType client_side_detection_type) {
switch (client_side_detection_type) {
case safe_browsing::ClientSideDetectionType::FORCE_REQUEST:
return safe_browsing::mojom::ClientSideDetectionType::kForceRequest;
case safe_browsing::ClientSideDetectionType::NOTIFICATION_PERMISSION_PROMPT:
return safe_browsing::mojom::ClientSideDetectionType::
kNotificationPermissionPrompt;
case safe_browsing::ClientSideDetectionType::TRIGGER_MODELS:
return safe_browsing::mojom::ClientSideDetectionType::kTriggerModels;
case safe_browsing::ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED:
return safe_browsing::mojom::ClientSideDetectionType::kKeyboardLock;
case safe_browsing::ClientSideDetectionType::POINTER_LOCK_REQUESTED:
return safe_browsing::mojom::ClientSideDetectionType::kPointerLock;
case safe_browsing::ClientSideDetectionType::VIBRATION_API:
return safe_browsing::mojom::ClientSideDetectionType::kVibrationApi;
case safe_browsing::ClientSideDetectionType::FULLSCREEN_API:
return safe_browsing::mojom::ClientSideDetectionType::kFullscreen;
case safe_browsing::ClientSideDetectionType::CLIPBOARD_COPY_API:
return safe_browsing::mojom::ClientSideDetectionType::kClipboardCopyApi;
case safe_browsing::ClientSideDetectionType::CREDIT_CARD_FORM:
return safe_browsing::mojom::ClientSideDetectionType::kCreditCardForm;
case safe_browsing::ClientSideDetectionType::IMAGE_EMBEDDING_MATCH:
return safe_browsing::mojom::ClientSideDetectionType::
kImageEmbeddingMatch;
case safe_browsing::ClientSideDetectionType::UNFAMILIAR_LOGIN_PAGE:
return safe_browsing::mojom::ClientSideDetectionType::
kUnfamiliarLoginPage;
case safe_browsing::ClientSideDetectionType::
CLIENT_SIDE_DETECTION_TYPE_UNSPECIFIED:
case safe_browsing::ClientSideDetectionType::USER_REPORT:
return safe_browsing::mojom::ClientSideDetectionType::kUserReport;
default:
NOTREACHED();
}
}
PhishingDetectorResult GetPhishingDetectorResult(
mojom::PhishingDetectorResult result) {
switch (result) {
case mojom::PhishingDetectorResult::SUCCESS:
return PhishingDetectorResult::CLASSIFICATION_SUCCESS;
case mojom::PhishingDetectorResult::CLASSIFIER_NOT_READY:
return PhishingDetectorResult::CLASSIFIER_NOT_READY;
case mojom::PhishingDetectorResult::CANCELLED:
return PhishingDetectorResult::CLASSIFICATION_CANCELLED;
case mojom::PhishingDetectorResult::FORWARD_BACK_TRANSITION:
return PhishingDetectorResult::FORWARD_BACK_TRANSITION;
case mojom::PhishingDetectorResult::INVALID_SCORE:
return PhishingDetectorResult::INVALID_SCORE;
case mojom::PhishingDetectorResult::INVALID_URL_FORMAT_REQUEST:
return PhishingDetectorResult::INVALID_URL_FORMAT_REQUEST;
case mojom::PhishingDetectorResult::INVALID_DOCUMENT_LOADER:
return PhishingDetectorResult::INVALID_DOCUMENT_LOADER;
case mojom::PhishingDetectorResult::URL_FEATURE_EXTRACTION_FAILED:
return PhishingDetectorResult::URL_FEATURE_EXTRACTION_FAILED;
case mojom::PhishingDetectorResult::DOM_EXTRACTION_FAILED:
return PhishingDetectorResult::DOM_EXTRACTION_FAILED;
case mojom::PhishingDetectorResult::TERM_EXTRACTION_FAILED:
return PhishingDetectorResult::TERM_EXTRACTION_FAILED;
case mojom::PhishingDetectorResult::VISUAL_EXTRACTION_FAILED:
return PhishingDetectorResult::VISUAL_EXTRACTION_FAILED;
case mojom::PhishingDetectorResult::CLASSIFICATION_SKIPPED:
return PhishingDetectorResult::CLASSIFICATION_SKIPPED;
}
}
ClientSideDetectionHostBase::ImageEmbeddingResult ToImageEmbeddingResult(
mojom::PhishingImageEmbeddingResult result) {
switch (result) {
case mojom::PhishingImageEmbeddingResult::kSuccess:
return ClientSideDetectionHostBase::ImageEmbeddingResult::kSuccess;
case mojom::PhishingImageEmbeddingResult::kImageEmbedderNotReady:
return ClientSideDetectionHostBase::ImageEmbeddingResult::
kImageEmbedderNotReady;
case mojom::PhishingImageEmbeddingResult::kCancelled:
return ClientSideDetectionHostBase::ImageEmbeddingResult::kCancelled;
case mojom::PhishingImageEmbeddingResult::kForwardBackTransition:
return ClientSideDetectionHostBase::ImageEmbeddingResult::
kForwardBackTransition;
case mojom::PhishingImageEmbeddingResult::kFailed:
return ClientSideDetectionHostBase::ImageEmbeddingResult::kFailed;
case mojom::PhishingImageEmbeddingResult::kInvalidURLFormatRequest:
return ClientSideDetectionHostBase::ImageEmbeddingResult::
kInvalidURLFormatRequest;
case mojom::PhishingImageEmbeddingResult::kInvalidDocumentLoader:
return ClientSideDetectionHostBase::ImageEmbeddingResult::
kInvalidDocumentLoader;
}
}
} // namespace
typedef base::OnceCallback<void(bool, bool, std::optional<bool>, bool)>
ShouldClassifyUrlCallback;
// This class is instantiated each time a new toplevel URL loads, and
// asynchronously checks whether the phishing classifier should run
// for this URL. If so, it notifies the host class by calling the provided
// callback from the UI thread. Objects of this class will be destroyed once
// nobody uses it anymore. If |web_contents|, |csd_service| or |host| go away
// you need to call Cancel(). We keep the |database_manager| alive in a ref
// pointer for as long as it takes.
class ClientSideDetectionHost::ShouldClassifyUrlRequest {
public:
ShouldClassifyUrlRequest(
const GURL& url,
const network::mojom::URLResponseHead* response_head,
ShouldClassifyUrlCallback start_phishing_classification,
WebContents* web_contents,
base::WeakPtr<ClientSideDetectionServiceBase> csd_service,
SafeBrowsingDatabaseManager* database_manager,
ClientSideDetectionType phishing_detection_request_type,
base::WeakPtr<ClientSideDetectionHost> host)
: url_(url),
web_contents_(web_contents),
csd_service_(csd_service),
database_manager_(database_manager),
phishing_detection_request_type_(phishing_detection_request_type),
host_(host),
start_phishing_classification_cb_(
std::move(start_phishing_classification)) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(web_contents_);
DCHECK(csd_service_);
DCHECK(database_manager_.get());
DCHECK(host_);
if (response_head) {
if (response_head->headers) {
response_head->headers->GetMimeType(&mime_type_);
}
remote_endpoint_ = response_head->remote_endpoint;
}
}
ShouldClassifyUrlRequest(const ShouldClassifyUrlRequest&) = delete;
ShouldClassifyUrlRequest& operator=(const ShouldClassifyUrlRequest&) = delete;
// The destructor can be called either from the UI or the IO thread.
~ShouldClassifyUrlRequest() = default;
void Start() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// We start by doing some simple checks that can run on the UI thread.
if (url_.SchemeIs(content::kChromeUIScheme)) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_CHROME_UI_PAGE);
}
if (base::FeatureList::IsEnabled(
kClientSideDetectionLocalResourceCheckFix)) {
// safe_browsing::CanGetReputationOfUrl() is another option to be
// comprehensive, but since IsPrivateIPAddress and SchemeIsHTTPOrHTTPS
// are checked below, using net::IsLocalhost() is sufficient.
// TODO: Consider safe_browsing::CanGetReputationOfUrl() in the future to
// have a consolidated preclassification check result.
if (url_.SchemeIsFile() || net::IsLocalhost(url_)) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_LOCAL_RESOURCE);
}
} else {
if (csd_service_ && !remote_endpoint_.address().IsValid()) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_LOCAL_RESOURCE);
}
}
bool is_mime_type_unsupported =
mime_type_ != "text/html" && mime_type_ != "application/xhtml+xml";
content::RenderFrameHost* rfh = web_contents_->GetPrimaryMainFrame();
bool is_error_page = rfh && rfh->IsErrorDocument();
if (!is_mime_type_unsupported) {
base::UmaHistogramBoolean(
"SBClientPhishing.IsErrorDocumentOnSupportedMimeType", is_error_page);
}
if (base::FeatureList::IsEnabled(kClientSideDetectionSkipErrorPage) &&
is_error_page) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_ERROR_DOCUMENT);
}
if (is_mime_type_unsupported) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_UNSUPPORTED_MIME_TYPE);
}
if (csd_service_ &&
csd_service_->IsPrivateIPAddress(remote_endpoint_.address())) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_PRIVATE_IP);
}
// For phishing we only classify HTTP or HTTPS pages.
if (!url_.SchemeIsHTTPOrHTTPS()) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_SCHEME_NOT_SUPPORTED);
}
// Don't run any classifier if the tab is incognito.
if (web_contents_->GetBrowserContext()->IsOffTheRecord()) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_OFF_THE_RECORD);
}
// Don't start classification if |url_| is allowlisted by enterprise policy.
if (host_ && host_->GetPrefs() &&
IsURLAllowlistedByPolicy(url_, *host_->GetPrefs())) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_ALLOWLISTED_BY_POLICY);
}
// If the tab has a delayed warning, ignore this second verdict. We don't
// want to immediately undelay a page that's already blocked as phishy.
if (host_ && host_->delegate_->HasSafeBrowsingUserInteractionObserver()) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_HAS_DELAYED_WARNING);
}
// We lookup the csd-allowlist before we lookup the cache because
// a URL may have recently been allowlisted. If the URL matches
// the csd-allowlist we won't start phishing classification.
if (ShouldClassifyForPhishing()) {
CheckSafeBrowsingDatabase(url_);
}
}
void Cancel(ClientSideDetectionType request_type) {
// We should only log if the callback has not been answered yet.
if (ShouldClassifyForPhishing()) {
base::UmaHistogramExactLinear(
"SBClientPhishing.PreClassificationCheckCancelActor", request_type,
ClientSideDetectionType_MAX + 1);
base::UmaHistogramExactLinear(
base::StrCat({"SBClientPhishing.PreClassificationCheckCancelActor.",
GetRequestTypeName(phishing_detection_request_type_)}),
request_type, ClientSideDetectionType_MAX + 1);
}
Cancel();
}
void Cancel() {
DontClassifyForPhishing(PreClassificationCheckResult::NO_CLASSIFY_CANCEL);
// Just to make sure we don't do anything bad we reset all these
// pointers except for the safebrowsing service class which may be
// accessed by CheckSafeBrowsingDatabase().
web_contents_ = nullptr;
csd_service_ = nullptr;
host_ = nullptr;
}
bool ShouldClassifyForPhishing() const {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
return !start_phishing_classification_cb_.is_null();
}
private:
friend class base::RefCountedThreadSafe<
ClientSideDetectionHost::ShouldClassifyUrlRequest>;
// This enum is used to track the result of the allowlists we use before we
// decide to classify. Currently, only the CSD match can halt classification
// from going forward. These values are persisted to logs. Entries should not
// be renumbered and numeric values should never be reused.
enum class ClientSideAllowlistMatchResult {
kNoMatch = 0,
kCsdMatch = 1,
kHighConfidenceMatch = 2,
kCsdAndHighConfidenceMatch = 3,
kMaxValue = kCsdAndHighConfidenceMatch
};
void DontClassifyForPhishing(PreClassificationCheckResult reason) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (ShouldClassifyForPhishing()) {
// Track the first reason why we stopped classifying for phishing.
ClientSideDetectionHostBase::
RecordPreClassificationCheckResultWithAndWithoutSuffix(
reason, phishing_detection_request_type_);
if (host_ && host_->IsEnhancedProtectionEnabled() &&
// Cancelation happens when the WebContents is destroyed, but we
// cannot access ClientSideDetectionFeatureCache at that time.
!web_contents_->IsBeingDestroyed()) {
ClientSideDetectionFeatureCache::CreateForWebContents(web_contents_);
ClientSideDetectionFeatureCache* feature_cache_map =
ClientSideDetectionFeatureCache::FromWebContents(web_contents_);
// TODO(andysjlim): Investigate why this is null sometimes.
LoginReputationClientRequest::DebuggingMetadata* debugging_metadata =
feature_cache_map->GetOrCreateDebuggingMetadataForURL(url_);
if (debugging_metadata) {
debugging_metadata->set_preclassification_check_result(reason);
}
}
std::move(start_phishing_classification_cb_)
.Run(false, send_sample_ping_, std::nullopt,
!remote_endpoint_.address().IsValid());
}
start_phishing_classification_cb_.Reset();
}
void CheckSafeBrowsingDatabase(const GURL& url) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
PreClassificationCheckResult phishing_reason =
PreClassificationCheckResult::NO_CLASSIFY_MAX;
// When doing debug feature dumps, ignore the allowlist.
if (HasDebugFeatureDirectory()) {
OnAllowlistCheckDone(url, phishing_reason,
/*match_allowlist=*/false);
return;
}
if (!database_manager_.get()) {
// We cannot check the Safe Browsing allowlists so we stop here
// for safety.
OnAllowlistCheckDone(
url,
/*phishing_reason=*/
PreClassificationCheckResult::NO_CLASSIFY_NO_DATABASE_MANAGER,
/*match_allowlist=*/false);
return;
}
// If we get a suspcious verdict from RTLookupResponse, we should get a
// second opinion on CSD side, so we skip the allowlist. If we get an
// explicit request to send a report from the user, we skip the allowlist.
// We also check the command line flag if the allowlist should be skipped.
if (phishing_detection_request_type_ ==
safe_browsing::ClientSideDetectionType::FORCE_REQUEST ||
phishing_detection_request_type_ ==
safe_browsing::ClientSideDetectionType::USER_REPORT ||
ShouldSkipCSDAllowlist()) {
OnAllowlistCheckDone(url, phishing_reason,
/*match_allowlist=*/false);
return;
}
// Query the CSD Allowlist asynchronously. We're already on the IO thread so
// can call AllowlistCheckerClient directly.
base::OnceCallback<void(bool)> result_callback =
base::BindOnce(&ClientSideDetectionHost::ShouldClassifyUrlRequest::
OnAllowlistCheckDone,
weak_factory_.GetWeakPtr(), url, phishing_reason);
AllowlistCheckerClient::StartCheckCsdAllowlist(database_manager_, url,
std::move(result_callback));
}
void OnAllowlistCheckDone(const GURL& url,
PreClassificationCheckResult phishing_reason,
bool match_allowlist) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// On CSD allowlist match, we still want to send a ping on a rare chance.
send_sample_ping_ = CanSendSamplePing();
if (match_allowlist && !send_sample_ping_) {
phishing_reason =
PreClassificationCheckResult::NO_CLASSIFY_MATCH_CSD_ALLOWLIST;
}
if (phishing_reason !=
PreClassificationCheckResult::NO_CLASSIFY_NO_DATABASE_MANAGER) {
switch (phishing_detection_request_type_) {
case CREDIT_CARD_FORM:
case CLIPBOARD_COPY_API:
case UNFAMILIAR_LOGIN_PAGE:
base::UmaHistogramBoolean(
base::StrCat(
{"SBClientPhishing.MatchCSDAllowlistOn",
GetRequestTypeName(phishing_detection_request_type_)}),
match_allowlist);
break;
default:
break;
}
// This check is also for logging purposes although the CSD allowlist
// could be matched or not checked at all. Once it completes,
// preclassification check will continue.
database_manager_->CheckUrlForHighConfidenceAllowlist(
url,
base::BindOnce(&ClientSideDetectionHost::ShouldClassifyUrlRequest::
OnHighConfidenceAllowlistCheckDone,
weak_factory_.GetWeakPtr(), phishing_reason,
base::TimeTicks::Now()));
} else {
CheckCache(phishing_reason);
}
}
void OnHighConfidenceAllowlistCheckDone(
PreClassificationCheckResult phishing_reason,
base::TimeTicks check_start_time,
bool did_match_high_confidence_allowlist,
std::optional<SafeBrowsingDatabaseManager::
HighConfidenceAllowlistCheckLoggingDetails>
logging_details) {
did_match_high_confidence_allowlist_ = did_match_high_confidence_allowlist;
UmaHistogramMediumTimes(
"SBClientPhishing.HighConfidenceAllowlistCheckDuration",
base::TimeTicks::Now() - check_start_time);
// TODO(andysjlim): This histogram will be logged to
// PreClassificationCheckResult through |phishing_reason|, but logged
// separately now because a new field PreClassificationCheckResult results
// in a new server data to be sent through debugging metadata.
ClientSideAllowlistMatchResult match_result =
GetClientSideAllowlistMatchResult(
phishing_reason ==
PreClassificationCheckResult::NO_CLASSIFY_MATCH_CSD_ALLOWLIST,
did_match_high_confidence_allowlist);
base::UmaHistogramEnumeration(
"SBClientPhishing.MatchHighConfidenceAllowlist", match_result);
base::UmaHistogramEnumeration(
base::StrCat({"SBClientPhishing.MatchHighConfidenceAllowlist.",
GetRequestTypeName(phishing_detection_request_type_)}),
match_result);
if (phishing_reason == NO_CLASSIFY_MAX && ShouldAcceptHCAllowlist()) {
phishing_reason =
PreClassificationCheckResult::NO_CLASSIFY_MATCH_HC_ALLOWLIST;
}
CheckCache(phishing_reason);
}
void CheckCache(PreClassificationCheckResult phishing_reason) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (phishing_reason != PreClassificationCheckResult::NO_CLASSIFY_MAX) {
DontClassifyForPhishing(phishing_reason);
}
if (!ShouldClassifyForPhishing()) {
return; // No point in doing anything else.
}
if (ShouldStopAtPreClassification()) {
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_ALLOWLIST_METRIC);
}
// For trigger model requests, if result is cached, we don't want to run
// classification again. In that case we're just trying to show the warning.
// If we're dumping features for debugging, ignore the cache.
bool is_phishing;
if (phishing_detection_request_type_ ==
ClientSideDetectionType::TRIGGER_MODELS &&
!HasDebugFeatureDirectory() && host_ && csd_service_ &&
csd_service_->GetValidCachedResult(url_, &is_phishing)) {
// Since we are already on the UI thread, this is safe.
host_->MaybeShowPhishingWarning(
/*is_from_cache=*/true, ClientSideDetectionType::TRIGGER_MODELS,
did_match_high_confidence_allowlist_, url_, is_phishing,
/*response_code=*/std::nullopt,
/*intelligent_scan_verdict=*/std::nullopt);
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_RESULT_FROM_CACHE);
}
// We want to limit the number of requests, but if we're dumping features
// for debugging or processing an explicit request for a report from a user,
// allow us to exceed the report limit.
if (!HasDebugFeatureDirectory() && csd_service_ &&
phishing_detection_request_type_ !=
ClientSideDetectionType::USER_REPORT &&
csd_service_->AtPhishingReportLimit()) {
base::UmaHistogramExactLinear("SBClientPhishing.RequestTypeAtReportLimit",
phishing_detection_request_type_,
ClientSideDetectionType_MAX + 1);
DontClassifyForPhishing(
PreClassificationCheckResult::NO_CLASSIFY_TOO_MANY_REPORTS);
}
// Everything checks out, so start classification.
// |web_contents_| is safe to call as we will be destructed
// before it is.
if (ShouldClassifyForPhishing()) {
ClientSideDetectionHostBase::
RecordPreClassificationCheckResultWithAndWithoutSuffix(
PreClassificationCheckResult::CLASSIFY,
phishing_detection_request_type_);
if (host_ && host_->IsEnhancedProtectionEnabled()) {
ClientSideDetectionFeatureCache::CreateForWebContents(web_contents_);
ClientSideDetectionFeatureCache* feature_cache_map =
ClientSideDetectionFeatureCache::FromWebContents(web_contents_);
feature_cache_map->GetOrCreateDebuggingMetadataForURL(url_)
->set_preclassification_check_result(
PreClassificationCheckResult::CLASSIFY);
}
std::move(start_phishing_classification_cb_)
.Run(true, send_sample_ping_, did_match_high_confidence_allowlist_,
!remote_endpoint_.address().IsValid());
// Reset the callback to make sure ShouldClassifyForPhishing()
// returns false.
start_phishing_classification_cb_.Reset();
}
}
bool ShouldStopAtPreClassification() {
switch (phishing_detection_request_type_) {
case CLIPBOARD_COPY_API:
return base::RandDouble() >= kCsdClipboardCopyApiSampleRate.Get();
case CREDIT_CARD_FORM:
return base::RandDouble() >= kCsdCreditCardFormSampleRate.Get();
case UNFAMILIAR_LOGIN_PAGE:
return base::RandDouble() >=
kCsdProactivePasswordProtectionSampleRate.Get();
default:
break;
}
return false;
}
bool CanSendSamplePing() {
return host_ && host_->CanSendSamplePing(phishing_detection_request_type_);
}
bool ShouldAcceptHCAllowlist() {
return host_ && host_->ShouldAcceptHCAllowlist(
phishing_detection_request_type_,
did_match_high_confidence_allowlist_.value_or(false));
}
ClientSideAllowlistMatchResult GetClientSideAllowlistMatchResult(
bool match_csd_allowlist,
bool match_hc_allowlist) {
if (match_csd_allowlist && match_hc_allowlist) {
return ClientSideAllowlistMatchResult::kCsdAndHighConfidenceMatch;
} else if (match_csd_allowlist) {
return ClientSideAllowlistMatchResult::kCsdMatch;
} else if (match_hc_allowlist) {
return ClientSideAllowlistMatchResult::kHighConfidenceMatch;
} else {
return ClientSideAllowlistMatchResult::kNoMatch;
}
}
const GURL url_;
bool send_sample_ping_ = false;
std::optional<bool> did_match_high_confidence_allowlist_;
std::string mime_type_;
net::IPEndPoint remote_endpoint_;
raw_ptr<WebContents> web_contents_;
base::WeakPtr<ClientSideDetectionServiceBase> csd_service_;
// We keep a ref pointer here just to make sure the safe browsing
// database manager stays alive long enough.
scoped_refptr<SafeBrowsingDatabaseManager> database_manager_;
ClientSideDetectionType phishing_detection_request_type_;
base::WeakPtr<ClientSideDetectionHost> host_;
ShouldClassifyUrlCallback start_phishing_classification_cb_;
base::WeakPtrFactory<ShouldClassifyUrlRequest> weak_factory_{this};
};
// static
const int ClientSideDetectionHost::kMaxHighResScreenshotWidth = 4096;
const int ClientSideDetectionHost::kMaxHighResScreenshotHeight = 2160;
// static
std::unique_ptr<ClientSideDetectionHost> ClientSideDetectionHost::Create(
content::WebContents* tab,
std::unique_ptr<Delegate> delegate,
IntelligentScanDelegate* intelligent_scan_delegate,
PrefService* pref_service,
VerdictCacheManager* cache_manager,
history::HistoryService* history_service,
base::WeakPtr<ClientSideDetectionService> csd_service,
std::unique_ptr<SafeBrowsingTokenFetcher> token_fetcher,
bool is_off_the_record,
const PrimaryAccountSignedIn& account_signed_in_callback) {
return base::WrapUnique(new ClientSideDetectionHost(
tab, std::move(delegate), intelligent_scan_delegate, pref_service,
cache_manager, history_service, csd_service, std::move(token_fetcher),
is_off_the_record, account_signed_in_callback));
}
ClientSideDetectionHost::ClientSideDetectionHost(
WebContents* tab,
std::unique_ptr<Delegate> delegate,
IntelligentScanDelegate* intelligent_scan_delegate,
PrefService* pref_service,
VerdictCacheManager* cache_manager,
history::HistoryService* history_service,
base::WeakPtr<ClientSideDetectionService> csd_service,
std::unique_ptr<SafeBrowsingTokenFetcher> token_fetcher,
bool is_off_the_record,
const PrimaryAccountSignedIn& account_signed_in_callback)
: ClientSideDetectionHostBase(csd_service,
cache_manager,
intelligent_scan_delegate,
pref_service,
std::move(token_fetcher),
history_service,
is_off_the_record),
content::WebContentsObserver(tab),
tab_(tab),
classification_request_(nullptr),
delegate_(std::move(delegate)),
account_signed_in_callback_(account_signed_in_callback) {
DCHECK(tab);
DCHECK(pref_service);
// Note: the CSD service will be nullptr here in testing.
if (auto service = GetClientSideDetectionService()) {
ClientSideDetectionFeatureCache::CreateForWebContents(web_contents());
ClientSideDetectionFeatureCache::FromWebContents(web_contents())
->AddClearCacheSubscription(service);
}
// |ui_manager_| and |database_manager_| can
// be null if safe browsing service is not available in the embedder.
ui_manager_ = delegate_->GetSafeBrowsingUIManager();
database_manager_ = delegate_->GetSafeBrowsingDBManager();
RegisterPermissionRequestManager();
RegisterAsyncCheckTracker();
RegisterAutofillManager();
}
ClientSideDetectionHost::~ClientSideDetectionHost() {
MaybeRunUserReportCallback();
if (classification_request_.get()) {
classification_request_->Cancel();
}
CancelPendingRequests();
}
void ClientSideDetectionHost::CancelPendingRequests() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
weak_factory_.InvalidateWeakPtrs();
ClientSideDetectionHostBase::CancelPendingRequests();
}
void ClientSideDetectionHost::GetInnerText(HostInnerTextCallback callback) {
delegate_->GetInnerText(std::move(callback));
}
bool ClientSideDetectionHost::IsAccountSignedIn() {
return !account_signed_in_callback_.is_null() &&
account_signed_in_callback_.Run();
}
bool ClientSideDetectionHost::IsErrorDocument() {
return web_contents()->GetPrimaryMainFrame()->IsErrorDocument();
}
ChromeUserPopulation ClientSideDetectionHost::GetUserPopulation() {
return delegate_->GetUserPopulation();
}
safe_browsing::credit_card_form::ReferringApp
ClientSideDetectionHost::GetReferringApp() const {
#if BUILDFLAG(IS_ANDROID)
return safe_browsing::credit_card_form::FromReferringAppInfo(
delegate_->GetReferringAppInfo(web_contents()));
#else
return safe_browsing::credit_card_form::kNoReferringApp;
#endif
}
void ClientSideDetectionHost::RegisterPermissionRequestManager() {
if (IsEnhancedProtectionEnabled()) {
permission_request_observation_.Observe(
permissions::PermissionRequestManager::FromWebContents(web_contents()));
}
}
void ClientSideDetectionHost::RegisterAsyncCheckTracker() {
if (IsEnhancedProtectionEnabled()) {
AsyncCheckTracker* tracker =
AsyncCheckTracker::FromWebContents(web_contents());
CHECK(tracker);
async_check_observation_.Observe(tracker);
}
}
void ClientSideDetectionHost::RegisterAutofillManager() {
if (!IsEnhancedProtectionEnabled()) {
return;
}
autofill_managers_observation_.Observe(
autofill::ContentAutofillClient::FromWebContents(web_contents()),
autofill::ScopedAutofillManagersObservation::InitializationPolicy::
kObservePreexistingManagers);
}
void ClientSideDetectionHost::MaybeRunUserReportCallback() {
user_report_timeout_timer_.Stop();
if (user_report_callback_) {
std::move(user_report_callback_).Run();
}
}
void ClientSideDetectionHost::ReportUnsafeSite(SkBitmap screenshot,
base::OnceClosure callback) {
if (!screenshot.drawsNothing() &&
screenshot.width() <= kMaxHighResScreenshotWidth &&
screenshot.height() <= kMaxHighResScreenshotHeight) {
screenshot_ = screenshot;
}
MaybeRunUserReportCallback();
user_report_callback_ = std::move(callback);
// Start a 30-second timer that will run the callback if it hasn't been run
// yet.
user_report_timeout_timer_.Start(
FROM_HERE, base::Seconds(kUserReportCallbackTimer),
base::BindOnce(&ClientSideDetectionHost::MaybeRunUserReportCallback,
weak_factory_.GetWeakPtr()));
MaybeStartPreClassification(ClientSideDetectionType::USER_REPORT);
}
void ClientSideDetectionHost::OnUnfamiliarLoginPageDetected() {
if (base::FeatureList::IsEnabled(kProactivePasswordProtection) &&
IsEnhancedProtectionEnabled()) {
MaybeStartPreClassification(ClientSideDetectionType::UNFAMILIAR_LOGIN_PAGE);
}
}
void ClientSideDetectionHost::MaybeStartPreClassification(
ClientSideDetectionType request_type) {
if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) {
MaybeRunUserReportCallback();
return;
}
// Cancel any pending classification request.
// TODO(b/447359124): Support multiple classifications on the same page.
if (classification_request_.get()) {
// First check if there's an ongoing preclassification check.
if (classification_request_->ShouldClassifyForPhishing() &&
base::FeatureList::IsEnabled(kClientSideDetectionTierSystem)) {
if (NewRequestTypeTierHigher(request_type)) {
classification_request_->Cancel(request_type);
} else {
base::UmaHistogramExactLinear(
base::StrCat({"SBClientPhishing.BlockingRequestType.",
GetRequestTypeName(request_type)}),
last_request_type(), ClientSideDetectionType_MAX + 1);
return;
}
} else {
classification_request_->Cancel(request_type);
}
}
// If there is a renderer classification going on and the incoming request
// type is not higher, do not let that cancel pending classification.
if (is_classifying() &&
base::FeatureList::IsEnabled(kClientSideDetectionTierSystem) &&
!NewRequestTypeTierHigher(request_type)) {
base::UmaHistogramExactLinear(
base::StrCat({"SBClientPhishing.BlockingRequestType.",
GetRequestTypeName(request_type)}),
last_request_type(), ClientSideDetectionType_MAX + 1);
return;
}
if (!GetClientSideDetectionService()) {
if (request_type == ClientSideDetectionType::USER_REPORT) {
MaybeRunUserReportCallback();
}
return;
}
if (!preclassification_started_cb_for_testing_.is_null()) {
preclassification_started_cb_for_testing_.Run(request_type);
}
content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame();
set_current_url(rfh->GetLastCommittedURL());
set_last_committed_url(request_type, current_url());
current_outermost_main_frame_id_ = rfh->GetGlobalId();
set_last_request_type(request_type);
LogClientSideDetectionEvent(
ClientSideDetectionEvent::kTriggerStartsPreClassification, request_type);
// Check whether we can cassify the current URL for phishing.
classification_request_ = std::make_unique<ShouldClassifyUrlRequest>(
rfh->GetLastCommittedURL(), rfh->GetLastResponseHead(),
base::BindOnce(&ClientSideDetectionHost::OnPhishingPreClassificationDone,
weak_factory_.GetWeakPtr(), request_type),
web_contents(), GetClientSideDetectionService(), database_manager_.get(),
request_type, weak_factory_.GetWeakPtr());
classification_request_->Start();
}
void ClientSideDetectionHost::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
// `current_navigation_id_` will be set to UnsafeResource, and later in
// `BaseUIManager::DisplayBlockingPage` we will check
// `AsyncCheckTracker::IsMainPageResourceLoadPending`, and `AsyncCheckTracker`
// tracks only committed not-same-document navigations, see
// `AsyncCheckTracker::DidFinishNavigation`. So we should never set
// `current_navigation_id_` to non-comitted or same-document navigations.
if ((navigation_handle->HasCommitted() &&
!navigation_handle->IsSameDocument())) {
current_navigation_id_ = navigation_handle->GetNavigationId();
}
}
void ClientSideDetectionHost::PrimaryPageChanged(content::Page& page) {
// TODO(noelutz): move this DCHECK to WebContents and fix all the unit tests
// that don't call this method on the UI thread.
// DCHECK_CURRENTLY_ON(BrowserThread::UI);
// If we navigate away and there currently is a pending phishing report
// request we have to cancel it to make sure we don't display an
// interstitial for the wrong page. Note that this won't cancel the server
// ping back but only cancel the showing of the interstitial.
CancelPendingRequests();
if (is_csd_running()) {
base::UmaHistogramExactLinear(
"SBClientPhishing.ClientSideDetection.InterruptedByNavigation",
last_request_type(), ClientSideDetectionType_MAX + 1);
}
set_is_csd_running(false);
set_is_classifying(false);
set_last_request_type(
ClientSideDetectionType::CLIENT_SIDE_DETECTION_TYPE_UNSPECIFIED);
set_should_send_as_force_request(false);
clear_clipboard_extracted_data();
MaybeRunUserReportCallback();
if (base::FeatureList::IsEnabled(kClientSideDetectionOnlyESBClassification) &&
!IsEnhancedProtectionEnabled()) {
return;
}
if (base::FeatureList::IsEnabled(kClientSideDetectionNewObservers)) {
if (did_first_visually_non_empty_paint_ ^ on_first_contentful_paint_) {
auto value = did_first_visually_non_empty_paint_
? CSDObserverCalled::kDidFirstVisuallyNonEmptyPaint
: CSDObserverCalled::kOnFirstContentfulPaint;
base::UmaHistogramEnumeration(
"SBClientPhishing.SingleObserverCalledOnNewPage", value);
}
did_first_visually_non_empty_paint_ = false;
on_first_contentful_paint_ = false;
set_trigger_model_request_sent_as_force_request(false);
// It is possible for the async check force request to complete before this,
// and we should have the URL set in case it can match in the verdict cache
// manager.
content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame();
content::NavigationEntry* nav_entry =
web_contents()->GetController().GetLastCommittedEntry();
bool is_reload = nav_entry && ui::PageTransitionCoreTypeIs(
nav_entry->GetTransitionType(),
ui::PAGE_TRANSITION_RELOAD);
if (current_url() == rfh->GetLastCommittedURL() && !is_reload) {
base::UmaHistogramBoolean("SBClientPhishing.SameURLAtPrimaryPageChanged",
true);
}
set_current_url(rfh->GetLastCommittedURL());
return;
}
set_trigger_model_request_sent_as_force_request(false);
MaybeStartPreClassification(ClientSideDetectionType::TRIGGER_MODELS);
}
void ClientSideDetectionHost::DidFirstVisuallyNonEmptyPaint() {
if (base::FeatureList::IsEnabled(kClientSideDetectionOnlyESBClassification) &&
!IsEnhancedProtectionEnabled()) {
return;
}
if (base::FeatureList::IsEnabled(kClientSideDetectionNewObservers)) {
did_first_visually_non_empty_paint_ = true;
if (on_first_contentful_paint_) {
if (should_send_as_force_request() || HasForceRequestFromRtUrlLookup()) {
base::UmaHistogramBoolean(
"SBClientPhishing.TriggerModelsConvertedToForceRequestAtLoad",
true);
MaybeStartPreClassification(ClientSideDetectionType::FORCE_REQUEST);
} else {
MaybeStartPreClassification(ClientSideDetectionType::TRIGGER_MODELS);
}
}
}
}
void ClientSideDetectionHost::OnFirstContentfulPaintInPrimaryMainFrame(
base::TimeTicks presentation_time) {
if (base::FeatureList::IsEnabled(kClientSideDetectionOnlyESBClassification) &&
!IsEnhancedProtectionEnabled()) {
return;
}
if (base::FeatureList::IsEnabled(kClientSideDetectionNewObservers)) {
on_first_contentful_paint_ = true;
if (did_first_visually_non_empty_paint_) {
if (should_send_as_force_request() || HasForceRequestFromRtUrlLookup()) {
base::UmaHistogramBoolean(
"SBClientPhishing.TriggerModelsConvertedToForceRequestAtLoad",
true);
MaybeStartPreClassification(ClientSideDetectionType::FORCE_REQUEST);
} else {
MaybeStartPreClassification(ClientSideDetectionType::TRIGGER_MODELS);
}
}
}
}
void ClientSideDetectionHost::OnPromptAdded() {
if (!IsEnhancedProtectionEnabled()) {
return;
}
permissions::PermissionRequestManager* permission_request_manager =
permissions::PermissionRequestManager::FromWebContents(web_contents());
CHECK(permission_request_manager);
if (std::ranges::contains(permission_request_manager->Requests(),
permissions::RequestType::kNotifications,
&permissions::PermissionRequest::request_type)) {
MaybeStartPreClassification(
ClientSideDetectionType::NOTIFICATION_PERMISSION_PROMPT);
}
}
void ClientSideDetectionHost::OnPermissionRequestManagerDestructed() {
permission_request_observation_.Reset();
}
void ClientSideDetectionHost::OnAsyncSafeBrowsingCheckCompleted() {
ClientSideDetectionHostBase::OnAsyncSafeBrowsingCheckCompleted();
}
void ClientSideDetectionHost::OnAsyncSafeBrowsingCheckTrackerDestructed() {
async_check_observation_.Reset();
}
void ClientSideDetectionHost::MaybeFillScreenshotData(
ClientPhishingRequest* request) {
if (request->client_side_detection_type() !=
ClientSideDetectionType::USER_REPORT) {
return;
}
if (screenshot_) {
visual_utils::EncodeScreenshot(
*screenshot_,
request->mutable_visual_features()->mutable_high_res_screenshot());
}
screenshot_ = std::nullopt;
}
void ClientSideDetectionHost::KeyboardLockRequested() {
if (!IsEnhancedProtectionEnabled()) {
return;
}
if (!HasDonePreclassificationCheckOnSameURL(
ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED)) {
MaybeStartPreClassification(
ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED);
}
}
void ClientSideDetectionHost::VibrationRequested() {
if (!IsEnhancedProtectionEnabled()) {
return;
}
// Vibration API can be triggered on a page in intervals between 0 and 1
// seconds. Because of this, we want to only classify once per given URL since
// a page can send a request multiple vibration at a time.
if (!HasDonePreclassificationCheckOnSameURL(
ClientSideDetectionType::VIBRATION_API)) {
MaybeStartPreClassification(ClientSideDetectionType::VIBRATION_API);
}
}
void ClientSideDetectionHost::OnTextCopiedToClipboard(
content::RenderFrameHost* render_frame_host,
const std::u16string& copied_text) {
ClientSideDetectionHostBase::OnTextCopiedToClipboard(copied_text);
}
void ClientSideDetectionHost::OnPhishingPreClassificationDone(
ClientSideDetectionType request_type,
bool should_classify,
bool is_sample_ping,
std::optional<bool> did_match_high_confidence_allowlist,
bool is_invalid_ip) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
LogClientSideDetectionEvent(
ClientSideDetectionEvent::kPreClassificationCheckComplete, request_type);
if (!preclassification_done_cb_for_testing_.is_null()) {
preclassification_done_cb_for_testing_.Run(request_type);
}
if (!should_classify) {
if (request_type == ClientSideDetectionType::USER_REPORT) {
MaybeRunUserReportCallback();
}
return;
}
set_is_csd_running(true);
bool intelligent_scan_ongoing = GetIntelligentScanId().has_value();
base::UmaHistogramBoolean(
"SBClientPhishing.IntelligentScanOngoingOnNewPreclassification",
intelligent_scan_ongoing);
base::UmaHistogramBoolean(
base::StrCat(
{"SBClientPhishing.IntelligentScanOngoingOnNewPreclassification.",
GetRequestTypeName(request_type)}),
intelligent_scan_ongoing);
if (intelligent_scan_ongoing) {
DCHECK(GetIntelligentScanDelegate());
GetIntelligentScanDelegate()->CancelIntelligentScan(
*GetIntelligentScanId());
}
content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame();
phishing_detector_.reset();
rfh->GetRemoteAssociatedInterfaces()->GetInterface(&phishing_detector_);
if (!phishing_detector_.is_bound()) {
if (request_type == ClientSideDetectionType::USER_REPORT) {
MaybeRunUserReportCallback();
}
return;
}
// For TRIGGER_MODELS and IMAGE_EMBEDDING_MATCH only, perform phishing
// classification as the next step.
// For all other triggers, if enabled, skip phishing detection.
if (IsEnhancedProtectionEnabled() &&
request_type == ClientSideDetectionType::TRIGGER_MODELS &&
// Only ESB users should be in the study.
base::FeatureList::IsEnabled(kClientSideDetectionImageEmbeddingMatch)) {
LogClientSideDetectionEvent(
ClientSideDetectionEvent::kImageClassificationBegin, request_type);
set_is_classifying(true);
phishing_detector_->StartPhishingDetection(
current_url(),
GetClientSideDetectionMojomType(
ClientSideDetectionType::IMAGE_EMBEDDING_MATCH),
base::BindOnce(&ClientSideDetectionHost::PhishingDetectionDone,
weak_factory_.GetWeakPtr(),
ClientSideDetectionType::IMAGE_EMBEDDING_MATCH,
is_sample_ping, did_match_high_confidence_allowlist,
is_invalid_ip, tick_clock()->NowTicks()));
return;
}
if (request_type != ClientSideDetectionType::TRIGGER_MODELS &&
base::FeatureList::IsEnabled(
kSkipImageClassificationScoringForNonPageLoadTriggers)) {
ClientPhishingRequest verdict;
verdict.set_url(current_url().spec());
verdict.set_client_score(0.0);
PhishingDetectionDone(request_type, is_sample_ping,
did_match_high_confidence_allowlist, is_invalid_ip,
tick_clock()->NowTicks(),
mojom::PhishingDetectorResult::CLASSIFICATION_SKIPPED,
mojo_base::ProtoWrapper(verdict));
return;
}
LogClientSideDetectionEvent(
ClientSideDetectionEvent::kImageClassificationBegin, request_type);
set_is_classifying(true);
phishing_detector_->StartPhishingDetection(
current_url(), GetClientSideDetectionMojomType(request_type),
base::BindOnce(&ClientSideDetectionHost::PhishingDetectionDone,
weak_factory_.GetWeakPtr(), request_type, is_sample_ping,
did_match_high_confidence_allowlist, is_invalid_ip,
tick_clock()->NowTicks()));
}
void ClientSideDetectionHost::PhishingDetectionDone(
ClientSideDetectionType request_type,
bool is_sample_ping,
std::optional<bool> did_match_high_confidence_allowlist,
bool is_invalid_ip,
base::TimeTicks start_time,
mojom::PhishingDetectorResult result,
std::optional<mojo_base::ProtoWrapper> wrapped_verdict) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// There is something seriously wrong if there is no service class but
// this method is called. The renderer should not start phishing detection
// if there isn't any service class in the browser.
DCHECK(GetClientSideDetectionService());
phishing_detector_.reset();
std::optional<ClientPhishingRequest> verdict;
// There's no need to parse the wrapped verdict if the result is not SUCCESS
// or CLASSIFICATION_SKIPPED.
if (wrapped_verdict.has_value() &&
(result == mojom::PhishingDetectorResult::SUCCESS ||
result == mojom::PhishingDetectorResult::CLASSIFICATION_SKIPPED)) {
verdict = wrapped_verdict->As<ClientPhishingRequest>();
}
ClientSideDetectionHostBase::PhishingDetectionDone(
request_type, is_sample_ping, did_match_high_confidence_allowlist,
is_invalid_ip, start_time, GetPhishingDetectorResult(result),
std::move(verdict));
}
visual_utils::CanExtractVisualFeaturesResult
ClientSideDetectionHost::DetermineVisualFeaturesExtraction() {
int viewport_width = -1;
int viewport_height = -1;
visual_utils::CanExtractVisualFeaturesResult
can_extract_visual_features_result;
content::RenderWidgetHostView* view =
web_contents()->GetRenderWidgetHostView();
#if BUILDFLAG(IS_ANDROID)
gfx::Size size;
// native view can be null in tests.
if (view && view->GetNativeView()) {
gfx::SizeF viewport = view->GetNativeView()->viewport_size();
viewport_width = static_cast<int>(viewport.width());
viewport_height = static_cast<int>(viewport.height());
size = gfx::Size(viewport_width, viewport_height);
}
can_extract_visual_features_result = visual_utils::CanExtractVisualFeatures(
IsEnhancedProtectionEnabled(),
web_contents()->GetBrowserContext()->IsOffTheRecord(), size);
#else
gfx::Size size;
if (view) {
size = view->GetVisibleViewportSize();
viewport_width = size.width();
viewport_height = size.height();
}
can_extract_visual_features_result = visual_utils::CanExtractVisualFeatures(
IsEnhancedProtectionEnabled(),
web_contents()->GetBrowserContext()->IsOffTheRecord(), size,
zoom::ZoomController::GetZoomLevelForWebContents(web_contents()));
#endif
base::UmaHistogramSparse("SBClientPhishing.Viewport.Width", viewport_width);
base::UmaHistogramSparse("SBClientPhishing.Viewport.Height", viewport_height);
float ppi = 0;
if (view && view->GetNativeView() && display::Screen::Get()) {
ppi = display::Screen::Get()
->GetDisplayNearestView(view->GetNativeView())
.GetPixelsPerInchX();
}
base::UmaHistogramSparse("SBClientPhishing.Viewport.PixelsPerInch",
static_cast<int>(ppi));
if (viewport_width <= 0xFFFF && viewport_width >= 0 &&
viewport_height <= 0xFFFF && viewport_height >= 0) {
int32_t encoded_resolution = (viewport_width << 16) | viewport_height;
base::UmaHistogramSparse("SBClientPhishing.Viewport.EncodedResolution",
encoded_resolution);
}
base::UmaHistogramEnumeration("SBClientPhishing.VisualFeaturesClearReason2",
can_extract_visual_features_result);
return can_extract_visual_features_result;
}
void ClientSideDetectionHost::PhishingImageEmbeddingDone(
std::unique_ptr<ClientPhishingRequest> verdict,
std::optional<bool> did_match_high_confidence_allowlist,
bool is_invalid_ip,
mojom::PhishingImageEmbeddingResult result,
std::optional<mojo_base::ProtoWrapper> image_feature_embedding_wrapper,
std::optional<mojo_base::ProtoWrapper> visual_features_wrapper) {
std::optional<ImageFeatureEmbedding> image_feature_embedding;
if (image_feature_embedding_wrapper.has_value()) {
image_feature_embedding =
image_feature_embedding_wrapper->As<ImageFeatureEmbedding>();
}
std::optional<VisualFeatures> visual_features;
if (visual_features_wrapper.has_value()) {
visual_features = visual_features_wrapper->As<VisualFeatures>();
}
ClientSideDetectionHostBase::PhishingImageEmbeddingDone(
std::move(verdict), did_match_high_confidence_allowlist, is_invalid_ip,
ToImageEmbeddingResult(result), std::move(image_feature_embedding),
std::move(visual_features));
}
void ClientSideDetectionHost::ShowBlockingPage(
GURL phishing_url,
ClientSideDetectionType request_type,
std::optional<IntelligentScanVerdict> intelligent_scan_verdict,
bool should_show_scam_warning) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (ui_manager_.get()) {
auto* primary_main_frame = web_contents()->GetPrimaryMainFrame();
const content::GlobalRenderFrameHostId primary_main_frame_id =
primary_main_frame->GetGlobalId();
security_interstitials::UnsafeResource resource;
resource.url = phishing_url;
resource.original_url = phishing_url;
resource.threat_type =
SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING;
resource.threat_source = safe_browsing::ThreatSource::CLIENT_SIDE_DETECTION;
resource.navigation_id = current_navigation_id_;
// When we present a scam warning, we want to add separate interstitial
// metrics to track specifics.
if (should_show_scam_warning) {
resource.threat_subtype = GetThreatSubtype(*intelligent_scan_verdict);
DCHECK(GetIntelligentScanDelegate());
GetIntelligentScanDelegate()->OnScamWarningShown();
}
resource.rfh_locator = security_interstitials::UnsafeResourceLocator::
CreateForRenderFrameToken(primary_main_frame_id.child_id.value(),
primary_main_frame->GetFrameToken().value());
if (!ui_manager_->IsAllowlisted(
resource.url, resource.rfh_locator, resource.navigation_id,
resource.threat_type, resource.threat_source)) {
// We need to stop any pending navigations, otherwise the interstitial
// might not get created properly.
web_contents()->GetController().DiscardNonCommittedEntries();
}
LogClientSideDetectionEvent(ClientSideDetectionEvent::kWarningShown,
request_type);
ui_manager_->DisplayBlockingPage(resource);
}
}
void ClientSideDetectionHost::UpdateDebuggingMetadataWithNetworkResult(
GURL phishing_url,
net::HttpStatusCode response_code) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
ClientSideDetectionFeatureCache::CreateForWebContents(web_contents());
ClientSideDetectionFeatureCache* feature_cache_map =
ClientSideDetectionFeatureCache::FromWebContents(web_contents());
feature_cache_map->GetOrCreateDebuggingMetadataForURL(phishing_url)
->set_network_result(response_code);
}
void ClientSideDetectionHost::set_ui_manager(BaseUIManager* ui_manager) {
ui_manager_ = ui_manager;
}
void ClientSideDetectionHost::set_database_manager(
SafeBrowsingDatabaseManager* database_manager) {
database_manager_ = database_manager;
}
void ClientSideDetectionHost::AddMiscellaneousMetadataToClientPhishingRequest(
ClientPhishingRequest* verdict,
bool is_invalid_ip) {
content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame();
// Check the frame id as a precaution against unexpected race conditions.
if (rfh && rfh->GetGlobalId() == current_outermost_main_frame_id_) {
const network::mojom::URLResponseHead* response_head =
rfh->GetLastResponseHead();
if (response_head && response_head->headers) {
verdict->set_http_response_code(response_head->headers->response_code());
}
}
ClientSideDetectionHostBase::AddMiscellaneousMetadataToClientPhishingRequest(
verdict, is_invalid_ip);
}
void ClientSideDetectionHost::AddReferrerChain(ClientPhishingRequest* verdict) {
if (IsEnhancedProtectionEnabled()) {
delegate_->AddReferrerChain(verdict, current_url(),
current_outermost_main_frame_id_);
}
}
void ClientSideDetectionHost::MaybeStartGeminiAntiscamProtection(
GURL url,
ClientSideDetectionType request_type,
std::optional<bool> did_match_high_confidence_allowlist) {
delegate_->MaybeStartGeminiAntiscamProtection(
url, request_type, did_match_high_confidence_allowlist);
}
ClientSideDetectionFeatureCacheBase*
ClientSideDetectionHost::GetFeatureCache() {
ClientSideDetectionFeatureCache::CreateForWebContents(web_contents());
return ClientSideDetectionFeatureCache::FromWebContents(web_contents());
}
void ClientSideDetectionHost::MaybeStartImageEmbedding(
std::unique_ptr<ClientPhishingRequest> verdict,
std::optional<bool> did_match_high_confidence_allowlist,
bool is_invalid_ip,
PhishingDetectorResult result) {
visual_utils::CanExtractVisualFeaturesResult
can_extract_visual_features_result = DetermineVisualFeaturesExtraction();
// Clear the blurred image from the visual features if we should not extract
// visual features.
if (can_extract_visual_features_result !=
visual_utils::CanExtractVisualFeaturesResult::kCanExtractVisualFeatures) {
verdict->mutable_visual_features()->clear_image();
} else {
base::UmaHistogramBoolean("SBClientPhishing.HasVisualFeaturesImage2",
verdict->has_visual_features() &&
verdict->visual_features().has_image());
}
if (auto service = GetClientSideDetectionService();
IsEnhancedProtectionEnabled() && service &&
service->HasImageEmbeddingModel() &&
service->IsModelMetadataImageEmbeddingVersionMatching() &&
!verdict->has_image_feature_embedding()) {
content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame();
phishing_image_embedder_.reset();
rfh->GetRemoteAssociatedInterfaces()->GetInterface(
&phishing_image_embedder_);
if (phishing_image_embedder_.is_bound()) {
LogClientSideDetectionEvent(
ClientSideDetectionEvent::kImageEmbeddingBegin,
verdict->client_side_detection_type());
bool can_extract_visual_features =
result == PhishingDetectorResult::CLASSIFICATION_SKIPPED ||
can_extract_visual_features_result ==
visual_utils::CanExtractVisualFeaturesResult::
kCanExtractVisualFeatures;
set_image_embedding_start_time(tick_clock()->NowTicks());
phishing_image_embedder_->StartImageEmbedding(
current_url(), can_extract_visual_features,
base::BindOnce(&ClientSideDetectionHost::PhishingImageEmbeddingDone,
weak_factory_.GetWeakPtr(), std::move(verdict),
did_match_high_confidence_allowlist, is_invalid_ip));
}
return;
}
MaybeStartIntelligentScanForScamDetection(
std::move(verdict), did_match_high_confidence_allowlist, is_invalid_ip);
}
std::vector<GURL> ClientSideDetectionHost::GetRedirectChain() {
if (!web_contents() ||
!web_contents()->GetController().GetLastCommittedEntry()) {
return std::vector<GURL>();
}
return web_contents()
->GetController()
.GetLastCommittedEntry()
->GetRedirectChain();
}
GURL ClientSideDetectionHost::GetCurrentUrl() const {
if (!web_contents() || !web_contents()->GetPrimaryMainFrame()) {
return GURL();
}
return web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL();
}
} // namespace safe_browsing