| // 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 |