blob: 87a8812e2bf04cc7dc711e6263edb30d40f1a8d0 [file] [log] [blame]
// Copyright (c) 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/base_resource_throttle.h"
#include "base/metrics/histogram_macros.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "components/safe_browsing/base_ui_manager.h"
#include "components/safe_browsing_db/util.h"
#include "components/security_interstitials/content/unsafe_resource.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/resource_request_info.h"
#include "content/public/browser/web_contents.h"
#include "net/base/load_flags.h"
#include "net/log/net_log_capture_mode.h"
#include "net/log/net_log_source.h"
#include "net/log/net_log_source_type.h"
#include "net/url_request/redirect_info.h"
#include "net/url_request/url_request.h"
using net::NetLogEventType;
using net::NetLogSourceType;
namespace safe_browsing {
namespace {
// Maximum time in milliseconds to wait for the safe browsing service to
// verify a URL. After this amount of time the outstanding check will be
// aborted, and the URL will be treated as if it were safe.
const int kCheckUrlTimeoutMs = 5000;
// Return a dictionary with "url"=|url-spec| and optionally
// |name|=|value| (if not null), for netlogging.
// This will also add a reference to the original request's net_log ID.
std::unique_ptr<base::Value> NetLogUrlCallback(
const net::URLRequest* request,
const GURL& url,
const char* name,
const char* value,
net::NetLogCaptureMode /* capture_mode */) {
std::unique_ptr<base::DictionaryValue> event_params(
new base::DictionaryValue());
event_params->SetString("url", url.spec());
if (name && value)
event_params->SetString(name, value);
request->net_log().source().AddToEventParameters(event_params.get());
return std::move(event_params);
}
// Return a dictionary with |name|=|value|, for netlogging.
std::unique_ptr<base::Value> NetLogStringCallback(const char* name,
const char* value,
net::NetLogCaptureMode) {
std::unique_ptr<base::DictionaryValue> event_params(
new base::DictionaryValue());
if (name && value)
event_params->SetString(name, value);
return std::move(event_params);
}
} // namespace
// TODO(eroman): Downgrade these CHECK()s to DCHECKs once there is more
// unit test coverage.
BaseResourceThrottle::BaseResourceThrottle(
const net::URLRequest* request,
content::ResourceType resource_type,
scoped_refptr<SafeBrowsingDatabaseManager> database_manager,
scoped_refptr<BaseUIManager> ui_manager)
: ui_manager_(ui_manager),
threat_type_(SB_THREAT_TYPE_SAFE),
database_manager_(database_manager),
request_(request),
state_(STATE_NONE),
defer_state_(DEFERRED_NONE),
resource_type_(resource_type),
net_log_with_source_(
net::NetLogWithSource::Make(request->net_log().net_log(),
NetLogSourceType::SAFE_BROWSING)) {}
// static
BaseResourceThrottle* BaseResourceThrottle::MaybeCreate(
net::URLRequest* request,
content::ResourceType resource_type,
scoped_refptr<SafeBrowsingDatabaseManager> database_manager,
scoped_refptr<BaseUIManager> ui_manager) {
if (database_manager->IsSupported()) {
return new BaseResourceThrottle(request, resource_type,
database_manager, ui_manager);
}
return nullptr;
}
BaseResourceThrottle::~BaseResourceThrottle() {
if (defer_state_ != DEFERRED_NONE) {
EndNetLogEvent(NetLogEventType::SAFE_BROWSING_DEFERRED, nullptr, nullptr);
}
if (state_ == STATE_CHECKING_URL) {
database_manager_->CancelCheck(this);
EndNetLogEvent(NetLogEventType::SAFE_BROWSING_CHECKING_URL, "result",
"request_canceled");
}
}
// Note on net_log calls: SAFE_BROWSING_DEFERRED events must be wholly
// nested within SAFE_BROWSING_CHECKING_URL events. Synchronous checks
// are not logged at all.
void BaseResourceThrottle::BeginNetLogEvent(NetLogEventType type,
const GURL& url,
const char* name,
const char* value) {
net_log_with_source_.BeginEvent(
type, base::Bind(&NetLogUrlCallback, request_, url, name, value));
request_->net_log().AddEvent(
type, net_log_with_source_.source().ToEventParametersCallback());
}
void BaseResourceThrottle::EndNetLogEvent(NetLogEventType type,
const char* name,
const char* value) {
net_log_with_source_.EndEvent(type,
base::Bind(&NetLogStringCallback, name, value));
request_->net_log().AddEvent(
type, net_log_with_source_.source().ToEventParametersCallback());
}
void BaseResourceThrottle::WillStartRequest(bool* defer) {
// We need to check the new URL before starting the request.
if (CheckUrl(request_->url()))
return;
// We let the check run in parallel with resource load only if this
// db_manager only supports asynchronous checks, like on mobile.
// Otherwise, we defer now.
if (database_manager_->ChecksAreAlwaysAsync())
return;
// If the URL couldn't be verified synchronously, defer starting the
// request until the check has completed.
defer_state_ = DEFERRED_START;
defer_start_time_ = base::TimeTicks::Now();
*defer = true;
BeginNetLogEvent(NetLogEventType::SAFE_BROWSING_DEFERRED, request_->url(),
"defer_reason", "at_start");
}
void BaseResourceThrottle::WillProcessResponse(bool* defer) {
CHECK_EQ(defer_state_, DEFERRED_NONE);
// TODO(nparker): Maybe remove this check, since it should have no effect.
if (!database_manager_->ChecksAreAlwaysAsync())
return;
if (state_ == STATE_CHECKING_URL ||
state_ == STATE_DISPLAYING_BLOCKING_PAGE) {
defer_state_ = DEFERRED_PROCESSING;
defer_start_time_ = base::TimeTicks::Now();
*defer = true;
BeginNetLogEvent(NetLogEventType::SAFE_BROWSING_DEFERRED, request_->url(),
"defer_reason", "at_response");
}
}
bool BaseResourceThrottle::MustProcessResponseBeforeReadingBody() {
// On Android, SafeBrowsing may only decide to cancel the request when the
// response has been received. Therefore, no part of it should be cached
// until this ResourceThrottle has been able to check the response. This
// prevents the following scenario:
// 1) A request is made for foo.com which has been hacked.
// 2) The request is only canceled at WillProcessResponse stage, but part of
// it has been cached.
// 3) foo.com is no longer hacked and removed from the SafeBrowsing list.
// 4) The user requests foo.com, which is not on the SafeBrowsing list. This
// is deemed safe. However, the resource is actually served from cache,
// using the version that was previously stored.
// 5) This results in the user accessing an unsafe resource without being
// notified that it's dangerous.
// TODO(clamy): Add a browser test that checks this specific scenario.
return true;
}
void BaseResourceThrottle::WillRedirectRequest(
const net::RedirectInfo& redirect_info,
bool* defer) {
CHECK_EQ(defer_state_, DEFERRED_NONE);
// Prev check completed and was safe.
if (state_ == STATE_NONE) {
// Save the redirect urls for possible malware detail reporting later.
redirect_urls_.push_back(redirect_info.new_url);
// We need to check the new URL before following the redirect.
if (CheckUrl(redirect_info.new_url))
return;
defer_state_ = DEFERRED_REDIRECT;
} else {
CHECK(state_ == STATE_CHECKING_URL ||
state_ == STATE_DISPLAYING_BLOCKING_PAGE);
// We can't check this new URL until we have finished checking
// the prev one, or resumed from the blocking page.
unchecked_redirect_url_ = redirect_info.new_url;
defer_state_ = DEFERRED_UNCHECKED_REDIRECT;
}
defer_start_time_ = base::TimeTicks::Now();
*defer = true;
BeginNetLogEvent(
NetLogEventType::SAFE_BROWSING_DEFERRED, redirect_info.new_url,
"defer_reason",
defer_state_ == DEFERRED_REDIRECT ? "redirect" : "unchecked_redirect");
}
const char* BaseResourceThrottle::GetNameForLogging() const {
return "BaseResourceThrottle";
}
void BaseResourceThrottle::MaybeDestroyPrerenderContents(
const content::ResourceRequestInfo* info) {}
// SafeBrowsingService::Client implementation, called on the IO thread once
// the URL has been classified.
void BaseResourceThrottle::OnCheckBrowseUrlResult(
const GURL& url,
SBThreatType threat_type,
const ThreatMetadata& metadata) {
CHECK_EQ(state_, STATE_CHECKING_URL);
CHECK(url.is_valid());
CHECK(url_being_checked_.is_valid());
CHECK_EQ(url, url_being_checked_);
timer_.Stop(); // Cancel the timeout timer.
threat_type_ = threat_type;
state_ = STATE_NONE;
if (defer_state_ != DEFERRED_NONE) {
EndNetLogEvent(NetLogEventType::SAFE_BROWSING_DEFERRED, nullptr, nullptr);
}
EndNetLogEvent(
NetLogEventType::SAFE_BROWSING_CHECKING_URL, "result",
threat_type_ == SB_THREAT_TYPE_SAFE ? "safe" : "unsafe");
if (threat_type == SB_THREAT_TYPE_SAFE) {
if (defer_state_ != DEFERRED_NONE) {
// Log how much time the safe browsing check cost us.
ui_manager_->LogPauseDelay(base::TimeTicks::Now() - defer_start_time_);
ResumeRequest();
} else {
ui_manager_->LogPauseDelay(base::TimeDelta());
}
return;
}
const content::ResourceRequestInfo* info =
content::ResourceRequestInfo::ForRequest(request_);
if (request_->load_flags() & net::LOAD_PREFETCH) {
// Destroy the prefetch with FINAL_STATUS_SAFEBROSWING.
if (resource_type_ == content::RESOURCE_TYPE_MAIN_FRAME) {
MaybeDestroyPrerenderContents(info);
}
// Don't prefetch resources that fail safe browsing, disallow them.
Cancel();
UMA_HISTOGRAM_ENUMERATION("SB2.ResourceTypes2.UnsafePrefetchCanceled",
resource_type_, content::RESOURCE_TYPE_LAST_TYPE);
return;
}
UMA_HISTOGRAM_ENUMERATION("SB2.ResourceTypes2.Unsafe", resource_type_,
content::RESOURCE_TYPE_LAST_TYPE);
security_interstitials::UnsafeResource resource;
resource.url = url;
resource.original_url = request_->original_url();
resource.redirect_urls = redirect_urls_;
resource.is_subresource = resource_type_ != content::RESOURCE_TYPE_MAIN_FRAME;
resource.is_subframe = resource_type_ == content::RESOURCE_TYPE_SUB_FRAME;
resource.threat_type = threat_type;
resource.threat_metadata = metadata;
resource.callback = base::Bind(
&BaseResourceThrottle::OnBlockingPageComplete, AsWeakPtr());
resource.callback_thread = content::BrowserThread::GetTaskRunnerForThread(
content::BrowserThread::IO);
resource.web_contents_getter = info->GetWebContentsGetterForRequest();
resource.threat_source = database_manager_->GetThreatSource();
state_ = STATE_DISPLAYING_BLOCKING_PAGE;
StartDisplayingBlockingPageHelper(resource);
}
void BaseResourceThrottle::StartDisplayingBlockingPageHelper(
security_interstitials::UnsafeResource resource) {
content::BrowserThread::PostTask(
content::BrowserThread::UI, FROM_HERE,
base::Bind(&BaseResourceThrottle::StartDisplayingBlockingPage,
AsWeakPtr(), ui_manager_, resource));
}
// Static
void BaseResourceThrottle::StartDisplayingBlockingPage(
const base::WeakPtr<BaseResourceThrottle>& throttle,
scoped_refptr<BaseUIManager> ui_manager,
const security_interstitials::UnsafeResource& resource) {
content::WebContents* web_contents = resource.web_contents_getter.Run();
if (web_contents) {
ui_manager->DisplayBlockingPage(resource);
return;
}
// Tab is gone or it's being prerendered.
content::BrowserThread::PostTask(
content::BrowserThread::IO, FROM_HERE,
base::Bind(&BaseResourceThrottle::Cancel, throttle));
}
void BaseResourceThrottle::OnBlockingPageComplete(bool proceed) {
CHECK_EQ(state_, STATE_DISPLAYING_BLOCKING_PAGE);
state_ = STATE_NONE;
if (proceed) {
threat_type_ = SB_THREAT_TYPE_SAFE;
if (defer_state_ != DEFERRED_NONE) {
ResumeRequest();
}
} else {
CancelResourceLoad();
}
}
void BaseResourceThrottle::CancelResourceLoad() {
Cancel();
}
scoped_refptr<BaseUIManager> BaseResourceThrottle::ui_manager() {
return ui_manager_;
}
bool BaseResourceThrottle::CheckUrl(const GURL& url) {
TRACE_EVENT1("loader", "BaseResourceThrottle::CheckUrl", "url",
url.spec());
CHECK_EQ(state_, STATE_NONE);
// To reduce aggregate latency on mobile, check only the most dangerous
// 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_,
content::RESOURCE_TYPE_LAST_TYPE);
return true;
}
// 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_,
content::RESOURCE_TYPE_LAST_TYPE);
if (database_manager_->CheckBrowseUrl(url, this)) {
threat_type_ = SB_THREAT_TYPE_SAFE;
ui_manager_->LogPauseDelay(base::TimeDelta()); // No delay.
return true;
}
state_ = STATE_CHECKING_URL;
url_being_checked_ = url;
BeginNetLogEvent(NetLogEventType::SAFE_BROWSING_CHECKING_URL, url, nullptr,
nullptr);
// Start a timer to abort the check if it takes too long.
// TODO(nparker): Set this only when we defer, based on remaining time,
// so we don't cancel earlier than necessary.
timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(kCheckUrlTimeoutMs),
this, &BaseResourceThrottle::OnCheckUrlTimeout);
return false;
}
void BaseResourceThrottle::OnCheckUrlTimeout() {
CHECK_EQ(state_, STATE_CHECKING_URL);
database_manager_->CancelCheck(this);
OnCheckBrowseUrlResult(url_being_checked_, safe_browsing::SB_THREAT_TYPE_SAFE,
ThreatMetadata());
}
void BaseResourceThrottle::ResumeRequest() {
CHECK_EQ(state_, STATE_NONE);
CHECK_NE(defer_state_, DEFERRED_NONE);
bool resume = true;
if (defer_state_ == DEFERRED_UNCHECKED_REDIRECT) {
// Save the redirect urls for possible malware detail reporting later.
redirect_urls_.push_back(unchecked_redirect_url_);
if (!CheckUrl(unchecked_redirect_url_)) {
// We're now waiting for the unchecked_redirect_url_.
defer_state_ = DEFERRED_REDIRECT;
resume = false;
BeginNetLogEvent(NetLogEventType::SAFE_BROWSING_DEFERRED,
unchecked_redirect_url_, "defer_reason",
"resumed_redirect");
}
}
if (resume) {
defer_state_ = DEFERRED_NONE;
Resume();
}
}
} // namespace safe_browsing