| // 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 <memory> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/command_line.h" |
| #include "base/containers/span.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/metrics/histogram_macros.h" |
| #include "base/rand_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/uuid.h" |
| #include "components/permissions/permission_request_manager.h" |
| #include "components/prefs/pref_service.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/client_side_phishing_model.h" |
| #include "components/safe_browsing/content/browser/content_unsafe_resource_util.h" |
| #include "components/safe_browsing/content/common/safe_browsing.mojom-shared.h" |
| #include "components/safe_browsing/content/common/safe_browsing.mojom.h" |
| #include "components/safe_browsing/content/common/visual_utils.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/sync/sync_utils.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/security_interstitials/core/unsafe_resource_locator.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_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/http/http_response_headers.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "services/service_manager/public/cpp/interface_provider.h" |
| #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" |
| #include "third_party/blink/public/mojom/loader/referrer.mojom.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 { |
| |
| // Probability value used to sample pings on CSD allowlist match. For other safe |
| // browsing countermeasures, we sample at 1 in 100 rate, but in this, we hit the |
| // allowlist 1000 times more than the rate at which we send a ping due to local |
| // model verdict. Therefore, we sample at 1 in 100,000 rate instead. |
| const float kProbabilityForSendingSampleRequest = 0.000001; |
| // Probability value used to accept the high confidence allowlist match for |
| // trigger and force request types. More information on why this value was |
| // chosen can be found at go/crca-cspp-expand-allowlist. |
| const float kProbabilityForAcceptingHCAllowlistTrigger = 0.95; |
| |
| void WriteFeaturesToDisk(const ClientPhishingRequest& features, |
| const base::FilePath& base_path) { |
| base::FilePath path = |
| base_path.AppendASCII(base::Uuid::GenerateRandomV4().AsLowercaseString()); |
| base::File file(path, base::File::FLAG_CREATE | base::File::FLAG_WRITE); |
| if (!file.IsValid()) { |
| return; |
| } |
| file.WriteAtCurrentPos(base::as_byte_span(features.SerializeAsString())); |
| } |
| |
| bool HasDebugFeatureDirectory() { |
| return base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kCsdDebugFeatureDirectoryFlag); |
| } |
| |
| bool ShouldSkipCSDAllowlist() { |
| return base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kSkipCSDAllowlistOnPreclassification); |
| } |
| |
| base::FilePath GetDebugFeatureDirectory() { |
| return base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( |
| switches::kCsdDebugFeatureDirectoryFlag); |
| } |
| |
| std::string GetRequestTypeName( |
| ClientSideDetectionType client_side_detection_type) { |
| switch (client_side_detection_type) { |
| case safe_browsing::ClientSideDetectionType:: |
| CLIENT_SIDE_DETECTION_TYPE_UNSPECIFIED: |
| return "Unknown"; |
| case safe_browsing::ClientSideDetectionType::FORCE_REQUEST: |
| return "ForceRequest"; |
| case safe_browsing::ClientSideDetectionType::NOTIFICATION_PERMISSION_PROMPT: |
| return "NotificationPermissionPrompt"; |
| case safe_browsing::ClientSideDetectionType::TRIGGER_MODELS: |
| return "TriggerModel"; |
| case safe_browsing::ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED: |
| return "KeyboardLockRequested"; |
| case safe_browsing::ClientSideDetectionType::POINTER_LOCK_REQUESTED: |
| return "PointerLockRequested"; |
| case safe_browsing::ClientSideDetectionType::VIBRATION_API: |
| return "VibrationApi"; |
| } |
| } |
| |
| 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; |
| } |
| } |
| |
| void RecordAsyncCheckTriggerForceRequestResult( |
| ClientSideDetectionHost::AsyncCheckTriggerForceRequestResult result) { |
| base::UmaHistogramEnumeration( |
| "SBClientPhishing.ClientSideDetection." |
| "AsyncCheckTriggerForceRequestResult", |
| result); |
| } |
| |
| } // namespace |
| |
| typedef base::OnceCallback<void(bool, bool, std::optional<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<ClientSideDetectionService> csd_service, |
| SafeBrowsingDatabaseManager* database_manager, |
| ClientSideDetectionType phishing_detection_request_type, |
| float probability_for_accepting_hc_allowlist_trigger, |
| 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), |
| probability_for_accepting_hc_allowlist_trigger_( |
| probability_for_accepting_hc_allowlist_trigger), |
| 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 (csd_service_ && |
| csd_service_->IsLocalResource(remote_endpoint_.address())) { |
| DontClassifyForPhishing( |
| PreClassificationCheckResult::NO_CLASSIFY_LOCAL_RESOURCE); |
| } |
| |
| // Only classify [X]HTML documents. |
| if (mime_type_ != "text/html" && mime_type_ != "application/xhtml+xml") { |
| 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_->delegate_->GetPrefs() && |
| IsURLAllowlistedByPolicy(url_, *host_->delegate_->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() { |
| 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; |
| } |
| |
| 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 |
| }; |
| |
| bool ShouldClassifyForPhishing() const { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| return !start_phishing_classification_cb_.is_null(); |
| } |
| |
| void DontClassifyForPhishing(PreClassificationCheckResult reason) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (ShouldClassifyForPhishing()) { |
| // Track the first reason why we stopped classifying for phishing. |
| base::UmaHistogramEnumeration( |
| "SBClientPhishing.PreClassificationCheckResult", reason, |
| PreClassificationCheckResult::NO_CLASSIFY_MAX); |
| if (base::FeatureList::IsEnabled( |
| kClientSideDetectionDebuggingMetadataCache) && |
| host_ && host_->delegate_->GetPrefs() && |
| IsEnhancedProtectionEnabled(*host_->delegate_->GetPrefs())) { |
| 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); |
| } |
| 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. We also check the |
| // command line flag if the allowlist should be skipped. |
| if (phishing_detection_request_type_ == |
| safe_browsing::ClientSideDetectionType::FORCE_REQUEST || |
| 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) { |
| // 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( |
| "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. |
| } |
| // 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, |
| /*IntelligentScanVerdict=*/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, allow us to exceed the report limit. |
| if (!HasDebugFeatureDirectory() && csd_service_ && |
| csd_service_->AtPhishingReportLimit()) { |
| 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()) { |
| base::UmaHistogramEnumeration( |
| "SBClientPhishing.PreClassificationCheckResult", |
| PreClassificationCheckResult::CLASSIFY, |
| PreClassificationCheckResult::NO_CLASSIFY_MAX); |
| if (base::FeatureList::IsEnabled( |
| kClientSideDetectionDebuggingMetadataCache) && |
| host_ && host_->delegate_->GetPrefs() && |
| IsEnhancedProtectionEnabled(*host_->delegate_->GetPrefs())) { |
| 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_); |
| // Reset the callback to make sure ShouldClassifyForPhishing() |
| // returns false. |
| start_phishing_classification_cb_.Reset(); |
| } |
| } |
| |
| bool CanSendSamplePing() { |
| return phishing_detection_request_type_ == |
| ClientSideDetectionType::TRIGGER_MODELS && |
| host_ && host_->delegate_->GetPrefs() && |
| IsEnhancedProtectionEnabled(*host_->delegate_->GetPrefs()) && |
| base::RandDouble() <= kProbabilityForSendingSampleRequest && |
| base::FeatureList::IsEnabled(kClientSideDetectionSamplePing); |
| } |
| |
| bool ShouldAcceptHCAllowlist() { |
| // It can be inferred that it has value because it was set right before, but |
| // check again for sanity. |
| if (!did_match_high_confidence_allowlist_.has_value() || |
| !did_match_high_confidence_allowlist_.value()) { |
| return false; |
| } |
| |
| switch (phishing_detection_request_type_) { |
| case ClientSideDetectionType::TRIGGER_MODELS: |
| return base::FeatureList::IsEnabled( |
| kClientSideDetectionAcceptHCAllowlist) && |
| base::RandDouble() <= |
| probability_for_accepting_hc_allowlist_trigger_; |
| default: |
| return 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<ClientSideDetectionService> 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_; |
| float probability_for_accepting_hc_allowlist_trigger_; |
| base::WeakPtr<ClientSideDetectionHost> host_; |
| ShouldClassifyUrlCallback start_phishing_classification_cb_; |
| |
| base::WeakPtrFactory<ShouldClassifyUrlRequest> weak_factory_{this}; |
| }; |
| |
| // static |
| std::unique_ptr<ClientSideDetectionHost> ClientSideDetectionHost::Create( |
| content::WebContents* tab, |
| std::unique_ptr<Delegate> delegate, |
| PrefService* pref_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), pref_service, std::move(token_fetcher), |
| is_off_the_record, account_signed_in_callback)); |
| } |
| |
| ClientSideDetectionHost::ClientSideDetectionHost( |
| WebContents* tab, |
| std::unique_ptr<Delegate> delegate, |
| PrefService* pref_service, |
| std::unique_ptr<SafeBrowsingTokenFetcher> token_fetcher, |
| bool is_off_the_record, |
| const PrimaryAccountSignedIn& account_signed_in_callback) |
| : content::WebContentsObserver(tab), |
| csd_service_(nullptr), |
| tab_(tab), |
| classification_request_(nullptr), |
| tick_clock_(base::DefaultTickClock::GetInstance()), |
| delegate_(std::move(delegate)), |
| pref_service_(pref_service), |
| token_fetcher_(std::move(token_fetcher)), |
| is_off_the_record_(is_off_the_record), |
| account_signed_in_callback_(account_signed_in_callback), |
| probability_for_accepting_hc_allowlist_trigger_( |
| kProbabilityForAcceptingHCAllowlistTrigger) { |
| DCHECK(tab); |
| DCHECK(pref_service); |
| // Note: csd_service_ and sb_service will be nullptr here in testing. |
| csd_service_ = delegate_->GetClientSideDetectionService(); |
| |
| if (csd_service_) { |
| ClientSideDetectionFeatureCache::CreateForWebContents(web_contents()); |
| ClientSideDetectionFeatureCache::FromWebContents(web_contents()) |
| ->AddClearCacheSubscription(csd_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(); |
| } |
| |
| ClientSideDetectionHost::~ClientSideDetectionHost() { |
| if (classification_request_.get()) { |
| classification_request_->Cancel(); |
| } |
| } |
| |
| void ClientSideDetectionHost::RegisterPermissionRequestManager() { |
| if (IsEnhancedProtectionEnabled(*delegate_->GetPrefs()) && |
| base::FeatureList::IsEnabled(kClientSideDetectionNotificationPrompt)) { |
| permission_request_observation_.Observe( |
| permissions::PermissionRequestManager::FromWebContents(web_contents())); |
| } |
| } |
| |
| void ClientSideDetectionHost::RegisterAsyncCheckTracker() { |
| if (IsEnhancedProtectionEnabled(*delegate_->GetPrefs())) { |
| AsyncCheckTracker* tracker = |
| AsyncCheckTracker::FromWebContents(web_contents()); |
| CHECK(tracker); |
| async_check_observation_.Observe(tracker); |
| } |
| } |
| |
| void ClientSideDetectionHost::MaybeStartPreClassification( |
| ClientSideDetectionType request_type) { |
| if (base::FeatureList::IsEnabled(kClientSideDetectionKillswitch)) { |
| return; |
| } |
| // Cancel any pending classification request. |
| if (classification_request_.get()) { |
| classification_request_->Cancel(); |
| } |
| // 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. |
| weak_factory_.InvalidateWeakPtrs(); |
| |
| if (!csd_service_) { |
| return; |
| } |
| |
| content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame(); |
| |
| current_url_ = rfh->GetLastCommittedURL(); |
| current_outermost_main_frame_id_ = rfh->GetGlobalId(); |
| // 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(), csd_service_, database_manager_.get(), request_type, |
| probability_for_accepting_hc_allowlist_trigger_, |
| weak_factory_.GetWeakPtr()); |
| classification_request_->Start(); |
| } |
| |
| 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); |
| |
| trigger_models_request_skipped_ = false; |
| MaybeStartPreClassification(ClientSideDetectionType::TRIGGER_MODELS); |
| } |
| |
| void ClientSideDetectionHost::OnPromptAdded() { |
| if (!IsEnhancedProtectionEnabled(*delegate_->GetPrefs())) { |
| return; |
| } |
| |
| permissions::PermissionRequestManager* permission_request_manager = |
| permissions::PermissionRequestManager::FromWebContents(web_contents()); |
| CHECK(permission_request_manager); |
| |
| if (base::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() { |
| // If TRIGGER_MODELS ping is not skipped, do not allow async check to trigger |
| // another request. This is to avoid duplicate pings. |
| if (!trigger_models_request_skipped_) { |
| RecordAsyncCheckTriggerForceRequestResult( |
| AsyncCheckTriggerForceRequestResult:: |
| kSkippedTriggerModelsPingNotSkipped); |
| return; |
| } |
| if (!HasForceRequestFromRtUrlLookup()) { |
| RecordAsyncCheckTriggerForceRequestResult( |
| AsyncCheckTriggerForceRequestResult::kSkippedNotForced); |
| return; |
| } |
| RecordAsyncCheckTriggerForceRequestResult( |
| AsyncCheckTriggerForceRequestResult::kTriggered); |
| MaybeStartPreClassification(ClientSideDetectionType::FORCE_REQUEST); |
| } |
| |
| void ClientSideDetectionHost::OnAsyncSafeBrowsingCheckTrackerDestructed() { |
| async_check_observation_.Reset(); |
| } |
| |
| void ClientSideDetectionHost::KeyboardLockRequested() { |
| if (!IsEnhancedProtectionEnabled(*delegate_->GetPrefs()) || |
| !base::FeatureList::IsEnabled( |
| kClientSideDetectionKeyboardPointerLockRequest)) { |
| return; |
| } |
| |
| MaybeStartPreClassification(ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED); |
| } |
| |
| void ClientSideDetectionHost::PointerLockRequested() { |
| if (!IsEnhancedProtectionEnabled(*delegate_->GetPrefs()) || |
| !base::FeatureList::IsEnabled( |
| kClientSideDetectionKeyboardPointerLockRequest)) { |
| return; |
| } |
| |
| MaybeStartPreClassification(ClientSideDetectionType::POINTER_LOCK_REQUESTED); |
| } |
| |
| void ClientSideDetectionHost::VibrationRequested() { |
| if (!IsEnhancedProtectionEnabled(*delegate_->GetPrefs()) || |
| !base::FeatureList::IsEnabled(kClientSideDetectionVibrationApi)) { |
| 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. |
| ClientSideDetectionFeatureCache::CreateForWebContents(web_contents()); |
| ClientSideDetectionFeatureCache* feature_cache_map = |
| ClientSideDetectionFeatureCache::FromWebContents(web_contents()); |
| if (!feature_cache_map->WasVibrationClassificationTriggered(current_url_)) { |
| MaybeStartPreClassification(ClientSideDetectionType::VIBRATION_API); |
| } |
| } |
| |
| void ClientSideDetectionHost::OnPhishingPreClassificationDone( |
| ClientSideDetectionType request_type, |
| bool should_classify, |
| bool is_sample_ping, |
| std::optional<bool> did_match_high_confidence_allowlist) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (should_classify) { |
| content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame(); |
| |
| phishing_detector_.reset(); |
| rfh->GetRemoteAssociatedInterfaces()->GetInterface(&phishing_detector_); |
| |
| if (phishing_detector_.is_bound()) { |
| phishing_detection_start_time_ = tick_clock_->NowTicks(); |
| phishing_detector_->StartPhishingDetection( |
| current_url_, |
| base::BindOnce(&ClientSideDetectionHost::PhishingDetectionDone, |
| weak_factory_.GetWeakPtr(), request_type, |
| is_sample_ping, did_match_high_confidence_allowlist)); |
| } |
| } |
| } |
| |
| void ClientSideDetectionHost::PhishingDetectionDone( |
| ClientSideDetectionType request_type, |
| bool is_sample_ping, |
| std::optional<bool> did_match_high_confidence_allowlist, |
| 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(csd_service_); |
| |
| ClientSideDetectionFeatureCache* feature_cache_map = nullptr; |
| |
| ClientSideDetectionFeatureCache::CreateForWebContents(web_contents()); |
| feature_cache_map = |
| ClientSideDetectionFeatureCache::FromWebContents(web_contents()); |
| |
| phishing_detector_.reset(); |
| |
| std::string request_type_name = GetRequestTypeName(request_type); |
| |
| UmaHistogramMediumTimes( |
| "SBClientPhishing.PhishingDetectionDuration", |
| base::TimeTicks::Now() - phishing_detection_start_time_); |
| UmaHistogramMediumTimes( |
| "SBClientPhishing.PhishingDetectionDuration." + request_type_name, |
| base::TimeTicks::Now() - phishing_detection_start_time_); |
| base::UmaHistogramEnumeration("SBClientPhishing.PhishingDetectorResult", |
| result); |
| base::UmaHistogramEnumeration( |
| "SBClientPhishing.PhishingDetectorResult." + request_type_name, result); |
| |
| if (feature_cache_map && |
| base::FeatureList::IsEnabled( |
| kClientSideDetectionDebuggingMetadataCache) && |
| IsEnhancedProtectionEnabled(*delegate_->GetPrefs())) { |
| feature_cache_map->GetOrCreateDebuggingMetadataForURL(current_url_) |
| ->set_phishing_detector_result(GetPhishingDetectorResult(result)); |
| } |
| |
| if (result == mojom::PhishingDetectorResult::CLASSIFIER_NOT_READY) { |
| bool is_model_available = csd_service_->IsModelAvailable(); |
| base::UmaHistogramBoolean( |
| "SBClientPhishing.BrowserReadyOnClassifierNotReady", |
| is_model_available); |
| } else if (feature_cache_map && |
| base::FeatureList::IsEnabled( |
| kClientSideDetectionDebuggingMetadataCache) && |
| IsEnhancedProtectionEnabled(*delegate_->GetPrefs())) { |
| // We should only add this if the classifier is ready, because then we have |
| // the trigger model version in the model class. |
| feature_cache_map->GetOrCreateDebuggingMetadataForURL(current_url_) |
| ->set_csd_model_version(csd_service_->GetTriggerModelVersion()); |
| } |
| |
| if (result != mojom::PhishingDetectorResult::SUCCESS) { |
| return; |
| } |
| |
| // We parse the protocol buffer here. If we're unable to parse it or it was |
| // not provided we won't send the verdict further. |
| std::optional<ClientPhishingRequest> verdict; |
| if (wrapped_verdict.has_value()) { |
| verdict = wrapped_verdict->As<ClientPhishingRequest>(); |
| } |
| base::UmaHistogramBoolean("SBClientPhishing.VerdictParseSuccessful", |
| verdict.has_value()); |
| if (csd_service_ && verdict.has_value()) { |
| verdict->set_client_side_detection_type(request_type); |
| if (is_sample_ping) { |
| verdict->set_report_type(ClientPhishingRequest::SAMPLE_REPORT); |
| } else { |
| verdict->set_report_type(ClientPhishingRequest::FULL_REPORT); |
| } |
| // We should only cache the verdict string if the result is SUCCESS, so that |
| // in a situation where it is not, PG can retry the classification |
| // because classifier can be ready or a new model is ready to address |
| // the failure reasons. |
| if (feature_cache_map) { |
| // Initial implementation of the feature is that only PG will use the |
| // cache to reuse the images that are computed by CSD-Phishing/PG. In |
| // scenarios where the user reloads the page, we could use the images |
| // again, and we will log to see the efficiency if we were to. |
| bool cache_csd_phishing_data_available = |
| feature_cache_map->GetVerdictForURL(current_url_) != nullptr; |
| |
| base::UmaHistogramBoolean( |
| "SBClientPhishing.CSDPhishingCachedDataAvailable", |
| cache_csd_phishing_data_available); |
| |
| feature_cache_map->InsertVerdict( |
| current_url_, std::make_unique<ClientPhishingRequest>(*verdict)); |
| } |
| |
| MaybeSendClientPhishingRequest( |
| std::make_unique<ClientPhishingRequest>(verdict.value()), |
| did_match_high_confidence_allowlist); |
| } |
| } |
| |
| // To keep the flow consistent, we want to append additional information to the |
| // ClientPhishingRequest message based on feature availability in the following |
| // order: image embedding, on-device model output, then token fetch. If one |
| // feature is not available, we will move on to the next in the order until we |
| // ultimately send the request. |
| void ClientSideDetectionHost::MaybeSendClientPhishingRequest( |
| std::unique_ptr<ClientPhishingRequest> verdict, |
| std::optional<bool> did_match_high_confidence_allowlist) { |
| csd_service_->ClassifyPhishingThroughThresholds(verdict.get()); |
| VLOG(2) << "Phishing classification score: " << verdict->client_score(); |
| VLOG(2) << "Visual model scores:"; |
| for (const ClientPhishingRequest::CategoryScore& label_and_value : |
| verdict->tflite_model_scores()) { |
| VLOG(2) << label_and_value.label() << ": " << label_and_value.value(); |
| } |
| |
| if (HasDebugFeatureDirectory()) { |
| base::ThreadPool::PostTask(FROM_HERE, {base::MayBlock()}, |
| base::BindOnce(&WriteFeaturesToDisk, *verdict, |
| GetDebugFeatureDirectory())); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| gfx::Size size; |
| content::RenderWidgetHostView* view = |
| web_contents()->GetRenderWidgetHostView(); |
| if (view) { |
| gfx::SizeF viewport = view->GetNativeView()->viewport_size(); |
| size = gfx::Size(static_cast<int>(viewport.width()), |
| static_cast<int>(viewport.height())); |
| } |
| visual_utils::CanExtractVisualFeaturesResult |
| can_extract_visual_features_result = |
| visual_utils::CanExtractVisualFeatures( |
| IsExtendedReportingEnabled(*delegate_->GetPrefs()), |
| web_contents()->GetBrowserContext()->IsOffTheRecord(), size); |
| #else |
| gfx::Size size; |
| content::RenderWidgetHostView* view = |
| web_contents()->GetRenderWidgetHostView(); |
| if (view) { |
| size = view->GetVisibleViewportSize(); |
| } |
| visual_utils::CanExtractVisualFeaturesResult |
| can_extract_visual_features_result = |
| visual_utils::CanExtractVisualFeatures( |
| IsExtendedReportingEnabled(*delegate_->GetPrefs()), |
| web_contents()->GetBrowserContext()->IsOffTheRecord(), size, |
| zoom::ZoomController::GetZoomLevelForWebContents(web_contents())); |
| #endif |
| base::UmaHistogramEnumeration("SBClientPhishing.VisualFeaturesClearReason", |
| can_extract_visual_features_result); |
| if (can_extract_visual_features_result != |
| visual_utils::CanExtractVisualFeaturesResult::kCanExtractVisualFeatures) { |
| verdict->clear_visual_features(); |
| } |
| |
| if (IsEnhancedProtectionEnabled(*delegate_->GetPrefs())) { |
| delegate_->AddReferrerChain(verdict.get(), current_url_, |
| current_outermost_main_frame_id_); |
| } |
| |
| base::UmaHistogramBoolean("SBClientPhishing.LocalModelDetectsPhishing", |
| verdict->is_phishing()); |
| std::string request_type_name = |
| GetRequestTypeName(verdict->client_side_detection_type()); |
| base::UmaHistogramBoolean( |
| "SBClientPhishing.LocalModelDetectsPhishing." + request_type_name, |
| verdict->is_phishing()); |
| |
| bool force_request_from_rt_url_lookup = false; |
| |
| if (verdict->client_side_detection_type() == |
| ClientSideDetectionType::TRIGGER_MODELS && |
| HasForceRequestFromRtUrlLookup()) { |
| verdict->set_client_side_detection_type( |
| safe_browsing::ClientSideDetectionType::FORCE_REQUEST); |
| force_request_from_rt_url_lookup = true; |
| if (base::FeatureList::IsEnabled( |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection)) { |
| raw_ptr<VerdictCacheManager> cache_manager = delegate_->GetCacheManager(); |
| |
| if (cache_manager && current_url_.is_valid()) { |
| safe_browsing::LlamaForcedTriggerInfo llama_forced_trigger_info; |
| if (cache_manager->GetCachedRealTimeLlamaForcedTriggerInfo( |
| current_url_, &llama_forced_trigger_info)) { |
| verdict->mutable_llama_forced_trigger_info()->Swap( |
| &llama_forced_trigger_info); |
| } |
| } |
| } |
| } |
| |
| base::UmaHistogramBoolean("SBClientPhishing.RTLookupForceRequest", |
| force_request_from_rt_url_lookup); |
| |
| base::UmaHistogramExactLinear( |
| "SBClientPhishing.ClientSideDetectionTypeRequest", |
| verdict->client_side_detection_type(), ClientSideDetectionType_MAX + 1); |
| |
| if (base::FeatureList::IsEnabled( |
| kClientSideDetectionDebuggingMetadataCache) && |
| IsEnhancedProtectionEnabled(*delegate_->GetPrefs())) { |
| ClientSideDetectionFeatureCache::CreateForWebContents(web_contents()); |
| ClientSideDetectionFeatureCache* feature_cache_map = |
| ClientSideDetectionFeatureCache::FromWebContents(web_contents()); |
| LoginReputationClientRequest::DebuggingMetadata* debugging_metadata = |
| feature_cache_map->GetOrCreateDebuggingMetadataForURL(current_url_); |
| debugging_metadata->set_local_model_detects_phishing( |
| verdict->is_phishing()); |
| debugging_metadata->set_forced_request(force_request_from_rt_url_lookup); |
| } |
| |
| // We only send a phishing verdict if the verdict is phishing, the client |
| // side detection type is |TRIGGER_MODELS|, AND the request is not a sample |
| // ping. The detection type can be changed to FORCE_REQUEST from a |
| // RTLookupResponse for a SBER/ESB user. This can also be changed when the |
| // request is made from a notification permission prompt, keyboard & pointer |
| // lock API. |
| trigger_models_request_skipped_ = |
| !verdict->is_phishing() && |
| verdict->client_side_detection_type() == |
| ClientSideDetectionType::TRIGGER_MODELS && |
| verdict->report_type() == ClientPhishingRequest::FULL_REPORT; |
| if (trigger_models_request_skipped_) { |
| return; |
| } |
| |
| // Fill in metadata about which model we used. |
| *verdict->mutable_population() = delegate_->GetUserPopulation(); |
| verdict->mutable_population()->add_finch_active_groups( |
| base::FeatureList::IsEnabled(kConditionalImageResize) |
| ? "ConditionalImageResize.Enabled" |
| : "ConditionalImageResize.Control"); |
| |
| raw_ptr<VerdictCacheManager> cache_manager = delegate_->GetCacheManager(); |
| if (cache_manager) { |
| ChromeUserPopulation::PageLoadToken token = |
| cache_manager->GetPageLoadToken(current_url_); |
| // It's possible that the token is not found because real time URL check |
| // is not performed for this navigation. Create a new page load token in |
| // this case. |
| if (!token.has_token_value()) { |
| token = cache_manager->CreatePageLoadToken(current_url_); |
| } |
| verdict->mutable_population()->mutable_page_load_tokens()->Add()->Swap( |
| &token); |
| } |
| |
| if (IsEnhancedProtectionEnabled(*delegate_->GetPrefs()) && |
| csd_service_->HasImageEmbeddingModel() && |
| csd_service_->IsModelMetadataImageEmbeddingVersionMatching()) { |
| content::RenderFrameHost* rfh = web_contents()->GetPrimaryMainFrame(); |
| |
| phishing_image_embedder_.reset(); |
| rfh->GetRemoteAssociatedInterfaces()->GetInterface( |
| &phishing_image_embedder_); |
| |
| if (phishing_image_embedder_.is_bound()) { |
| phishing_image_embedder_->StartImageEmbedding( |
| current_url_, |
| base::BindOnce(&ClientSideDetectionHost::PhishingImageEmbeddingDone, |
| weak_factory_.GetWeakPtr(), std::move(verdict), |
| did_match_high_confidence_allowlist)); |
| } |
| |
| return; |
| } |
| |
| MaybeInquireOnDeviceForScamDetection(std::move(verdict), |
| did_match_high_confidence_allowlist); |
| } |
| |
| void ClientSideDetectionHost::PhishingImageEmbeddingDone( |
| std::unique_ptr<ClientPhishingRequest> verdict, |
| std::optional<bool> did_match_high_confidence_allowlist, |
| mojom::PhishingImageEmbeddingResult result, |
| std::optional<mojo_base::ProtoWrapper> image_feature_embedding) { |
| base::UmaHistogramEnumeration("SBClientPhishing.PhishingImageEmbeddingResult", |
| result); |
| if (result == mojom::PhishingImageEmbeddingResult::kSuccess) { |
| std::optional<ImageFeatureEmbedding> embedding; |
| if (image_feature_embedding.has_value()) { |
| embedding = image_feature_embedding->As<ImageFeatureEmbedding>(); |
| } |
| if (embedding.has_value()) { |
| *verdict->mutable_image_feature_embedding() = |
| std::move(embedding.value()); |
| } else { |
| VLOG(0) << "Failed to parse image feature embedding."; |
| } |
| } |
| |
| MaybeInquireOnDeviceForScamDetection(std::move(verdict), |
| did_match_high_confidence_allowlist); |
| } |
| |
| void ClientSideDetectionHost::MaybeInquireOnDeviceForScamDetection( |
| std::unique_ptr<ClientPhishingRequest> verdict, |
| std::optional<bool> did_match_high_confidence_allowlist) { |
| bool is_keyboard_lock_requested = |
| base::FeatureList::IsEnabled( |
| kClientSideDetectionBrandAndIntentForScamDetection) && |
| verdict->client_side_detection_type() == |
| ClientSideDetectionType::KEYBOARD_LOCK_REQUESTED; |
| |
| bool is_intelligent_scan_requested = |
| base::FeatureList::IsEnabled( |
| kClientSideDetectionLlamaForcedTriggerInfoForScamDetection) && |
| verdict->has_llama_forced_trigger_info() && |
| verdict->llama_forced_trigger_info().intelligent_scan(); |
| |
| if (IsEnhancedProtectionEnabled(*delegate_->GetPrefs()) && |
| (is_keyboard_lock_requested || is_intelligent_scan_requested)) { |
| delegate_->GetInnerText( |
| base::BindOnce(&ClientSideDetectionHost::OnInnerTextComplete, |
| weak_factory_.GetWeakPtr(), std::move(verdict), |
| did_match_high_confidence_allowlist)); |
| return; |
| } |
| |
| MaybeGetAccessToken(std::move(verdict), did_match_high_confidence_allowlist); |
| } |
| |
| void ClientSideDetectionHost::OnInnerTextComplete( |
| std::unique_ptr<ClientPhishingRequest> verdict, |
| std::optional<bool> did_match_high_confidence_allowlist, |
| std::string inner_text) { |
| base::UmaHistogramCounts100000("SBClientPhishing.OnDeviceModelInnerTextSize", |
| inner_text.size()); |
| csd_service_->InquireOnDeviceModel( |
| verdict.get(), inner_text, |
| base::BindOnce(&ClientSideDetectionHost::OnInquireOnDeviceModelDone, |
| weak_factory_.GetWeakPtr(), std::move(verdict), |
| did_match_high_confidence_allowlist)); |
| } |
| |
| void ClientSideDetectionHost::OnInquireOnDeviceModelDone( |
| std::unique_ptr<ClientPhishingRequest> verdict, |
| std::optional<bool> did_match_high_confidence_allowlist, |
| std::optional<optimization_guide::proto::ScamDetectionResponse> response) { |
| base::UmaHistogramBoolean( |
| "SBClientPhishing.OnDeviceModelHasSuccessfulResponse", |
| response.has_value()); |
| |
| if (response.has_value()) { |
| IntelligentScanInfo intelligent_scan_info; |
| intelligent_scan_info.set_brand(response->brand()); |
| intelligent_scan_info.set_intent(response->intent()); |
| *verdict->mutable_intelligent_scan_info() = |
| std::move(intelligent_scan_info); |
| } |
| |
| MaybeGetAccessToken(std::move(verdict), did_match_high_confidence_allowlist); |
| } |
| |
| void ClientSideDetectionHost::MaybeGetAccessToken( |
| std::unique_ptr<ClientPhishingRequest> verdict, |
| std::optional<bool> did_match_high_confidence_allowlist) { |
| if (CanGetAccessToken()) { |
| token_fetcher_->Start(base::BindOnce( |
| &ClientSideDetectionHost::OnGotAccessToken, weak_factory_.GetWeakPtr(), |
| std::move(verdict), did_match_high_confidence_allowlist)); |
| return; |
| } |
| |
| std::string empty_access_token; |
| SendRequest(std::move(verdict), empty_access_token, |
| did_match_high_confidence_allowlist); |
| } |
| |
| void ClientSideDetectionHost::MaybeShowPhishingWarning( |
| bool is_from_cache, |
| ClientSideDetectionType request_type, |
| std::optional<bool> did_match_high_confidence_allowlist, |
| GURL phishing_url, |
| bool is_phishing, |
| std::optional<net::HttpStatusCode> response_code, |
| std::optional<IntelligentScanVerdict> intelligent_scan_verdict) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| std::string request_type_name = GetRequestTypeName(request_type); |
| if (!is_from_cache) { |
| base::UmaHistogramBoolean("SBClientPhishing.ServerModelDetectsPhishing", |
| is_phishing); |
| base::UmaHistogramBoolean( |
| "SBClientPhishing.ServerModelDetectsPhishing." + request_type_name, |
| is_phishing); |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| kClientSideDetectionDebuggingMetadataCache) && |
| IsEnhancedProtectionEnabled(*delegate_->GetPrefs()) && |
| response_code.has_value()) { |
| ClientSideDetectionFeatureCache::CreateForWebContents(web_contents()); |
| ClientSideDetectionFeatureCache* feature_cache_map = |
| ClientSideDetectionFeatureCache::FromWebContents(web_contents()); |
| feature_cache_map->GetOrCreateDebuggingMetadataForURL(phishing_url) |
| ->set_network_result(response_code.value()); |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| kClientSideDetectionBrandAndIntentForScamDetection) && |
| IsEnhancedProtectionEnabled(*delegate_->GetPrefs()) && |
| intelligent_scan_verdict.has_value()) { |
| base::UmaHistogramExactLinear("SBClientPhishing.IntelligentScanVerdict", |
| intelligent_scan_verdict.value(), |
| IntelligentScanVerdict_MAX + 1); |
| } |
| |
| bool should_show_warning = |
| base::FeatureList::IsEnabled(kClientSideDetectionShowScamVerdictWarning) |
| ? (is_phishing || |
| (intelligent_scan_verdict.has_value() && |
| intelligent_scan_verdict != |
| IntelligentScanVerdict:: |
| INTELLIGENT_SCAN_VERDICT_UNSPECIFIED && |
| intelligent_scan_verdict != |
| IntelligentScanVerdict::INTELLIGENT_SCAN_VERDICT_SAFE)) |
| : is_phishing; |
| |
| if (should_show_warning) { |
| if (!is_from_cache && did_match_high_confidence_allowlist.has_value()) { |
| base::UmaHistogramBoolean( |
| "SBClientPhishing.HighConfidenceAllowlistMatchOnServerVerdictPhishy", |
| did_match_high_confidence_allowlist.value()); |
| base::UmaHistogramBoolean( |
| "SBClientPhishing." |
| "HighConfidenceAllowlistMatchOnServerVerdictPhishy." + |
| request_type_name, |
| did_match_high_confidence_allowlist.value()); |
| } |
| DCHECK(web_contents()); |
| 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.rfh_locator = security_interstitials::UnsafeResourceLocator:: |
| CreateForRenderFrameToken( |
| primary_main_frame_id.child_id, |
| primary_main_frame->GetFrameToken().value()); |
| if (!ui_manager_->IsAllowlisted(resource.url, resource.rfh_locator, |
| resource.navigation_id, |
| resource.threat_type)) { |
| // We need to stop any pending navigations, otherwise the interstitial |
| // might not get created properly. |
| web_contents()->GetController().DiscardNonCommittedEntries(); |
| } |
| ui_manager_->DisplayBlockingPage(resource); |
| } |
| // If there is true phishing verdict, invalidate weakptr so that no longer |
| // consider the malware vedict. |
| weak_factory_.InvalidateWeakPtrs(); |
| } |
| } |
| |
| bool ClientSideDetectionHost::HasForceRequestFromRtUrlLookup() { |
| raw_ptr<VerdictCacheManager> cache_manager = delegate_->GetCacheManager(); |
| |
| if (!cache_manager || !current_url_.is_valid()) { |
| return false; |
| } |
| |
| safe_browsing::ClientSideDetectionType cached_csd_type = |
| cache_manager->GetCachedRealTimeUrlClientSideDetectionType(current_url_); |
| return cached_csd_type == |
| safe_browsing::ClientSideDetectionType::FORCE_REQUEST && |
| IsEnhancedProtectionEnabled(*delegate_->GetPrefs()); |
| } |
| |
| void ClientSideDetectionHost::set_client_side_detection_service( |
| base::WeakPtr<ClientSideDetectionService> service) { |
| csd_service_ = service; |
| } |
| |
| 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::OnGotAccessToken( |
| std::unique_ptr<ClientPhishingRequest> verdict, |
| std::optional<bool> did_match_high_confidence_allowlist, |
| const std::string& access_token) { |
| ClientSideDetectionHost::SendRequest(std::move(verdict), access_token, |
| did_match_high_confidence_allowlist); |
| } |
| |
| bool ClientSideDetectionHost::CanGetAccessToken() { |
| if (is_off_the_record_) { |
| return false; |
| } |
| |
| // Return true if the primary user account of an ESB user is signed in. |
| return IsEnhancedProtectionEnabled(*pref_service_) && |
| !account_signed_in_callback_.is_null() && |
| account_signed_in_callback_.Run(); |
| } |
| |
| void ClientSideDetectionHost::SendRequest( |
| std::unique_ptr<ClientPhishingRequest> verdict, |
| const std::string& access_token, |
| std::optional<bool> did_match_high_confidence_allowlist) { |
| ClientSideDetectionService::ClientReportPhishingRequestCallback callback = |
| base::BindOnce(&ClientSideDetectionHost::MaybeShowPhishingWarning, |
| weak_factory_.GetWeakPtr(), |
| /*is_from_cache=*/false, |
| verdict->client_side_detection_type(), |
| did_match_high_confidence_allowlist); |
| csd_service_->SendClientReportPhishingRequest( |
| std::move(verdict), std::move(callback), access_token); |
| } |
| |
| void ClientSideDetectionHost:: |
| set_high_confidence_allowlist_acceptance_rate_for_testing( |
| float acceptance_rate) { |
| probability_for_accepting_hc_allowlist_trigger_ = acceptance_rate; |
| } |
| |
| } // namespace safe_browsing |