blob: ba5c020dba6e326eb7a0f589b41ae1b19a4d1c1a [file] [log] [blame]
// Copyright 2017 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/browser_url_loader_throttle.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/trace_event/trace_event.h"
#include "components/safe_browsing/buildflags.h"
#include "components/safe_browsing/content/browser/async_check_tracker.h"
#include "components/safe_browsing/core/browser/hashprefix_realtime/hash_realtime_service.h"
#include "components/safe_browsing/core/browser/realtime/url_lookup_service_base.h"
#include "components/safe_browsing/core/browser/safe_browsing_url_checker_impl.h"
#include "components/safe_browsing/core/browser/url_checker_delegate.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/utils.h"
#include "components/safe_browsing/core/common/web_ui_constants.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "net/base/load_flags.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"
namespace {
constexpr char kFullURLLookup[] = "FullUrlLookup";
constexpr char kFromCacheUmaSuffix[] = ".FromCache";
constexpr char kFromNetworkUmaSuffix[] = ".FromNetwork";
void LogTotalDelay2Metrics(const std::string& url_check_type,
base::TimeDelta total_delay) {
base::UmaHistogramTimes(
base::StrCat(
{"SafeBrowsing.BrowserThrottle.TotalDelay2", url_check_type}),
total_delay);
}
void LogTotalDelay2MetricsWithResponseType(bool is_response_from_cache,
base::TimeDelta total_delay) {
base::UmaHistogramTimes(
base::StrCat({"SafeBrowsing.BrowserThrottle.TotalDelay2",
is_response_from_cache ? kFromCacheUmaSuffix
: kFromNetworkUmaSuffix}),
total_delay);
}
// Returns true if the URL is known to be safe. We also require that this URL
// never redirects to a potentially unsafe URL, because the redirected URLs are
// also skipped if this function returns true.
bool KnownSafeUrl(const GURL& url) {
return url.SchemeIs(content::kChromeUIScheme) &&
!safe_browsing::IsSafeBrowsingWebUIUrl(url);
}
} // namespace
namespace safe_browsing {
BrowserURLLoaderThrottle::SkipCheckChecker::SkipCheckChecker(
UrlCheckerHolder::GetDelegateCallback delegate_getter,
content::FrameTreeNodeId frame_tree_node_id)
: delegate_getter_(std::move(delegate_getter)),
frame_tree_node_id_(frame_tree_node_id) {}
BrowserURLLoaderThrottle::SkipCheckChecker::~SkipCheckChecker() = default;
void BrowserURLLoaderThrottle::SkipCheckChecker::CheckOriginalUrl(
OnCompleteCheckCallback callback,
const GURL& url,
bool originated_from_service_worker) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
scoped_refptr<UrlCheckerDelegate> url_checker_delegate =
std::move(delegate_getter_).Run();
should_skip_checks_ =
!url_checker_delegate ||
url_checker_delegate->ShouldSkipRequestCheck(
url, frame_tree_node_id_.value(),
/*render_process_id=*/content::ChildProcessHost::kInvalidUniqueID,
/*render_frame_token=*/std::nullopt, originated_from_service_worker);
std::move(callback).Run(should_skip_checks_);
}
void BrowserURLLoaderThrottle::SkipCheckChecker::CheckRedirectUrl(
OnCompleteCheckCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::move(callback).Run(should_skip_checks_);
}
// static
std::unique_ptr<BrowserURLLoaderThrottle> BrowserURLLoaderThrottle::Create(
UrlCheckerHolder::GetDelegateCallback delegate_getter,
const base::RepeatingCallback<content::WebContents*()>& web_contents_getter,
content::FrameTreeNodeId frame_tree_node_id,
std::optional<int64_t> navigation_id,
base::WeakPtr<RealTimeUrlLookupServiceBase> url_lookup_service,
base::WeakPtr<HashRealTimeService> hash_realtime_service,
hash_realtime_utils::HashRealTimeSelection hash_realtime_selection,
base::WeakPtr<AsyncCheckTracker> async_check_tracker,
std::optional<internal::ReferringAppInfo> referring_app_info) {
return base::WrapUnique<BrowserURLLoaderThrottle>(
new BrowserURLLoaderThrottle(
std::move(delegate_getter), web_contents_getter, frame_tree_node_id,
navigation_id, url_lookup_service, hash_realtime_service,
hash_realtime_selection, async_check_tracker,
std::move(referring_app_info)));
}
BrowserURLLoaderThrottle::BrowserURLLoaderThrottle(
UrlCheckerHolder::GetDelegateCallback delegate_getter,
const base::RepeatingCallback<content::WebContents*()>& web_contents_getter,
content::FrameTreeNodeId frame_tree_node_id,
std::optional<int64_t> navigation_id,
base::WeakPtr<RealTimeUrlLookupServiceBase> url_lookup_service,
base::WeakPtr<HashRealTimeService> hash_realtime_service,
hash_realtime_utils::HashRealTimeSelection hash_realtime_selection,
base::WeakPtr<AsyncCheckTracker> async_check_tracker,
std::optional<internal::ReferringAppInfo> referring_app_info)
: async_check_tracker_(async_check_tracker),
url_lookup_service_(url_lookup_service),
hash_realtime_service_(hash_realtime_service),
hash_realtime_selection_(hash_realtime_selection),
frame_tree_node_id_(frame_tree_node_id),
navigation_id_(navigation_id),
delegate_getter_(delegate_getter),
web_contents_getter_(web_contents_getter),
referring_app_info_(referring_app_info) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Decide whether to do real time URL lookups or not.
url_real_time_lookup_enabled_ =
url_lookup_service_ ? url_lookup_service_->CanPerformFullURLLookup()
: false;
url_lookup_service_metric_suffix_ =
url_real_time_lookup_enabled_ ? url_lookup_service_->GetMetricSuffix()
: kNoRealTimeURLLookupService;
content::WebContents* web_contents = web_contents_getter_.Run();
tab_id_ = web_contents ? sessions::SessionTabHelper::IdForTab(web_contents)
: SessionID::InvalidValue();
skip_check_checker_ =
std::make_unique<SkipCheckChecker>(delegate_getter_, frame_tree_node_id);
}
BrowserURLLoaderThrottle::~BrowserURLLoaderThrottle() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (deferred_) {
TRACE_EVENT_END("safe_browsing", /* Deferred */
perfetto::Track::FromPointer(this));
}
if (was_async_faster_than_sync_.has_value()) {
base::UmaHistogramBoolean(
"SafeBrowsing.BrowserThrottle.IsAsyncCheckFasterThanSyncCheck",
was_async_faster_than_sync_.value());
}
DeleteUrlChecker();
}
void BrowserURLLoaderThrottle::WillStartRequest(
network::ResourceRequest* request,
bool* defer) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK_EQ(0u, pending_sync_checks_);
DCHECK_EQ(0u, pending_async_checks_);
DCHECK(!blocked_);
base::UmaHistogramEnumeration(
"SafeBrowsing.BrowserThrottle.RequestDestination", request->destination);
// Decision override used in enterprise mode to send safe urls for check
// since admins can ban chrome:// pages
bool should_override_known_safe_decision =
url_real_time_lookup_enabled_ &&
url_lookup_service_->ShouldOverrideKnownSafeUrlDecision(request->url);
if (KnownSafeUrl(request->url) && !should_override_known_safe_decision) {
skip_checks_ = true;
return;
}
if (request->destination != network::mojom::RequestDestination::kDocument) {
VLOG(2) << __func__ << " : Skipping: " << request->url << " : "
<< request->destination;
base::UmaHistogramEnumeration(
"SafeBrowsing.BrowserThrottle.RequestDestination.Skipped",
request->destination);
skip_checks_ = true;
return;
}
base::UmaHistogramEnumeration(
"SafeBrowsing.BrowserThrottle.RequestDestination.Checked",
request->destination);
// Decide whether safe browsing database can be checked.
// If url_lookup_service_ is null, safe browsing database should be checked by
// default.
bool can_check_db = url_lookup_service_
? url_lookup_service_->CanCheckSafeBrowsingDb()
: true;
bool can_check_high_confidence_allowlist =
url_lookup_service_
? url_lookup_service_->CanCheckSafeBrowsingHighConfidenceAllowlist()
: true;
if (async_check_tracker_ && navigation_id_.has_value() &&
!(request->load_flags & net::LOAD_PREFETCH)) {
CHECK(can_check_db);
CHECK(url_real_time_lookup_enabled_ ||
hash_realtime_selection_ !=
hash_realtime_utils::HashRealTimeSelection::kNone);
// If async check is enabled, sync_sb_checker only performs local database
// check.
sync_sb_checker_ = std::make_unique<UrlCheckerHolder>(
delegate_getter_, frame_tree_node_id_, navigation_id_,
web_contents_getter_,
/*complete_callback=*/
base::BindRepeating(&BrowserURLLoaderThrottle::OnCompleteSyncCheck,
weak_factory_.GetWeakPtr()),
/*url_real_time_lookup_enabled=*/false, can_check_db,
/*can_check_high_confidence_allowlist=*/true,
/*url_lookup_service_metric_suffix=*/kNoRealTimeURLLookupService,
/*url_lookup_service=*/nullptr,
/*hash_realtime_service=*/nullptr,
/*hash_realtime_selection=*/
hash_realtime_utils::HashRealTimeSelection::kNone,
/*is_async_check=*/false,
/*check_allowlist_before_hash_database=*/
async_check_tracker_->should_sync_checker_check_allowlist(),
SessionID::InvalidValue(), referring_app_info_);
async_sb_checker_ = std::make_unique<UrlCheckerHolder>(
delegate_getter_, frame_tree_node_id_, navigation_id_,
web_contents_getter_,
/*complete_callback=*/
base::BindRepeating(&BrowserURLLoaderThrottle::OnCompleteAsyncCheck,
weak_factory_.GetWeakPtr()),
url_real_time_lookup_enabled_, can_check_db,
can_check_high_confidence_allowlist, url_lookup_service_metric_suffix_,
url_lookup_service_, hash_realtime_service_, hash_realtime_selection_,
/*is_async_check=*/true, /*check_allowlist_before_hash_database=*/false,
tab_id_, referring_app_info_);
if (on_sync_sb_checker_created_callback_for_testing_) {
std::move(on_sync_sb_checker_created_callback_for_testing_).Run();
}
if (on_async_sb_checker_created_callback_for_testing_) {
std::move(on_async_sb_checker_created_callback_for_testing_).Run();
}
} else {
sync_sb_checker_ = std::make_unique<UrlCheckerHolder>(
delegate_getter_, frame_tree_node_id_, navigation_id_,
web_contents_getter_,
/*complete_callback=*/
base::BindRepeating(&BrowserURLLoaderThrottle::OnCompleteSyncCheck,
weak_factory_.GetWeakPtr()),
url_real_time_lookup_enabled_, can_check_db,
can_check_high_confidence_allowlist, url_lookup_service_metric_suffix_,
url_lookup_service_, hash_realtime_service_, hash_realtime_selection_,
/*is_async_check=*/false,
/*check_allowlist_before_hash_database=*/false, tab_id_,
referring_app_info_);
if (on_sync_sb_checker_created_callback_for_testing_) {
std::move(on_sync_sb_checker_created_callback_for_testing_).Run();
}
}
pending_sync_checks_++;
if (async_sb_checker_) {
pending_async_checks_++;
}
was_async_faster_than_sync_ = std::nullopt;
start_request_time_ = base::TimeTicks::Now();
is_start_request_called_ = true;
skip_check_checker_->CheckOriginalUrl(
base::BindOnce(
&BrowserURLLoaderThrottle::OnSkipCheckCompleteOnOriginalUrl,
weak_factory_.GetWeakPtr(), request->headers, request->load_flags,
request->has_user_gesture, request->url, request->method),
request->url, request->originated_from_service_worker);
}
void BrowserURLLoaderThrottle::OnSkipCheckCompleteOnOriginalUrl(
const net::HttpRequestHeaders& headers,
int load_flags,
bool has_user_gesture,
const GURL& url,
const std::string& method,
bool should_skip) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (should_skip) {
SkipChecks();
return;
}
UrlCheckerHolder::StartParams params(headers, load_flags, has_user_gesture,
url, method);
sync_sb_checker_->Start(params);
if (async_sb_checker_) {
async_sb_checker_->Start(params);
}
}
void BrowserURLLoaderThrottle::WillRedirectRequest(
net::RedirectInfo* redirect_info,
const network::mojom::URLResponseHead& /* response_head */,
bool* defer,
std::vector<std::string>* /* to_be_removed_headers */,
net::HttpRequestHeaders* /* modified_headers */,
net::HttpRequestHeaders* /* modified_cors_exempt_headers */) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (blocked_) {
// OnCheckUrlResult() has set |blocked_| to true and called
// |delegate_->CancelWithError|, but this method is called before the
// request is actually cancelled. In that case, simply defer the request.
*defer = true;
return;
}
if (skip_checks_) {
return;
}
pending_sync_checks_++;
if (async_sb_checker_) {
pending_async_checks_++;
}
was_async_faster_than_sync_ = std::nullopt;
// The check to |skip_check_checker| cannot be skipped because
// WillRedirectRequest may be called while |skip_check_checker| is still in
// progress.
skip_check_checker_->CheckRedirectUrl(base::BindOnce(
&BrowserURLLoaderThrottle::OnSkipCheckCompleteOnRedirectUrl,
weak_factory_.GetWeakPtr(), redirect_info->new_url,
redirect_info->new_method));
}
void BrowserURLLoaderThrottle::OnSkipCheckCompleteOnRedirectUrl(
const GURL& url,
const std::string& method,
bool should_skip) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (should_skip) {
SkipChecks();
return;
}
if (blocked_) {
// The original URL may already be blocked while skip check checker is
// performing the check. In that case, the SB checkers were already deleted.
return;
}
sync_sb_checker_->CheckUrl(url, method);
if (async_sb_checker_) {
async_sb_checker_->CheckUrl(url, method);
}
}
void BrowserURLLoaderThrottle::WillProcessResponse(
const GURL& response_url,
network::mojom::URLResponseHead* response_head,
bool* defer) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (blocked_) {
// OnCompleteCheck() has set |blocked_| to true and called
// |delegate_->CancelWithError|, but this method is called before the
// request is actually cancelled. In that case, simply defer the request.
*defer = true;
return;
}
bool sync_check_completed = (pending_sync_checks_ == 0);
base::UmaHistogramBoolean(
"SafeBrowsing.BrowserThrottle.IsCheckCompletedOnProcessResponse",
sync_check_completed);
is_response_from_cache_ =
response_head->was_fetched_via_cache && !response_head->network_accessed;
if (is_start_request_called_) {
base::TimeDelta interval = base::TimeTicks::Now() - start_request_time_;
base::UmaHistogramTimes(
"SafeBrowsing.BrowserThrottle.IntervalBetweenStartAndProcess",
interval);
base::UmaHistogramTimes(
base::StrCat(
{"SafeBrowsing.BrowserThrottle.IntervalBetweenStartAndProcess",
is_response_from_cache_ ? kFromCacheUmaSuffix
: kFromNetworkUmaSuffix}),
interval);
if (sync_check_completed) {
LogTotalDelay2MetricsWithResponseType(is_response_from_cache_,
base::TimeDelta());
}
is_start_request_called_ = false;
}
if (sync_check_completed) {
MaybeTransferAsyncChecker();
return;
}
DCHECK(!deferred_);
deferred_ = true;
defer_start_time_ = base::TimeTicks::Now();
*defer = true;
TRACE_EVENT_BEGIN("safe_browsing", "Deferred",
perfetto::Track::FromPointer(this));
}
const char* BrowserURLLoaderThrottle::NameForLoggingWillProcessResponse() {
return "SafeBrowsingBrowserThrottle";
}
UrlCheckerHolder* BrowserURLLoaderThrottle::GetSyncSBCheckerForTesting() {
return sync_sb_checker_.get();
}
UrlCheckerHolder* BrowserURLLoaderThrottle::GetAsyncSBCheckerForTesting() {
return async_sb_checker_.get();
}
void BrowserURLLoaderThrottle::SetOnSyncSBCheckerCreatedCallbackForTesting(
base::OnceClosure callback) {
on_sync_sb_checker_created_callback_for_testing_ = std::move(callback);
}
void BrowserURLLoaderThrottle::SetOnAsyncSBCheckerCreatedCallbackForTesting(
base::OnceClosure callback) {
on_async_sb_checker_created_callback_for_testing_ = std::move(callback);
}
void BrowserURLLoaderThrottle::OnCompleteSyncCheck(
UrlCheckerHolder::OnCompleteCheckResult result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(url_real_time_lookup_enabled_ ||
result.performed_check !=
SafeBrowsingUrlCheckerImpl::PerformedCheck::kUrlRealTimeCheck);
// |blocked| may already be set by |async_sb_checker_|.
if (blocked_) {
return;
}
DCHECK_LT(0u, pending_sync_checks_);
pending_sync_checks_--;
if (async_sb_checker_ && pending_sync_checks_ == 0 &&
!was_async_faster_than_sync_.has_value()) {
was_async_faster_than_sync_ = false;
}
// If the resource load is going to finish (either being cancelled or
// resumed), record the total delay.
if (!result.proceed || pending_sync_checks_ == 0) {
// If the resource load is currently deferred, there is a delay.
if (deferred_) {
total_delay_ = base::TimeTicks::Now() - defer_start_time_;
LogTotalDelay2MetricsWithResponseType(is_response_from_cache_,
total_delay_);
}
LogTotalDelay2Metrics(GetUrlCheckTypeForLogging(result.performed_check),
total_delay_);
}
if (result.proceed) {
if (pending_sync_checks_ == 0 && deferred_) {
deferred_ = false;
TRACE_EVENT_END("safe_browsing", /* Deferred */
perfetto::Track::FromPointer(this));
delegate_->Resume();
MaybeTransferAsyncChecker();
}
} else {
BlockUrlLoader(result.showed_interstitial);
}
}
void BrowserURLLoaderThrottle::OnCompleteAsyncCheck(
UrlCheckerHolder::OnCompleteCheckResult result) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// |blocked| may already be set by |sync_sb_checker_|.
if (blocked_) {
return;
}
DCHECK_LT(0u, pending_async_checks_);
pending_async_checks_--;
if (pending_async_checks_ == 0 && !was_async_faster_than_sync_.has_value()) {
was_async_faster_than_sync_ = true;
}
if (!result.proceed) {
BlockUrlLoader(result.showed_interstitial);
}
// There is no need to set |deferred_| for async check because it never defers
// URL loader.
}
void BrowserURLLoaderThrottle::BlockUrlLoader(bool showed_interstitial) {
blocked_ = true;
DeleteUrlChecker();
// If we didn't show an interstitial, we cancel with ERR_ABORTED to not show
// an error page either.
delegate_->CancelWithError(
showed_interstitial ? kNetErrorCodeForSafeBrowsing : net::ERR_ABORTED,
kCustomCancelReasonForURLLoader);
}
std::string BrowserURLLoaderThrottle::GetUrlCheckTypeForLogging(
SafeBrowsingUrlCheckerImpl::PerformedCheck performed_check) {
switch (performed_check) {
case SafeBrowsingUrlCheckerImpl::PerformedCheck::kUrlRealTimeCheck:
return base::StrCat({url_lookup_service_metric_suffix_, kFullURLLookup});
case SafeBrowsingUrlCheckerImpl::PerformedCheck::kHashDatabaseCheck:
return ".HashPrefixDatabaseCheck";
case SafeBrowsingUrlCheckerImpl::PerformedCheck::kCheckSkipped:
return ".SkippedCheck";
case SafeBrowsingUrlCheckerImpl::PerformedCheck::kHashRealTimeCheck:
return ".HashPrefixRealTimeCheck";
case SafeBrowsingUrlCheckerImpl::PerformedCheck::kUnknown:
NOTREACHED();
}
}
void BrowserURLLoaderThrottle::SkipChecks() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Future checks for redirects will be skipped.
skip_checks_ = true;
pending_sync_checks_--;
if (async_sb_checker_) {
pending_async_checks_--;
// Don't set |was_async_faster_than_sync_| if the counter reaches 0,
// because the checks being skipped is considered a tie between sync and
// async checks.
}
if (pending_sync_checks_ == 0 && deferred_) {
delegate_->Resume();
}
}
void BrowserURLLoaderThrottle::DeleteUrlChecker() {
pending_sync_checks_ = 0;
pending_async_checks_ = 0;
sync_sb_checker_.reset();
async_sb_checker_.reset();
skip_check_checker_.reset();
}
void BrowserURLLoaderThrottle::MaybeTransferAsyncChecker() {
// If the sync check has completed but the async check has not, move the async
// check to AsyncCheckTracker.
DCHECK_EQ(pending_sync_checks_, 0u);
if (async_sb_checker_) {
base::UmaHistogramBoolean(
"SafeBrowsing.BrowserThrottle.IsAsyncCheckerTransferred",
pending_async_checks_ > 0);
}
if (pending_async_checks_ > 0) {
async_check_tracker_->TransferUrlChecker(std::move(async_sb_checker_));
}
}
} // namespace safe_browsing