| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/safe_browsing/password_protection/password_protection_service.h" |
| |
| #include <stddef.h> |
| |
| #include <memory> |
| #include <string> |
| |
| #include "base/base64.h" |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/post_task.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/history/core/browser/history_service.h" |
| #include "components/password_manager/core/browser/password_manager_metrics_util.h" |
| #include "components/password_manager/core/browser/password_reuse_detector.h" |
| #include "components/safe_browsing/common/utils.h" |
| #include "components/safe_browsing/db/database_manager.h" |
| #include "components/safe_browsing/features.h" |
| #include "components/safe_browsing/password_protection/password_protection_navigation_throttle.h" |
| #include "components/safe_browsing/password_protection/password_protection_request.h" |
| #include "components/zoom/zoom_controller.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/page_zoom.h" |
| #include "google_apis/google_api_keys.h" |
| #include "net/base/escape.h" |
| #include "net/base/url_util.h" |
| |
| using content::BrowserThread; |
| using content::WebContents; |
| using history::HistoryService; |
| using password_manager::metrics_util::PasswordType; |
| |
| namespace safe_browsing { |
| |
| using PasswordReuseEvent = LoginReputationClientRequest::PasswordReuseEvent; |
| |
| namespace { |
| |
| // Keys for storing password protection verdict into a DictionaryValue. |
| const int kRequestTimeoutMs = 10000; |
| const char kPasswordProtectionRequestUrl[] = |
| "https://sb-ssl.google.com/safebrowsing/clientreport/login"; |
| |
| } // namespace |
| |
| PasswordProtectionService::PasswordProtectionService( |
| const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| HistoryService* history_service) |
| : database_manager_(database_manager), |
| url_loader_factory_(url_loader_factory), |
| history_service_observer_(this) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (history_service) |
| history_service_observer_.Add(history_service); |
| } |
| |
| PasswordProtectionService::~PasswordProtectionService() { |
| tracker_.TryCancelAll(); |
| CancelPendingRequests(); |
| history_service_observer_.RemoveAll(); |
| weak_factory_.InvalidateWeakPtrs(); |
| } |
| |
| bool PasswordProtectionService::CanGetReputationOfURL(const GURL& url) { |
| if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS() || net::IsLocalhost(url)) |
| return false; |
| |
| const std::string hostname = url.HostNoBrackets(); |
| return !net::IsHostnameNonUnique(hostname) && |
| hostname.find('.') != std::string::npos; |
| } |
| |
| #if defined(ON_FOCUS_PING_ENABLED) |
| void PasswordProtectionService::MaybeStartPasswordFieldOnFocusRequest( |
| WebContents* web_contents, |
| const GURL& main_frame_url, |
| const GURL& password_form_action, |
| const GURL& password_form_frame_url, |
| const std::string& hosted_domain) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| RequestOutcome reason; |
| if (!base::FeatureList::IsEnabled(safe_browsing::kSendOnFocusPing)) { |
| return; |
| } |
| if (CanSendPing(LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE, |
| main_frame_url, |
| GetPasswordProtectionReusedPasswordAccountType( |
| PasswordType::PASSWORD_TYPE_UNKNOWN, |
| /*username=*/""), |
| &reason)) { |
| StartRequest(web_contents, main_frame_url, password_form_action, |
| password_form_frame_url, /* username */ "", |
| PasswordType::PASSWORD_TYPE_UNKNOWN, |
| {}, /* matching_domains: not used for this type */ |
| LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE, true); |
| } |
| } |
| #endif |
| |
| #if defined(SYNC_PASSWORD_REUSE_DETECTION_ENABLED) |
| void PasswordProtectionService::MaybeStartProtectedPasswordEntryRequest( |
| WebContents* web_contents, |
| const GURL& main_frame_url, |
| const std::string& username, |
| PasswordType password_type, |
| const std::vector<std::string>& matching_domains, |
| bool password_field_exists) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| ReusedPasswordAccountType reused_password_account_type = |
| GetPasswordProtectionReusedPasswordAccountType(password_type, username); |
| RequestOutcome reason; |
| // Need to populate |reason| to be passed into CanShowInterstitial. |
| bool can_send_ping = |
| CanSendPing(LoginReputationClientRequest::PASSWORD_REUSE_EVENT, |
| main_frame_url, reused_password_account_type, &reason); |
| if (IsSupportedPasswordTypeForPinging(password_type)) { |
| // Collect metrics about typical page-zoom on login pages. |
| double zoom_level = |
| zoom::ZoomController::GetZoomLevelForWebContents(web_contents); |
| UMA_HISTOGRAM_COUNTS_1000( |
| "PasswordProtection.PageZoomFactor", |
| static_cast<int>(100 * content::ZoomLevelToZoomFactor(zoom_level))); |
| if (can_send_ping) { |
| StartRequest(web_contents, main_frame_url, GURL(), GURL(), username, |
| password_type, matching_domains, |
| LoginReputationClientRequest::PASSWORD_REUSE_EVENT, |
| password_field_exists); |
| } else { |
| if (reused_password_account_type.is_account_syncing()) |
| MaybeLogPasswordReuseLookupEvent(web_contents, reason, password_type, |
| nullptr); |
| } |
| } |
| if (CanShowInterstitial(reason, reused_password_account_type, |
| main_frame_url)) { |
| username_for_last_shown_warning_ = username; |
| reused_password_account_type_for_last_shown_warning_ = |
| reused_password_account_type; |
| ShowInterstitial(web_contents, reused_password_account_type); |
| } |
| } |
| |
| bool PasswordProtectionService::ShouldShowModalWarning( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| ReusedPasswordAccountType password_type, |
| LoginReputationClientResponse::VerdictType verdict_type) { |
| if (trigger_type != LoginReputationClientRequest::PASSWORD_REUSE_EVENT || |
| !IsSupportedPasswordTypeForModalWarning(password_type)) { |
| return false; |
| } |
| |
| return (verdict_type == LoginReputationClientResponse::PHISHING || |
| verdict_type == LoginReputationClientResponse::LOW_REPUTATION) && |
| IsWarningEnabled(password_type); |
| } |
| |
| void PasswordProtectionService::RemoveWarningRequestsByWebContents( |
| content::WebContents* web_contents) { |
| for (auto it = warning_requests_.begin(); it != warning_requests_.end();) { |
| if (it->get()->web_contents() == web_contents) |
| it = warning_requests_.erase(it); |
| else |
| ++it; |
| } |
| } |
| |
| bool PasswordProtectionService::IsModalWarningShowingInWebContents( |
| content::WebContents* web_contents) { |
| for (const auto& request : warning_requests_) { |
| if (request->web_contents() == web_contents) |
| return true; |
| } |
| return false; |
| } |
| #endif |
| |
| LoginReputationClientResponse::VerdictType |
| PasswordProtectionService::GetCachedVerdict( |
| const GURL& url, |
| LoginReputationClientRequest::TriggerType trigger_type, |
| ReusedPasswordAccountType password_type, |
| LoginReputationClientResponse* out_response) { |
| return LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| } |
| |
| void PasswordProtectionService::CacheVerdict( |
| const GURL& url, |
| LoginReputationClientRequest::TriggerType trigger_type, |
| ReusedPasswordAccountType password_type, |
| const LoginReputationClientResponse& verdict, |
| const base::Time& receive_time) {} |
| |
| void PasswordProtectionService::StartRequest( |
| WebContents* web_contents, |
| const GURL& main_frame_url, |
| const GURL& password_form_action, |
| const GURL& password_form_frame_url, |
| const std::string& username, |
| PasswordType password_type, |
| const std::vector<std::string>& matching_domains, |
| LoginReputationClientRequest::TriggerType trigger_type, |
| bool password_field_exists) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| scoped_refptr<PasswordProtectionRequest> request( |
| new PasswordProtectionRequest( |
| web_contents, main_frame_url, password_form_action, |
| password_form_frame_url, username, password_type, matching_domains, |
| trigger_type, password_field_exists, this, GetRequestTimeoutInMS())); |
| request->Start(); |
| pending_requests_.insert(std::move(request)); |
| } |
| |
| bool PasswordProtectionService::CanSendPing( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| const GURL& main_frame_url, |
| ReusedPasswordAccountType password_type, |
| RequestOutcome* reason) { |
| *reason = RequestOutcome::UNKNOWN; |
| bool is_pinging_enabled = |
| IsPingingEnabled(trigger_type, password_type, reason); |
| // Pinging is enabled for password_reuse trigger level; however we need to |
| // make sure *reason is set appropriately. |
| PasswordProtectionTrigger trigger_level = |
| GetPasswordProtectionWarningTriggerPref(password_type); |
| if (trigger_level == PASSWORD_REUSE) { |
| *reason = RequestOutcome::PASSWORD_ALERT_MODE; |
| } |
| if (is_pinging_enabled && |
| !IsURLWhitelistedForPasswordEntry(main_frame_url, reason)) { |
| return true; |
| } |
| LogNoPingingReason(trigger_type, *reason, password_type); |
| return false; |
| } |
| |
| void PasswordProtectionService::RequestFinished( |
| PasswordProtectionRequest* request, |
| RequestOutcome outcome, |
| std::unique_ptr<LoginReputationClientResponse> response) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(request); |
| |
| if (response) { |
| ReusedPasswordAccountType password_type = |
| GetPasswordProtectionReusedPasswordAccountType(request->password_type(), |
| request->username()); |
| if (outcome != RequestOutcome::RESPONSE_ALREADY_CACHED) { |
| CacheVerdict(request->main_frame_url(), request->trigger_type(), |
| password_type, *response, base::Time::Now()); |
| } |
| bool enable_warning_for_non_sync_users = base::FeatureList::IsEnabled( |
| safe_browsing::kPasswordProtectionForSignedInUsers); |
| if (!enable_warning_for_non_sync_users && |
| request->password_type() == PasswordType::OTHER_GAIA_PASSWORD) { |
| return; |
| } |
| |
| // If it's password alert mode and a Gsuite/enterprise account, we do not |
| // show a modal warning. |
| if (outcome == RequestOutcome::PASSWORD_ALERT_MODE && |
| (password_type.account_type() == ReusedPasswordAccountType::GSUITE || |
| password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE)) { |
| return; |
| } |
| |
| #if defined(SYNC_PASSWORD_REUSE_DETECTION_ENABLED) |
| if (ShouldShowModalWarning(request->trigger_type(), password_type, |
| response->verdict_type())) { |
| username_for_last_shown_warning_ = request->username(); |
| reused_password_account_type_for_last_shown_warning_ = password_type; |
| ShowModalWarning(request->web_contents(), request->request_outcome(), |
| response->verdict_type(), response->verdict_token(), |
| password_type); |
| request->set_is_modal_warning_showing(true); |
| } |
| #endif |
| } |
| |
| request->HandleDeferredNavigations(); |
| |
| #if defined(SYNC_PASSWORD_REUSE_DETECTION_ENABLED) |
| // If the request is canceled, the PasswordProtectionService is already |
| // partially destroyed, and we won't be able to log accurate metrics. |
| if (outcome != RequestOutcome::CANCELED) { |
| auto verdict = |
| response ? response->verdict_type() |
| : LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED; |
| auto is_phishing_url = verdict == LoginReputationClientResponse::PHISHING; |
| MaybeReportPasswordReuseDetected(request->web_contents(), |
| request->username(), |
| request->password_type(), is_phishing_url); |
| } |
| #endif |
| |
| // Remove request from |pending_requests_| list. If it triggers warning, add |
| // it into the !warning_reqeusts_| list. |
| for (auto it = pending_requests_.begin(); it != pending_requests_.end(); |
| it++) { |
| if (it->get() == request) { |
| if (request->is_modal_warning_showing()) |
| warning_requests_.insert(std::move(request)); |
| pending_requests_.erase(it); |
| break; |
| } |
| } |
| } |
| |
| void PasswordProtectionService::CancelPendingRequests() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| for (auto it = pending_requests_.begin(); it != pending_requests_.end();) { |
| PasswordProtectionRequest* request = it->get(); |
| // These are the requests for whom we're still waiting for verdicts. |
| // We need to advance the iterator before we cancel because canceling |
| // the request will invalidate it when RequestFinished is called. |
| it++; |
| request->Cancel(false); |
| } |
| DCHECK(pending_requests_.empty()); |
| } |
| |
| int PasswordProtectionService::GetStoredVerdictCount( |
| LoginReputationClientRequest::TriggerType trigger_type) { |
| return -1; |
| } |
| |
| scoped_refptr<SafeBrowsingDatabaseManager> |
| PasswordProtectionService::database_manager() { |
| return database_manager_; |
| } |
| |
| GURL PasswordProtectionService::GetPasswordProtectionRequestUrl() { |
| GURL url(kPasswordProtectionRequestUrl); |
| std::string api_key = google_apis::GetAPIKey(); |
| DCHECK(!api_key.empty()); |
| return url.Resolve("?key=" + net::EscapeQueryParamValue(api_key, true)); |
| } |
| |
| int PasswordProtectionService::GetRequestTimeoutInMS() { |
| return kRequestTimeoutMs; |
| } |
| |
| void PasswordProtectionService::FillUserPopulation( |
| LoginReputationClientRequest::TriggerType trigger_type, |
| LoginReputationClientRequest* request_proto) { |
| ChromeUserPopulation* user_population = request_proto->mutable_population(); |
| user_population->set_user_population( |
| IsExtendedReporting() ? ChromeUserPopulation::EXTENDED_REPORTING |
| : ChromeUserPopulation::SAFE_BROWSING); |
| user_population->set_profile_management_status( |
| GetProfileManagementStatus(GetBrowserPolicyConnector())); |
| user_population->set_is_history_sync_enabled(IsHistorySyncEnabled()); |
| #if BUILDFLAG(FULL_SAFE_BROWSING) |
| user_population->set_is_under_advanced_protection( |
| IsUnderAdvancedProtection()); |
| #endif |
| user_population->set_is_incognito(IsIncognito()); |
| } |
| |
| void PasswordProtectionService::OnURLsDeleted( |
| history::HistoryService* history_service, |
| const history::DeletionInfo& deletion_info) { |
| base::PostTask( |
| FROM_HERE, {BrowserThread::UI}, |
| base::BindRepeating(&PasswordProtectionService:: |
| RemoveUnhandledSyncPasswordReuseOnURLsDeleted, |
| GetWeakPtr(), deletion_info.IsAllHistory(), |
| deletion_info.deleted_rows())); |
| } |
| |
| void PasswordProtectionService::HistoryServiceBeingDeleted( |
| history::HistoryService* history_service) { |
| history_service_observer_.RemoveAll(); |
| } |
| |
| std::unique_ptr<PasswordProtectionNavigationThrottle> |
| PasswordProtectionService::MaybeCreateNavigationThrottle( |
| content::NavigationHandle* navigation_handle) { |
| if (!navigation_handle->IsRendererInitiated()) |
| return nullptr; |
| |
| content::WebContents* web_contents = navigation_handle->GetWebContents(); |
| for (scoped_refptr<PasswordProtectionRequest> request : pending_requests_) { |
| if (request->web_contents() == web_contents && |
| request->trigger_type() == |
| safe_browsing::LoginReputationClientRequest::PASSWORD_REUSE_EVENT && |
| IsSupportedPasswordTypeForModalWarning( |
| GetPasswordProtectionReusedPasswordAccountType( |
| request->password_type(), username_for_last_shown_warning()))) { |
| return std::make_unique<PasswordProtectionNavigationThrottle>( |
| navigation_handle, request, /*is_warning_showing=*/false); |
| } |
| } |
| |
| for (scoped_refptr<PasswordProtectionRequest> request : warning_requests_) { |
| if (request->web_contents() == web_contents) { |
| return std::make_unique<PasswordProtectionNavigationThrottle>( |
| navigation_handle, request, /*is_warning_showing=*/true); |
| } |
| } |
| return nullptr; |
| } |
| |
| bool PasswordProtectionService::IsWarningEnabled( |
| ReusedPasswordAccountType password_type) { |
| return GetPasswordProtectionWarningTriggerPref(password_type) == |
| PHISHING_REUSE; |
| } |
| |
| // static |
| ReusedPasswordType |
| PasswordProtectionService::GetPasswordProtectionReusedPasswordType( |
| password_manager::metrics_util::PasswordType password_type) { |
| switch (password_type) { |
| case PasswordType::SAVED_PASSWORD: |
| return PasswordReuseEvent::SAVED_PASSWORD; |
| case PasswordType::PRIMARY_ACCOUNT_PASSWORD: |
| return PasswordReuseEvent::SIGN_IN_PASSWORD; |
| case PasswordType::OTHER_GAIA_PASSWORD: |
| return PasswordReuseEvent::OTHER_GAIA_PASSWORD; |
| case PasswordType::ENTERPRISE_PASSWORD: |
| return PasswordReuseEvent::ENTERPRISE_PASSWORD; |
| case PasswordType::PASSWORD_TYPE_UNKNOWN: |
| return PasswordReuseEvent::REUSED_PASSWORD_TYPE_UNKNOWN; |
| case PasswordType::PASSWORD_TYPE_COUNT: |
| break; |
| } |
| NOTREACHED(); |
| return PasswordReuseEvent::REUSED_PASSWORD_TYPE_UNKNOWN; |
| } |
| |
| ReusedPasswordAccountType |
| PasswordProtectionService::GetPasswordProtectionReusedPasswordAccountType( |
| password_manager::metrics_util::PasswordType password_type, |
| const std::string& username) const { |
| ReusedPasswordAccountType reused_password_account_type; |
| switch (password_type) { |
| case PasswordType::SAVED_PASSWORD: |
| reused_password_account_type.set_account_type( |
| ReusedPasswordAccountType::SAVED_PASSWORD); |
| return reused_password_account_type; |
| case PasswordType::ENTERPRISE_PASSWORD: |
| reused_password_account_type.set_account_type( |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE); |
| return reused_password_account_type; |
| case PasswordType::PRIMARY_ACCOUNT_PASSWORD: { |
| reused_password_account_type.set_is_account_syncing( |
| IsPrimaryAccountSyncing()); |
| if (!IsPrimaryAccountSignedIn()) { |
| reused_password_account_type.set_account_type( |
| ReusedPasswordAccountType::UNKNOWN); |
| return reused_password_account_type; |
| } |
| reused_password_account_type.set_account_type( |
| IsPrimaryAccountGmail() ? ReusedPasswordAccountType::GMAIL |
| : ReusedPasswordAccountType::GSUITE); |
| return reused_password_account_type; |
| } |
| case PasswordType::OTHER_GAIA_PASSWORD: { |
| AccountInfo account_info = GetSignedInNonSyncAccount(username); |
| if (account_info.account_id.empty()) { |
| reused_password_account_type.set_account_type( |
| ReusedPasswordAccountType::UNKNOWN); |
| return reused_password_account_type; |
| } |
| reused_password_account_type.set_account_type( |
| IsOtherGaiaAccountGmail(username) |
| ? ReusedPasswordAccountType::GMAIL |
| : ReusedPasswordAccountType::GSUITE); |
| return reused_password_account_type; |
| } |
| case PasswordType::PASSWORD_TYPE_UNKNOWN: |
| case PasswordType::PASSWORD_TYPE_COUNT: |
| reused_password_account_type.set_account_type( |
| ReusedPasswordAccountType::UNKNOWN); |
| return reused_password_account_type; |
| } |
| NOTREACHED(); |
| return reused_password_account_type; |
| } |
| |
| // static |
| PasswordType |
| PasswordProtectionService::ConvertReusedPasswordAccountTypeToPasswordType( |
| ReusedPasswordAccountType password_type) { |
| if (password_type.is_account_syncing()) { |
| return PasswordType::PRIMARY_ACCOUNT_PASSWORD; |
| } else if (password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) { |
| return PasswordType::ENTERPRISE_PASSWORD; |
| } else if (password_type.account_type() == |
| ReusedPasswordAccountType::SAVED_PASSWORD) { |
| return PasswordType::SAVED_PASSWORD; |
| } else if (password_type.account_type() == |
| ReusedPasswordAccountType::UNKNOWN) { |
| return PasswordType::PASSWORD_TYPE_UNKNOWN; |
| } else { |
| return PasswordType::OTHER_GAIA_PASSWORD; |
| } |
| } |
| |
| bool PasswordProtectionService::IsSupportedPasswordTypeForPinging( |
| PasswordType password_type) const { |
| switch (password_type) { |
| case PasswordType::SAVED_PASSWORD: |
| return true; |
| case PasswordType::PRIMARY_ACCOUNT_PASSWORD: |
| return true; |
| case PasswordType::ENTERPRISE_PASSWORD: |
| return true; |
| case PasswordType::OTHER_GAIA_PASSWORD: |
| return base::FeatureList::IsEnabled( |
| safe_browsing::kPasswordProtectionForSignedInUsers); |
| case PasswordType::PASSWORD_TYPE_UNKNOWN: |
| case PasswordType::PASSWORD_TYPE_COUNT: |
| return false; |
| } |
| NOTREACHED(); |
| return false; |
| } |
| |
| bool PasswordProtectionService::IsSupportedPasswordTypeForModalWarning( |
| ReusedPasswordAccountType password_type) const { |
| if (password_type.account_type() == |
| ReusedPasswordAccountType::NON_GAIA_ENTERPRISE) |
| return true; |
| |
| if (password_type.account_type() != ReusedPasswordAccountType::GMAIL && |
| password_type.account_type() != ReusedPasswordAccountType::GSUITE) |
| return false; |
| |
| return password_type.is_account_syncing() || |
| base::FeatureList::IsEnabled( |
| safe_browsing::kPasswordProtectionForSignedInUsers); |
| } |
| |
| #if BUILDFLAG(FULL_SAFE_BROWSING) |
| void PasswordProtectionService::GetPhishingDetector( |
| service_manager::InterfaceProvider* provider, |
| mojom::PhishingDetectorPtr* phishing_detector) { |
| provider->GetInterface(phishing_detector); |
| } |
| #endif |
| |
| } // namespace safe_browsing |