blob: e6422d169839673e228f589ced362baca7550a99 [file] [log] [blame]
// Copyright 2018 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/triggers/suspicious_site_trigger.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/histogram_macros_local.h"
#include "base/task/sequenced_task_runner.h"
#include "components/history/core/browser/history_service.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/content/browser/content_unsafe_resource_util.h"
#include "components/safe_browsing/content/browser/triggers/trigger_manager.h"
#include "components/safe_browsing/content/browser/triggers/trigger_throttler.h"
#include "components/safe_browsing/content/browser/web_contents_key.h"
#include "components/safe_browsing/core/browser/referrer_chain_provider.h"
#include "components/security_interstitials/core/unsafe_resource.h"
#include "components/security_interstitials/core/unsafe_resource_locator.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/page.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace safe_browsing {
namespace {
// Number of milliseconds to allow data collection to run before sending a
// report (since this trigger runs in the background).
const int64_t kSuspiciousSiteCollectionPeriodMilliseconds = 5000;
} // namespace
const char kSuspiciousSiteTriggerEventMetricName[] =
"SafeBrowsing.Triggers.SuspiciousSite.Event";
const char kSuspiciousSiteTriggerReportRejectionTestMetricName[] =
"SafeBrowsing.Triggers.SuspiciousSite.ReportRejectionReason";
const char kSuspiciousSiteTriggerReportDelayStateTestMetricName[] =
"SafeBrowsing.Triggers.SuspiciousSite.DelayTimerState";
void NotifySuspiciousSiteTriggerDetected(
const base::RepeatingCallback<content::WebContents*()>&
web_contents_getter) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
content::WebContents* web_contents = web_contents_getter.Run();
if (web_contents) {
safe_browsing::SuspiciousSiteTrigger* trigger =
safe_browsing::SuspiciousSiteTrigger::FromWebContents(web_contents);
if (trigger)
trigger->SuspiciousSiteDetected();
}
}
SuspiciousSiteTrigger::SuspiciousSiteTrigger(
content::WebContents* web_contents,
TriggerManager* trigger_manager,
PrefService* prefs,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
history::HistoryService* history_service,
ReferrerChainProvider* referrer_chain_provider,
bool monitor_mode)
: content::WebContentsObserver(web_contents),
content::WebContentsUserData<SuspiciousSiteTrigger>(*web_contents),
finish_report_delay_ms_(kSuspiciousSiteCollectionPeriodMilliseconds),
current_state_(monitor_mode ? TriggerState::MONITOR_MODE
: TriggerState::IDLE),
trigger_manager_(trigger_manager),
prefs_(prefs),
url_loader_factory_(url_loader_factory),
history_service_(history_service),
referrer_chain_provider_(referrer_chain_provider),
task_runner_(content::GetUIThreadTaskRunner({})) {}
SuspiciousSiteTrigger::~SuspiciousSiteTrigger() = default;
bool SuspiciousSiteTrigger::MaybeStartReport() {
TriggerManager::DataCollectionPermissions permissions =
TriggerManager::GetDataCollectionPermissions(*prefs_, web_contents());
// We use primary page's main document to construct `resource` below, since
// `WebContents::StartLoading()` is invoked for navigations which updates it.
// For more details, refer to the below document:
// https://docs.google.com/document/d/1WqzPSpWtJ9bqaaecYWTWvg6h2Q1UMh7kVB8C61o8QL4/edit?resourcekey=0-q9YIhsh5LS-ZfBXcBQCOew#heading=h.b3xiyzalkc6q
content::RenderFrameHost& primary_rfh =
web_contents()->GetPrimaryPage().GetMainDocument();
const content::GlobalRenderFrameHostId primary_rfh_id =
primary_rfh.GetGlobalId();
security_interstitials::UnsafeResource resource;
resource.threat_type = SBThreatType::SB_THREAT_TYPE_SUSPICIOUS_SITE;
resource.url = primary_rfh.GetLastCommittedURL();
resource.rfh_locator =
security_interstitials::UnsafeResourceLocator::CreateForRenderFrameToken(
primary_rfh_id.child_id, primary_rfh.GetFrameToken().value());
TriggerManagerReason reason;
if (!trigger_manager_->StartCollectingThreatDetailsWithReason(
TriggerType::SUSPICIOUS_SITE, web_contents(), resource,
url_loader_factory_, history_service_, referrer_chain_provider_,
permissions, &reason)) {
UMA_HISTOGRAM_ENUMERATION(kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::REPORT_START_FAILED);
UMA_HISTOGRAM_ENUMERATION(
kSuspiciousSiteTriggerReportRejectionTestMetricName, reason);
return false;
}
// Call back into the trigger after a short delay, allowing the report
// to complete.
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&SuspiciousSiteTrigger::ReportDelayTimerFired,
weak_ptr_factory_.GetWeakPtr()),
base::Milliseconds(finish_report_delay_ms_));
UMA_HISTOGRAM_ENUMERATION(kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::REPORT_STARTED);
return true;
}
void SuspiciousSiteTrigger::FinishReport() {
TriggerManager::DataCollectionPermissions permissions =
TriggerManager::GetDataCollectionPermissions(*prefs_, web_contents());
auto result = trigger_manager_->FinishCollectingThreatDetails(
TriggerType::SUSPICIOUS_SITE, GetWebContentsKey(web_contents()),
base::TimeDelta(),
/*did_proceed=*/false, /*num_visits=*/0, permissions);
if (result.IsReportSent()) {
UMA_HISTOGRAM_ENUMERATION(kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::REPORT_FINISHED);
} else {
UMA_HISTOGRAM_ENUMERATION(kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::REPORT_FINISH_FAILED);
}
}
void SuspiciousSiteTrigger::SuspiciousSiteDetectedWhenMonitoring() {
DCHECK_EQ(TriggerState::MONITOR_MODE, current_state_);
TriggerManager::DataCollectionPermissions permissions =
TriggerManager::GetDataCollectionPermissions(*prefs_, web_contents());
TriggerManagerReason reason;
if (trigger_manager_->CanStartDataCollectionWithReason(
permissions, TriggerType::SUSPICIOUS_SITE, &reason) ||
reason == TriggerManagerReason::DAILY_QUOTA_EXCEEDED) {
UMA_HISTOGRAM_ENUMERATION(
kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::REPORT_POSSIBLE_BUT_SKIPPED);
}
}
void SuspiciousSiteTrigger::DidStartLoading() {
UMA_HISTOGRAM_ENUMERATION(kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::PAGE_LOAD_START);
switch (current_state_) {
case TriggerState::IDLE:
// Load started, move to loading state.
current_state_ = TriggerState::LOADING;
return;
case TriggerState::LOADING:
// No-op, still loading.
return;
case TriggerState::LOADING_WILL_REPORT:
// This happens if the user leaves the suspicious page before it
// finishes loading. A report can't be created in this case since the
// page is now gone.
UMA_HISTOGRAM_ENUMERATION(
kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::PENDING_REPORT_CANCELLED_BY_LOAD);
current_state_ = TriggerState::LOADING;
return;
case TriggerState::REPORT_STARTED:
// A new page load has started while creating the current report.
// Finish the report immediately with whatever data has been captured
// so far. A report timer will have already started, but it will be
// ignored when it fires.
current_state_ = TriggerState::LOADING;
FinishReport();
return;
case TriggerState::MONITOR_MODE:
// No-op, monitoring only.
return;
}
}
void SuspiciousSiteTrigger::DidStopLoading() {
UMA_HISTOGRAM_ENUMERATION(kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::PAGE_LOAD_FINISH);
switch (current_state_) {
case TriggerState::IDLE:
// No-op, load stopped and we're already idle.
return;
case TriggerState::LOADING:
// Load finished, return to Idle state.
current_state_ = TriggerState::IDLE;
return;
case TriggerState::LOADING_WILL_REPORT:
// Suspicious site detected mid-load and the page has now
// finished loading, so try starting a report now.
// If we fail to start a report for whatever reason, return to Idle.
if (MaybeStartReport()) {
current_state_ = TriggerState::REPORT_STARTED;
} else {
current_state_ = TriggerState::IDLE;
}
return;
case TriggerState::REPORT_STARTED:
// No-op. Let the report continue running.
return;
case TriggerState::MONITOR_MODE:
// No-op, monitoring only.
return;
}
}
void SuspiciousSiteTrigger::SuspiciousSiteDetected() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
UMA_HISTOGRAM_ENUMERATION(
kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::SUSPICIOUS_SITE_DETECTED);
switch (current_state_) {
case TriggerState::IDLE:
// Suspicious site detected while idle, start a report immediately.
// If we fail to start a report for whatever reason, remain Idle.
if (MaybeStartReport()) {
current_state_ = TriggerState::REPORT_STARTED;
}
return;
case TriggerState::LOADING:
// Suspicious site detected in the middle of the load, remember this
// and let the page finish loading. The report will be started after
// the page has loaded.
current_state_ = TriggerState::LOADING_WILL_REPORT;
return;
case TriggerState::LOADING_WILL_REPORT:
// No-op. Current page has multiple suspicious URLs in it, remain in
// the LOADING_WILL_REPORT state. A report will begin when the page
// finishes loading.
return;
case TriggerState::REPORT_STARTED:
// No-op. The current report should capture all suspicious sites.
return;
case TriggerState::MONITOR_MODE:
// We monitor how often a suspicious site hit could result in a report.
SuspiciousSiteDetectedWhenMonitoring();
return;
}
}
void SuspiciousSiteTrigger::ReportDelayTimerFired() {
UMA_HISTOGRAM_ENUMERATION(kSuspiciousSiteTriggerEventMetricName,
SuspiciousSiteTriggerEvent::REPORT_DELAY_TIMER);
// This local histogram is used as a signal for testing.
UMA_HISTOGRAM_ENUMERATION(
kSuspiciousSiteTriggerReportDelayStateTestMetricName, current_state_);
switch (current_state_) {
case TriggerState::IDLE:
case TriggerState::LOADING:
case TriggerState::LOADING_WILL_REPORT:
case TriggerState::MONITOR_MODE:
// Invalid, expecting to be in REPORT_STARTED state.
return;
case TriggerState::REPORT_STARTED:
// The delay timer has fired so complete the current report.
current_state_ = TriggerState::IDLE;
FinishReport();
return;
}
}
void SuspiciousSiteTrigger::SetTaskRunnerForTest(
scoped_refptr<base::SequencedTaskRunner> task_runner) {
task_runner_ = task_runner;
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(SuspiciousSiteTrigger);
} // namespace safe_browsing