blob: da22ca6b31bede9924f8367dd16ac16a9f8a600c [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/browser/safe_browsing_url_checker_impl.h"
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/histogram_macros_local.h"
#include "base/task/post_task.h"
#include "base/trace_event/trace_event.h"
#include "components/safe_browsing/browser/url_checker_delegate.h"
#include "components/safe_browsing/realtime/policy_engine.h"
#include "components/safe_browsing/realtime/url_lookup_service.h"
#include "components/safe_browsing/web_ui/constants.h"
#include "components/security_interstitials/content/unsafe_resource.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "net/log/net_log_event_type.h"
namespace safe_browsing {
namespace {
// Maximum time in milliseconds to wait for the SafeBrowsing service reputation
// check. After this amount of time the outstanding check will be aborted, and
// the resource will be treated as if it were safe.
const int kCheckUrlTimeoutMs = 5000;
void RecordCheckUrlTimeout(bool timed_out) {
UMA_HISTOGRAM_BOOLEAN("SafeBrowsing.CheckUrl.Timeout", timed_out);
}
} // namespace
SafeBrowsingUrlCheckerImpl::Notifier::Notifier(CheckUrlCallback callback)
: callback_(std::move(callback)) {}
SafeBrowsingUrlCheckerImpl::Notifier::Notifier(
NativeCheckUrlCallback native_callback)
: native_callback_(std::move(native_callback)) {}
SafeBrowsingUrlCheckerImpl::Notifier::~Notifier() = default;
SafeBrowsingUrlCheckerImpl::Notifier::Notifier(Notifier&& other) = default;
SafeBrowsingUrlCheckerImpl::Notifier& SafeBrowsingUrlCheckerImpl::Notifier::
operator=(Notifier&& other) = default;
void SafeBrowsingUrlCheckerImpl::Notifier::OnStartSlowCheck() {
if (callback_) {
std::move(callback_).Run(slow_check_notifier_.BindNewPipeAndPassReceiver(),
false, false);
return;
}
DCHECK(native_callback_);
std::move(native_callback_).Run(&native_slow_check_notifier_, false, false);
}
void SafeBrowsingUrlCheckerImpl::Notifier::OnCompleteCheck(
bool proceed,
bool showed_interstitial) {
if (callback_) {
std::move(callback_).Run(mojo::NullReceiver(), proceed,
showed_interstitial);
return;
}
if (native_callback_) {
std::move(native_callback_).Run(nullptr, proceed, showed_interstitial);
return;
}
if (slow_check_notifier_) {
slow_check_notifier_->OnCompleteCheck(proceed, showed_interstitial);
slow_check_notifier_.reset();
return;
}
std::move(native_slow_check_notifier_).Run(proceed, showed_interstitial);
}
SafeBrowsingUrlCheckerImpl::UrlInfo::UrlInfo(const GURL& in_url,
const std::string& in_method,
Notifier in_notifier)
: url(in_url), method(in_method), notifier(std::move(in_notifier)) {}
SafeBrowsingUrlCheckerImpl::UrlInfo::UrlInfo(UrlInfo&& other) = default;
SafeBrowsingUrlCheckerImpl::UrlInfo::~UrlInfo() = default;
SafeBrowsingUrlCheckerImpl::SafeBrowsingUrlCheckerImpl(
const net::HttpRequestHeaders& headers,
int load_flags,
content::ResourceType resource_type,
bool has_user_gesture,
scoped_refptr<UrlCheckerDelegate> url_checker_delegate,
const base::Callback<content::WebContents*()>& web_contents_getter,
bool real_time_lookup_enabled)
: headers_(headers),
load_flags_(load_flags),
resource_type_(resource_type),
has_user_gesture_(has_user_gesture),
web_contents_getter_(web_contents_getter),
url_checker_delegate_(std::move(url_checker_delegate)),
database_manager_(url_checker_delegate_->GetDatabaseManager()),
real_time_lookup_enabled_(real_time_lookup_enabled) {}
SafeBrowsingUrlCheckerImpl::~SafeBrowsingUrlCheckerImpl() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
if (state_ == STATE_CHECKING_URL) {
database_manager_->CancelCheck(this);
TRACE_EVENT_ASYNC_END1("safe_browsing", "CheckUrl", this, "result",
"request_canceled");
}
}
void SafeBrowsingUrlCheckerImpl::CheckUrl(const GURL& url,
const std::string& method,
CheckUrlCallback callback) {
CheckUrlImpl(url, method, Notifier(std::move(callback)));
}
void SafeBrowsingUrlCheckerImpl::CheckUrl(const GURL& url,
const std::string& method,
NativeCheckUrlCallback callback) {
CheckUrlImpl(url, method, Notifier(std::move(callback)));
}
void SafeBrowsingUrlCheckerImpl::OnCheckBrowseUrlResult(
const GURL& url,
SBThreatType threat_type,
const ThreatMetadata& metadata) {
OnUrlResult(url, threat_type, metadata);
}
void SafeBrowsingUrlCheckerImpl::OnUrlResult(const GURL& url,
SBThreatType threat_type,
const ThreatMetadata& metadata) {
DCHECK_EQ(STATE_CHECKING_URL, state_);
DCHECK_LT(next_index_, urls_.size());
DCHECK_EQ(urls_[next_index_].url, url);
timer_.Stop();
RecordCheckUrlTimeout(/*timed_out=*/false);
TRACE_EVENT_ASYNC_END1(
"safe_browsing", "CheckUrl", this, "result",
threat_type == SB_THREAT_TYPE_SAFE ? "safe" : "unsafe");
if (threat_type == SB_THREAT_TYPE_SAFE ||
threat_type == SB_THREAT_TYPE_SUSPICIOUS_SITE) {
state_ = STATE_NONE;
if (threat_type == SB_THREAT_TYPE_SUSPICIOUS_SITE) {
url_checker_delegate_->NotifySuspiciousSiteDetected(web_contents_getter_);
}
if (!RunNextCallback(true, false))
return;
ProcessUrls();
return;
}
if (load_flags_ & net::LOAD_PREFETCH) {
// Destroy the prefetch with FINAL_STATUS_SAFEBROSWING.
if (resource_type_ == content::ResourceType::kMainFrame) {
url_checker_delegate_->MaybeDestroyPrerenderContents(
web_contents_getter_);
}
// Record the result of canceled unsafe prefetch. This is used as a signal
// for testing.
LOCAL_HISTOGRAM_ENUMERATION("SB2Test.ResourceTypes2.UnsafePrefetchCanceled",
resource_type_);
BlockAndProcessUrls(false);
return;
}
UMA_HISTOGRAM_ENUMERATION("SB2.ResourceTypes2.Unsafe", resource_type_);
security_interstitials::UnsafeResource resource;
resource.url = url;
resource.original_url = urls_[0].url;
if (urls_.size() > 1) {
resource.redirect_urls.reserve(urls_.size() - 1);
for (size_t i = 1; i < urls_.size(); ++i)
resource.redirect_urls.push_back(urls_[i].url);
}
resource.is_subresource = resource_type_ != content::ResourceType::kMainFrame;
resource.is_subframe = resource_type_ == content::ResourceType::kSubFrame;
resource.threat_type = threat_type;
resource.threat_metadata = metadata;
resource.callback =
base::Bind(&SafeBrowsingUrlCheckerImpl::OnBlockingPageComplete,
weak_factory_.GetWeakPtr());
resource.callback_thread =
base::CreateSingleThreadTaskRunner({content::BrowserThread::IO});
resource.web_contents_getter = web_contents_getter_;
resource.threat_source = database_manager_->GetThreatSource();
state_ = STATE_DISPLAYING_BLOCKING_PAGE;
url_checker_delegate_->StartDisplayingBlockingPageHelper(
resource, urls_[next_index_].method, headers_,
resource_type_ == content::ResourceType::kMainFrame, has_user_gesture_);
}
void SafeBrowsingUrlCheckerImpl::OnTimeout() {
RecordCheckUrlTimeout(/*timed_out=*/true);
database_manager_->CancelCheck(this);
OnUrlResult(urls_[next_index_].url, safe_browsing::SB_THREAT_TYPE_SAFE,
ThreatMetadata());
}
void SafeBrowsingUrlCheckerImpl::CheckUrlImpl(const GURL& url,
const std::string& method,
Notifier notifier) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
DVLOG(1) << "SafeBrowsingUrlCheckerImpl checks URL: " << url;
urls_.emplace_back(url, method, std::move(notifier));
ProcessUrls();
}
void SafeBrowsingUrlCheckerImpl::ProcessUrls() {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
DCHECK_NE(STATE_BLOCKED, state_);
if (state_ == STATE_CHECKING_URL ||
state_ == STATE_DISPLAYING_BLOCKING_PAGE) {
return;
}
while (next_index_ < urls_.size()) {
DCHECK_EQ(STATE_NONE, state_);
const GURL& url = urls_[next_index_].url;
if (url_checker_delegate_->IsUrlWhitelisted(url)) {
if (!RunNextCallback(true, false))
return;
continue;
}
// TODO(yzshen): Consider moving CanCheckResourceType() to the renderer
// side. That would save some IPCs. It requires a method on the
// SafeBrowsing mojo interface to query all supported resource types.
if (!database_manager_->CanCheckResourceType(resource_type_)) {
// TODO(vakh): Consider changing this metric to
// SafeBrowsing.V4ResourceType to be consistent with the other PVer4
// metrics.
UMA_HISTOGRAM_ENUMERATION("SB2.ResourceTypes2.Skipped", resource_type_);
if (!RunNextCallback(true, false))
return;
continue;
}
// TODO(vakh): Consider changing this metric to SafeBrowsing.V4ResourceType
// to be consistent with the other PVer4 metrics.
UMA_HISTOGRAM_ENUMERATION("SB2.ResourceTypes2.Checked", resource_type_);
SBThreatType threat_type = CheckWebUIUrls(url);
if (threat_type != safe_browsing::SB_THREAT_TYPE_SAFE) {
state_ = STATE_CHECKING_URL;
TRACE_EVENT_ASYNC_BEGIN1("safe_browsing", "CheckUrl", this, "url",
url.spec());
base::PostTask(
FROM_HERE, {content::BrowserThread::IO},
base::BindOnce(&SafeBrowsingUrlCheckerImpl::OnCheckBrowseUrlResult,
weak_factory_.GetWeakPtr(), url, threat_type,
ThreatMetadata()));
break;
}
TRACE_EVENT_ASYNC_BEGIN1("safe_browsing", "CheckUrl", this, "url",
url.spec());
// Start a timer to abort the check if it takes too long.
timer_.Start(FROM_HERE,
base::TimeDelta::FromMilliseconds(kCheckUrlTimeoutMs), this,
&SafeBrowsingUrlCheckerImpl::OnTimeout);
bool safe_synchronously;
auto* rt_lookup_service = database_manager_->GetRealTimeUrlLookupService();
if (real_time_lookup_enabled_ &&
RealTimePolicyEngine::CanPerformFullURLLookupForResourceType(
resource_type_) &&
rt_lookup_service && rt_lookup_service->CanCheckUrl(url) &&
!rt_lookup_service->IsInBackoffMode()) {
UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.RT.ResourceTypes.Checked",
resource_type_);
safe_synchronously = false;
AsyncMatch match =
database_manager_->CheckUrlForHighConfidenceAllowlist(url, this);
switch (match) {
case AsyncMatch::ASYNC:
// Hash-prefix matched. A call to
// |OnCheckUrlForHighConfidenceAllowlist| will follow.
break;
case AsyncMatch::MATCH:
// Full-hash matched locally so queue a call to
// |OnCheckUrlForHighConfidenceAllowlist| to trigger the hash-based
// checking.
base::PostTask(
FROM_HERE, {content::BrowserThread::IO},
base::BindOnce(&SafeBrowsingUrlCheckerImpl::
OnCheckUrlForHighConfidenceAllowlist,
weak_factory_.GetWeakPtr(),
/*did_match_allowlist=*/true));
break;
case AsyncMatch::NO_MATCH:
// No match found locally. Queue the call to
// |OnCheckUrlForHighConfidenceAllowlist| to perform the full URL
// lookup.
base::PostTask(
FROM_HERE, {content::BrowserThread::IO},
base::BindOnce(&SafeBrowsingUrlCheckerImpl::
OnCheckUrlForHighConfidenceAllowlist,
weak_factory_.GetWeakPtr(),
/*did_match_allowlist=*/false));
break;
}
} else {
safe_synchronously = database_manager_->CheckBrowseUrl(
url, url_checker_delegate_->GetThreatTypes(), this);
}
if (safe_synchronously) {
timer_.Stop();
RecordCheckUrlTimeout(/*timed_out=*/false);
if (!RunNextCallback(true, false))
return;
continue;
}
state_ = STATE_CHECKING_URL;
// Only send out notification of starting a slow check if the database
// manager actually supports fast checks (i.e., synchronous checks) but is
// not able to complete the check synchronously in this case.
// Don't send out notification if the database manager doesn't support
// synchronous checks at all (e.g., on mobile).
if (!database_manager_->ChecksAreAlwaysAsync())
urls_[next_index_].notifier.OnStartSlowCheck();
break;
}
}
void SafeBrowsingUrlCheckerImpl::BlockAndProcessUrls(bool showed_interstitial) {
DVLOG(1) << "SafeBrowsingUrlCheckerImpl blocks URL: "
<< urls_[next_index_].url;
state_ = STATE_BLOCKED;
// If user decided to not proceed through a warning, mark all the remaining
// redirects as "bad".
while (next_index_ < urls_.size()) {
if (!RunNextCallback(false, showed_interstitial))
return;
}
}
void SafeBrowsingUrlCheckerImpl::OnBlockingPageComplete(bool proceed) {
DCHECK_EQ(STATE_DISPLAYING_BLOCKING_PAGE, state_);
if (proceed) {
state_ = STATE_NONE;
if (!RunNextCallback(true, true))
return;
ProcessUrls();
} else {
BlockAndProcessUrls(true);
}
}
SBThreatType SafeBrowsingUrlCheckerImpl::CheckWebUIUrls(const GURL& url) {
if (url == kChromeUISafeBrowsingMatchMalwareUrl)
return safe_browsing::SB_THREAT_TYPE_URL_MALWARE;
if (url == kChromeUISafeBrowsingMatchPhishingUrl)
return safe_browsing::SB_THREAT_TYPE_URL_PHISHING;
if (url == kChromeUISafeBrowsingMatchUnwantedUrl)
return safe_browsing::SB_THREAT_TYPE_URL_UNWANTED;
if (url == kChromeUISafeBrowsingMatchBillingUrl)
return safe_browsing::SB_THREAT_TYPE_BILLING;
return safe_browsing::SB_THREAT_TYPE_SAFE;
}
bool SafeBrowsingUrlCheckerImpl::RunNextCallback(bool proceed,
bool showed_interstitial) {
DCHECK_LT(next_index_, urls_.size());
auto weak_self = weak_factory_.GetWeakPtr();
urls_[next_index_++].notifier.OnCompleteCheck(proceed, showed_interstitial);
return !!weak_self;
}
void SafeBrowsingUrlCheckerImpl::OnCheckUrlForHighConfidenceAllowlist(
bool did_match_allowlist) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
DCHECK_EQ(content::ResourceType::kMainFrame, resource_type_);
const GURL& url = urls_[next_index_].url;
if (did_match_allowlist) {
// If the URL matches the high-confidence allowlist, still do the hash based
// checks.
if (database_manager_->CheckBrowseUrl(
url, url_checker_delegate_->GetThreatTypes(), this)) {
// No match found in the local database. Safe to call |OnUrlResult| here
// directly.
OnUrlResult(url, SB_THREAT_TYPE_SAFE, ThreatMetadata());
}
return;
}
RTLookupResponseCallback result_callback =
base::Bind(&SafeBrowsingUrlCheckerImpl::OnRTLookupResponse,
weak_factory_.GetWeakPtr());
auto* rt_lookup_service = database_manager_->GetRealTimeUrlLookupService();
rt_lookup_service->StartLookup(url, std::move(result_callback));
}
void SafeBrowsingUrlCheckerImpl::OnRTLookupResponse(
std::unique_ptr<RTLookupResponse> response) {
DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
DCHECK_EQ(content::ResourceType::kMainFrame, resource_type_);
const GURL& url = urls_[next_index_].url;
if (response && (response->threat_info_size() > 0) &&
(response->threat_info(0).verdict_type() ==
RTLookupResponse::ThreatInfo::DANGEROUS)) {
OnUrlResult(url,
RealTimeUrlLookupService::GetSBThreatTypeForRTThreatType(
response->threat_info(0).threat_type()),
ThreatMetadata());
} else {
OnUrlResult(url, SB_THREAT_TYPE_SAFE, ThreatMetadata());
}
// TODO(vakh): Log |response| to chrome://safe-browsing
}
} // namespace safe_browsing