blob: 4170d5472abfcc4f771f46ea982f15319997c66d [file] [log] [blame]
// 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_request.h"
#include <cstddef>
#include <memory>
#include "base/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/post_task.h"
#include "base/time/time.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/safe_browsing.mojom.h"
#include "components/safe_browsing/db/allowlist_checker_client.h"
#include "components/safe_browsing/features.h"
#include "components/safe_browsing/password_protection/metrics_util.h"
#include "components/safe_browsing/password_protection/password_protection_navigation_throttle.h"
#include "components/safe_browsing/password_protection/visual_utils.h"
#include "components/safe_browsing/proto/csd.pb.h"
#include "components/safe_browsing/web_ui/safe_browsing_ui.h"
#include "components/zoom/zoom_controller.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "net/base/escape.h"
#include "net/base/load_flags.h"
#include "net/base/url_util.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "url/origin.h"
using content::BrowserThread;
using content::WebContents;
namespace safe_browsing {
using ReusedPasswordAccountType =
LoginReputationClientRequest::PasswordReuseEvent::ReusedPasswordAccountType;
namespace {
// Cap on how many reused domains can be included in a report, to limit
// the size of the report. UMA suggests 99.9% will have < 200 domains.
const int kMaxReusedDomains = 200;
#if BUILDFLAG(FULL_SAFE_BROWSING)
// The maximum time to wait for DOM features to be collected, in milliseconds.
const int kDomFeatureTimeoutMs = 3000;
// Parameters chosen to ensure privacy is preserved by visual features.
const int kMinWidthForVisualFeatures = 576;
const int kMinHeightForVisualFeatures = 576;
const float kMaxZoomForVisualFeatures = 2.0;
std::unique_ptr<VisualFeatures> ExtractVisualFeatures(
const SkBitmap& screenshot) {
auto features = std::make_unique<VisualFeatures>();
visual_utils::GetHistogramForImage(screenshot,
features->mutable_color_histogram());
visual_utils::GetBlurredImage(screenshot, features->mutable_image());
return features;
}
#endif
} // namespace
PasswordProtectionRequest::PasswordProtectionRequest(
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 type,
bool password_field_exists,
PasswordProtectionService* pps,
int request_timeout_in_ms)
: content::WebContentsObserver(web_contents),
web_contents_(web_contents),
main_frame_url_(main_frame_url),
password_form_action_(password_form_action),
password_form_frame_url_(password_form_frame_url),
username_(username),
password_type_(password_type),
matching_domains_(matching_domains),
trigger_type_(type),
password_field_exists_(password_field_exists),
password_protection_service_(pps),
request_timeout_in_ms_(request_timeout_in_ms),
request_proto_(std::make_unique<LoginReputationClientRequest>()),
is_modal_warning_showing_(false) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(trigger_type_ == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE ||
trigger_type_ == LoginReputationClientRequest::PASSWORD_REUSE_EVENT);
DCHECK(trigger_type_ != LoginReputationClientRequest::PASSWORD_REUSE_EVENT ||
password_type_ != PasswordType::SAVED_PASSWORD ||
matching_domains_.size() > 0);
request_proto_->set_trigger_type(trigger_type_);
}
PasswordProtectionRequest::~PasswordProtectionRequest() {
weakptr_factory_.InvalidateWeakPtrs();
}
void PasswordProtectionRequest::Start() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
CheckWhitelist();
}
void PasswordProtectionRequest::CheckWhitelist() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// In order to send pings for about:blank, we skip the whitelist check for
// URLs with unsupported schemes.
if (!password_protection_service_->database_manager()->CanCheckUrl(
main_frame_url_)) {
OnWhitelistCheckDone(false);
return;
}
// Start a task on the IO thread to check the whitelist. It may
// callback immediately on the IO thread or take some time if a full-hash-
// check is required.
auto result_callback = base::Bind(&OnWhitelistCheckDoneOnIO, GetWeakPtr());
tracker_.PostTask(
base::CreateSingleThreadTaskRunner({BrowserThread::IO}).get(), FROM_HERE,
base::BindOnce(&AllowlistCheckerClient::StartCheckCsdWhitelist,
password_protection_service_->database_manager(),
main_frame_url_, result_callback));
}
// static
void PasswordProtectionRequest::OnWhitelistCheckDoneOnIO(
base::WeakPtr<PasswordProtectionRequest> weak_request,
bool match_whitelist) {
// Don't access weak_request on IO thread. Move it back to UI thread first.
base::PostTask(
FROM_HERE, {BrowserThread::UI},
base::BindOnce(&PasswordProtectionRequest::OnWhitelistCheckDone,
weak_request, match_whitelist));
}
void PasswordProtectionRequest::OnWhitelistCheckDone(bool match_whitelist) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (match_whitelist) {
if (password_protection_service_->CanSendSamplePing()) {
FillRequestProto(/*is_sampled_ping=*/true);
}
Finish(RequestOutcome::MATCHED_WHITELIST, nullptr);
} else {
// In case the request to Safe Browsing takes too long,
// we set a timer to cancel that request and return an "unspecified verdict"
// so that the navigation isn't blocked indefinitely.
StartTimeout();
CheckCachedVerdicts();
}
}
void PasswordProtectionRequest::CheckCachedVerdicts() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!password_protection_service_) {
Finish(RequestOutcome::SERVICE_DESTROYED, nullptr);
return;
}
std::unique_ptr<LoginReputationClientResponse> cached_response =
std::make_unique<LoginReputationClientResponse>();
ReusedPasswordAccountType password_account_type =
password_protection_service_
->GetPasswordProtectionReusedPasswordAccountType(password_type_,
username_);
auto verdict = password_protection_service_->GetCachedVerdict(
main_frame_url_, trigger_type_, password_account_type,
cached_response.get());
if (verdict != LoginReputationClientResponse::VERDICT_TYPE_UNSPECIFIED) {
set_request_outcome(RequestOutcome::RESPONSE_ALREADY_CACHED);
Finish(RequestOutcome::RESPONSE_ALREADY_CACHED, std::move(cached_response));
} else {
FillRequestProto(/*is_sampled_ping=*/false);
}
}
void PasswordProtectionRequest::FillRequestProto(bool is_sampled_ping) {
request_proto_->set_page_url(main_frame_url_.spec());
LoginReputationClientRequest::Frame* main_frame =
request_proto_->add_frames();
main_frame->set_url(main_frame_url_.spec());
main_frame->set_frame_index(0 /* main frame */);
password_protection_service_->FillReferrerChain(
main_frame_url_, SessionID::InvalidValue(), main_frame);
// If a sample ping is send, only the URL and referrer chain is sent in the
// request.
if (is_sampled_ping) {
request_proto_->set_report_type(
LoginReputationClientRequest::SAMPLE_REPORT);
request_proto_->clear_trigger_type();
if (main_frame->referrer_chain_size() > 0) {
password_protection_service_->SanitizeReferrerChain(
main_frame->mutable_referrer_chain());
}
SendRequest();
return;
} else {
request_proto_->set_report_type(LoginReputationClientRequest::FULL_REPORT);
}
password_protection_service_->FillUserPopulation(trigger_type_,
request_proto_.get());
request_proto_->set_stored_verdict_cnt(
password_protection_service_->GetStoredVerdictCount(trigger_type_));
bool clicked_through_interstitial =
password_protection_service_->UserClickedThroughSBInterstitial(
web_contents_);
request_proto_->set_clicked_through_interstitial(
clicked_through_interstitial);
request_proto_->set_content_type(web_contents_->GetContentsMimeType());
#if BUILDFLAG(FULL_SAFE_BROWSING)
if (password_protection_service_->IsExtendedReporting() &&
!password_protection_service_->IsIncognito()) {
gfx::Size content_area_size =
password_protection_service_->GetCurrentContentAreaSize();
request_proto_->set_content_area_height(content_area_size.height());
request_proto_->set_content_area_width(content_area_size.width());
}
#endif
switch (trigger_type_) {
case LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE: {
LoginReputationClientRequest::Frame::Form* password_form;
if (password_form_frame_url_ == main_frame_url_) {
main_frame->set_has_password_field(true);
password_form = main_frame->add_forms();
} else {
LoginReputationClientRequest::Frame* password_frame =
request_proto_->add_frames();
password_frame->set_url(password_form_frame_url_.spec());
password_frame->set_has_password_field(true);
password_form = password_frame->add_forms();
}
password_form->set_action_url(password_form_action_.spec());
break;
}
case LoginReputationClientRequest::PASSWORD_REUSE_EVENT: {
main_frame->set_has_password_field(password_field_exists_);
LoginReputationClientRequest::PasswordReuseEvent* reuse_event =
request_proto_->mutable_password_reuse_event();
bool matches_signin_password =
password_type_ == PasswordType::PRIMARY_ACCOUNT_PASSWORD;
reuse_event->set_is_chrome_signin_password(matches_signin_password);
reuse_event->set_reused_password_type(
password_protection_service_->GetPasswordProtectionReusedPasswordType(
password_type_));
if (matches_signin_password) {
reuse_event->set_sync_account_type(
password_protection_service_->GetSyncAccountType());
LogSyncAccountType(reuse_event->sync_account_type());
}
if (password_protection_service_->IsExtendedReporting() &&
!password_protection_service_->IsIncognito()) {
for (const auto& domain : matching_domains_) {
reuse_event->add_domains_matching_password(domain);
if (reuse_event->domains_matching_password_size() >=
kMaxReusedDomains)
break;
}
}
if (base::FeatureList::IsEnabled(
safe_browsing::kPasswordProtectionForSignedInUsers)) {
ReusedPasswordAccountType password_account_type_to_add =
password_protection_service_
->GetPasswordProtectionReusedPasswordAccountType(password_type_,
username_);
*reuse_event->mutable_reused_password_account_type() =
password_account_type_to_add;
}
break;
}
default:
NOTREACHED();
}
#if BUILDFLAG(FULL_SAFE_BROWSING)
// Get the page DOM features.
content::RenderFrameHost* rfh = web_contents_->GetMainFrame();
password_protection_service_->GetPhishingDetector(rfh->GetRemoteInterfaces(),
&phishing_detector_);
dom_features_collection_complete_ = false;
phishing_detector_->StartPhishingDetection(
main_frame_url_,
base::BindRepeating(&PasswordProtectionRequest::OnGetDomFeatures,
GetWeakPtr()));
base::PostDelayedTask(
FROM_HERE, {BrowserThread::UI},
base::BindOnce(&PasswordProtectionRequest::OnGetDomFeatureTimeout,
GetWeakPtr()),
base::TimeDelta::FromMilliseconds(kDomFeatureTimeoutMs));
dom_feature_start_time_ = base::TimeTicks::Now();
#else
SendRequest();
#endif
}
#if BUILDFLAG(FULL_SAFE_BROWSING)
void PasswordProtectionRequest::OnGetDomFeatures(
mojom::PhishingDetectorResult result,
const std::string& verdict) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (dom_features_collection_complete_)
return;
UMA_HISTOGRAM_ENUMERATION("PasswordProtection.RendererDomFeatureResult",
result);
if (result != mojom::PhishingDetectorResult::SUCCESS &&
result != mojom::PhishingDetectorResult::INVALID_SCORE)
return;
dom_features_collection_complete_ = true;
ClientPhishingRequest dom_features_request;
if (dom_features_request.ParseFromString(verdict)) {
for (const ClientPhishingRequest::Feature& feature :
dom_features_request.feature_map()) {
DomFeatures::Feature* new_feature =
request_proto_->mutable_dom_features()->add_feature_map();
new_feature->set_name(feature.name());
new_feature->set_value(feature.value());
}
for (const ClientPhishingRequest::Feature& feature :
dom_features_request.non_model_feature_map()) {
DomFeatures::Feature* new_feature =
request_proto_->mutable_dom_features()->add_feature_map();
new_feature->set_name(feature.name());
new_feature->set_value(feature.value());
}
request_proto_->mutable_dom_features()->mutable_shingle_hashes()->Swap(
dom_features_request.mutable_shingle_hashes());
request_proto_->mutable_dom_features()->set_model_version(
dom_features_request.model_version());
}
UMA_HISTOGRAM_TIMES("PasswordProtection.DomFeatureExtractionDuration",
base::TimeTicks::Now() - dom_feature_start_time_);
MaybeCollectVisualFeatures();
}
void PasswordProtectionRequest::OnGetDomFeatureTimeout() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!dom_features_collection_complete_) {
dom_features_collection_complete_ = true;
MaybeCollectVisualFeatures();
}
}
void PasswordProtectionRequest::MaybeCollectVisualFeatures() {
// Once the DOM features are collected, either collect visual features, or go
// straight to sending the ping.
if (trigger_type_ == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE &&
password_protection_service_->IsExtendedReporting() &&
zoom::ZoomController::GetZoomLevelForWebContents(web_contents_) <=
kMaxZoomForVisualFeatures &&
request_proto_->content_area_width() >= kMinWidthForVisualFeatures &&
request_proto_->content_area_height() >= kMinHeightForVisualFeatures) {
CollectVisualFeatures();
} else {
SendRequest();
}
}
void PasswordProtectionRequest::CollectVisualFeatures() {
content::RenderWidgetHostView* view =
web_contents_ ? web_contents_->GetRenderWidgetHostView() : nullptr;
if (!view) {
SendRequest();
return;
}
visual_feature_start_time_ = base::TimeTicks::Now();
view->CopyFromSurface(
gfx::Rect(), gfx::Size(),
base::BindOnce(&PasswordProtectionRequest::OnScreenshotTaken,
GetWeakPtr()));
}
void PasswordProtectionRequest::OnScreenshotTaken(const SkBitmap& screenshot) {
// Do the feature extraction on a worker thread, to avoid blocking the UI.
base::PostTaskAndReplyWithResult(
FROM_HERE,
{base::ThreadPool(), base::MayBlock(), base::TaskPriority::BEST_EFFORT,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ExtractVisualFeatures, screenshot),
base::BindOnce(&PasswordProtectionRequest::OnVisualFeatureCollectionDone,
GetWeakPtr()));
}
void PasswordProtectionRequest::OnVisualFeatureCollectionDone(
std::unique_ptr<VisualFeatures> visual_features) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
request_proto_->mutable_visual_features()->Swap(visual_features.get());
UMA_HISTOGRAM_TIMES("PasswordProtection.VisualFeatureExtractionDuration",
base::TimeTicks::Now() - visual_feature_start_time_);
SendRequest();
}
#endif
void PasswordProtectionRequest::SendRequest() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
web_ui_token_ =
WebUIInfoSingleton::GetInstance()->AddToPGPings(*request_proto_);
std::string serialized_request;
if (!request_proto_->SerializeToString(&serialized_request)) {
Finish(RequestOutcome::REQUEST_MALFORMED, nullptr);
return;
}
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("password_protection_request", R"(
semantics {
sender: "Safe Browsing"
description:
"When the user is about to log in to a new, uncommon site, Chrome "
"will send a request to Safe Browsing to determine if the page is "
"phishing. It'll then show a warning if the page poses a risk of "
"phishing."
trigger:
"When a user focuses on a password field on a page that they "
"haven't visited before and that isn't popular or known to be safe."
data:
"URL and referrer of the current page, password form action, and "
"iframe structure."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: YES
cookies_store: "Safe Browsing Cookie Store"
setting:
"Users can control this feature via 'Protect you and your device "
"from dangerous sites'. By default, this setting is enabled."
"Alternatively, you can turn it off via "
"'PasswordProtectionWarningTrigger' enterprise policy setting."
chrome_policy {
PasswordProtectionWarningTrigger {
policy_options {mode: MANDATORY}
PasswordProtectionWarningTrigger: 2
}
}
})");
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url =
PasswordProtectionService::GetPasswordProtectionRequestUrl();
resource_request->method = "POST";
resource_request->load_flags = net::LOAD_DISABLE_CACHE;
url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
traffic_annotation);
url_loader_->AttachStringForUpload(serialized_request,
"application/octet-stream");
request_start_time_ = base::TimeTicks::Now();
url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
password_protection_service_->url_loader_factory().get(),
base::BindOnce(&PasswordProtectionRequest::OnURLLoaderComplete,
base::Unretained(this)));
}
void PasswordProtectionRequest::StartTimeout() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// If request is not done withing 10 seconds, we cancel this request.
// The weak pointer used for the timeout will be invalidated (and
// hence would prevent the timeout) if the check completes on time and
// execution reaches Finish().
base::PostDelayedTask(
FROM_HERE, {BrowserThread::UI},
base::BindOnce(&PasswordProtectionRequest::Cancel, GetWeakPtr(), true),
base::TimeDelta::FromMilliseconds(request_timeout_in_ms_));
}
void PasswordProtectionRequest::OnURLLoaderComplete(
std::unique_ptr<std::string> response_body) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
int response_code = 0;
if (url_loader_->ResponseInfo() && url_loader_->ResponseInfo()->headers)
response_code = url_loader_->ResponseInfo()->headers->response_code();
const bool is_success = url_loader_->NetError() == net::OK;
LogPasswordProtectionNetworkResponseAndDuration(
is_success ? response_code : url_loader_->NetError(),
request_start_time_);
if (!is_success || net::HTTP_OK != response_code) {
Finish(RequestOutcome::FETCH_FAILED, nullptr);
return;
}
std::unique_ptr<LoginReputationClientResponse> response =
std::make_unique<LoginReputationClientResponse>();
DCHECK(response_body);
url_loader_.reset(); // We don't need it anymore.
if (response_body && response->ParseFromString(*response_body)) {
WebUIInfoSingleton::GetInstance()->AddToPGResponses(web_ui_token_,
*response);
set_request_outcome(RequestOutcome::SUCCEEDED);
Finish(RequestOutcome::SUCCEEDED, std::move(response));
} else {
Finish(RequestOutcome::RESPONSE_MALFORMED, nullptr);
}
}
void PasswordProtectionRequest::Finish(
RequestOutcome outcome,
std::unique_ptr<LoginReputationClientResponse> response) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
tracker_.TryCancelAll();
// 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) {
ReusedPasswordAccountType password_account_type =
password_protection_service_
->GetPasswordProtectionReusedPasswordAccountType(password_type_,
username_);
if (trigger_type_ == LoginReputationClientRequest::UNFAMILIAR_LOGIN_PAGE) {
LogPasswordOnFocusRequestOutcome(outcome);
} else {
#if defined(SYNC_PASSWORD_REUSE_DETECTION_ENABLED)
LogPasswordEntryRequestOutcome(outcome, password_account_type);
#endif
#if defined(SYNC_PASSWORD_REUSE_WARNING_ENABLED)
if (password_type_ == PasswordType::PRIMARY_ACCOUNT_PASSWORD) {
password_protection_service_->MaybeLogPasswordReuseLookupEvent(
web_contents_, outcome, password_type_, response.get());
}
#endif
}
if (outcome == RequestOutcome::SUCCEEDED && response) {
LogPasswordProtectionVerdict(trigger_type_, password_account_type,
response->verdict_type());
}
}
password_protection_service_->RequestFinished(this, outcome,
std::move(response));
}
void PasswordProtectionRequest::Cancel(bool timed_out) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
url_loader_.reset();
// If request is canceled because |password_protection_service_| is shutting
// down, ignore all these deferred navigations.
if (!timed_out) {
throttles_.clear();
}
Finish(timed_out ? RequestOutcome::TIMEDOUT : RequestOutcome::CANCELED,
nullptr);
}
void PasswordProtectionRequest::HandleDeferredNavigations() {
for (auto* throttle : throttles_) {
if (is_modal_warning_showing_)
throttle->CancelNavigation(content::NavigationThrottle::CANCEL);
else
throttle->ResumeNavigation();
}
throttles_.clear();
}
void PasswordProtectionRequest::WebContentsDestroyed() {
Cancel(/*timed_out=*/false);
}
} // namespace safe_browsing