blob: 6784feaaeccd48b4d16befd767edaeaf84091b2d [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 "chrome/browser/safe_browsing/download_protection/check_client_download_request.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/task_scheduler/post_task.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/download_protection/download_feedback_service.h"
#include "chrome/browser/safe_browsing/download_protection/download_protection_service.h"
#include "chrome/browser/safe_browsing/download_protection/download_protection_util.h"
#include "chrome/browser/safe_browsing/download_protection/ppapi_download_request.h"
#include "chrome/common/safe_browsing/download_protection_util.h"
#include "chrome/common/safe_browsing/file_type_policies.h"
#include "components/data_use_measurement/core/data_use_user_data.h"
#include "components/safe_browsing/common/utils.h"
#include "content/public/browser/browser_context.h"
#include "net/http/http_cache.h"
#include "net/http/http_status_code.h"
namespace safe_browsing {
namespace {
const char kDownloadExtensionUmaName[] = "SBClientDownload.DownloadExtensions";
const char kUnsupportedSchemeUmaPrefix[] = "SBClientDownload.UnsupportedScheme";
void RecordFileExtensionType(const std::string& metric_name,
const base::FilePath& file) {
UMA_HISTOGRAM_SPARSE_SLOWLY(
metric_name, FileTypePolicies::GetInstance()->UmaValueForFile(file));
}
void RecordArchivedArchiveFileExtensionType(const base::FilePath& file) {
UMA_HISTOGRAM_SPARSE_SLOWLY(
"SBClientDownload.ArchivedArchiveExtensions",
FileTypePolicies::GetInstance()->UmaValueForFile(file));
}
std::string GetUnsupportedSchemeName(const GURL& download_url) {
if (download_url.SchemeIs(url::kContentScheme))
return "ContentScheme";
if (download_url.SchemeIs(url::kContentIDScheme))
return "ContentIdScheme";
if (download_url.SchemeIsFile())
return download_url.has_host() ? "RemoteFileScheme" : "LocalFileScheme";
if (download_url.SchemeIsFileSystem())
return "FileSystemScheme";
if (download_url.SchemeIs(url::kFtpScheme))
return "FtpScheme";
if (download_url.SchemeIs(url::kGopherScheme))
return "GopherScheme";
if (download_url.SchemeIs(url::kJavaScriptScheme))
return "JavaScriptScheme";
if (download_url.SchemeIsWSOrWSS())
return "WSOrWSSScheme";
return "OtherUnsupportedScheme";
}
} // namespace
CheckClientDownloadRequest::CheckClientDownloadRequest(
content::DownloadItem* item,
const CheckDownloadCallback& callback,
DownloadProtectionService* service,
const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager,
BinaryFeatureExtractor* binary_feature_extractor)
: item_(item),
url_chain_(item->GetUrlChain()),
referrer_url_(item->GetReferrerUrl()),
tab_url_(item->GetTabUrl()),
tab_referrer_url_(item->GetTabReferrerUrl()),
archived_executable_(false),
archive_is_valid_(ArchiveValid::UNSET),
#if defined(OS_MACOSX)
disk_image_signature_(nullptr),
#endif
callback_(callback),
service_(service),
binary_feature_extractor_(binary_feature_extractor),
database_manager_(database_manager),
pingback_enabled_(service_->enabled()),
finished_(false),
type_(ClientDownloadRequest::WIN_EXECUTABLE),
start_time_(base::TimeTicks::Now()),
skipped_url_whitelist_(false),
skipped_certificate_whitelist_(false),
is_extended_reporting_(false),
is_incognito_(false),
weakptr_factory_(this) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
item_->AddObserver(this);
}
bool CheckClientDownloadRequest::ShouldSampleUnsupportedFile(
const base::FilePath& filename) {
// If this extension is specifically marked as SAMPLED_PING (as are
// all "unknown" extensions), we may want to sample it. Sampling it means
// we'll send a "light ping" with private info removed, and we won't
// use the verdict.
const FileTypePolicies* policies = FileTypePolicies::GetInstance();
return service_ && is_extended_reporting_ && !is_incognito_ &&
base::RandDouble() < policies->SampledPingProbability() &&
policies->PingSettingForFile(filename) ==
DownloadFileType::SAMPLED_PING;
}
void CheckClientDownloadRequest::Start() {
DVLOG(2) << "Starting SafeBrowsing download check for: "
<< item_->DebugString(true);
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (item_->GetBrowserContext()) {
Profile* profile = Profile::FromBrowserContext(item_->GetBrowserContext());
is_extended_reporting_ =
profile && IsExtendedReportingEnabled(*profile->GetPrefs());
is_incognito_ = item_->GetBrowserContext()->IsOffTheRecord();
}
DownloadCheckResultReason reason = REASON_MAX;
if (!IsSupportedDownload(*item_, item_->GetTargetFilePath(), &reason,
&type_)) {
switch (reason) {
case REASON_EMPTY_URL_CHAIN:
case REASON_INVALID_URL:
case REASON_LOCAL_FILE:
case REASON_REMOTE_FILE:
PostFinishTask(DownloadCheckResult::UNKNOWN, reason);
return;
case REASON_UNSUPPORTED_URL_SCHEME:
RecordFileExtensionType(
base::StringPrintf(
"%s.%s", kUnsupportedSchemeUmaPrefix,
GetUnsupportedSchemeName(item_->GetUrlChain().back()).c_str()),
item_->GetTargetFilePath());
PostFinishTask(DownloadCheckResult::UNKNOWN, reason);
return;
case REASON_NOT_BINARY_FILE:
if (ShouldSampleUnsupportedFile(item_->GetTargetFilePath())) {
// Send a "light ping" and don't use the verdict.
type_ = ClientDownloadRequest::SAMPLED_UNSUPPORTED_FILE;
break;
}
RecordFileExtensionType(kDownloadExtensionUmaName,
item_->GetTargetFilePath());
PostFinishTask(DownloadCheckResult::UNKNOWN, reason);
return;
default:
// We only expect the reasons explicitly handled above.
NOTREACHED();
}
}
RecordFileExtensionType(kDownloadExtensionUmaName,
item_->GetTargetFilePath());
// Compute features from the file contents. Note that we record histograms
// based on the result, so this runs regardless of whether the pingbacks
// are enabled.
if (item_->GetTargetFilePath().MatchesExtension(FILE_PATH_LITERAL(".zip"))) {
StartExtractZipFeatures();
#if defined(OS_MACOSX)
} else if (item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".dmg")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".img")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".iso")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".smi")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".cdr")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".dart")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".dc42")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".diskcopy42")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".dmgpart")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".dvdr")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".imgpart")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".ndif")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".sparsebundle")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".sparseimage")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".toast")) ||
item_->GetTargetFilePath().MatchesExtension(
FILE_PATH_LITERAL(".udif"))) {
StartExtractDmgFeatures();
#endif
} else {
#if defined(OS_MACOSX)
// Checks for existence of "koly" signature even if file doesn't have
// archive-type extension, then calls ExtractFileOrDmgFeatures() with
// result.
base::PostTaskWithTraitsAndReplyWithResult(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BACKGROUND},
base::Bind(DiskImageTypeSnifferMac::IsAppleDiskImage,
item_->GetTargetFilePath()),
base::Bind(&CheckClientDownloadRequest::ExtractFileOrDmgFeatures,
this));
#else
StartExtractFileFeatures();
#endif
}
}
// Start a timeout to cancel the request if it takes too long.
// This should only be called after we have finished accessing the file.
void CheckClientDownloadRequest::StartTimeout() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!service_) {
// Request has already been cancelled.
return;
}
timeout_start_time_ = base::TimeTicks::Now();
BrowserThread::PostDelayedTask(
BrowserThread::UI, FROM_HERE,
base::BindOnce(&CheckClientDownloadRequest::Cancel,
weakptr_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(
service_->download_request_timeout_ms()));
}
// Canceling a request will cause us to always report the result as
// DownloadCheckResult::UNKNOWN unless a pending request is about to call
// FinishRequest.
void CheckClientDownloadRequest::Cancel() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (fetcher_.get()) {
// The DownloadProtectionService is going to release its reference, so we
// might be destroyed before the URLFetcher completes. Cancel the
// fetcher so it does not try to invoke OnURLFetchComplete.
fetcher_.reset();
}
// Note: If there is no fetcher, then some callback is still holding a
// reference to this object. We'll eventually wind up in some method on
// the UI thread that will call FinishRequest() again. If FinishRequest()
// is called a second time, it will be a no-op.
FinishRequest(DownloadCheckResult::UNKNOWN, REASON_REQUEST_CANCELED);
// Calling FinishRequest might delete this object, we may be deleted by
// this point.
}
// content::DownloadItem::Observer implementation.
void CheckClientDownloadRequest::OnDownloadDestroyed(
content::DownloadItem* download) {
Cancel();
DCHECK(item_ == NULL);
}
// TODO: this method puts "DownloadProtectionService::" in front of a lot of
// stuff to avoid referencing the enums i copied to this .h file. From the
// net::URLFetcherDelegate interface.
void CheckClientDownloadRequest::OnURLFetchComplete(
const net::URLFetcher* source) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(source, fetcher_.get());
DVLOG(2) << "Received a response for URL: " << item_->GetUrlChain().back()
<< ": success=" << source->GetStatus().is_success()
<< " response_code=" << source->GetResponseCode();
if (source->GetStatus().is_success()) {
UMA_HISTOGRAM_SPARSE_SLOWLY("SBClientDownload.DownloadRequestResponseCode",
source->GetResponseCode());
}
UMA_HISTOGRAM_SPARSE_SLOWLY("SBClientDownload.DownloadRequestNetError",
-source->GetStatus().error());
DownloadCheckResultReason reason = REASON_SERVER_PING_FAILED;
DownloadCheckResult result = DownloadCheckResult::UNKNOWN;
std::string token;
if (source->GetStatus().is_success() &&
net::HTTP_OK == source->GetResponseCode()) {
ClientDownloadResponse response;
std::string data;
bool got_data = source->GetResponseAsString(&data);
DCHECK(got_data);
if (!response.ParseFromString(data)) {
reason = REASON_INVALID_RESPONSE_PROTO;
result = DownloadCheckResult::UNKNOWN;
} else if (type_ == ClientDownloadRequest::SAMPLED_UNSUPPORTED_FILE) {
// Ignore the verdict because we were just reporting a sampled file.
reason = REASON_SAMPLED_UNSUPPORTED_FILE;
result = DownloadCheckResult::UNKNOWN;
} else {
switch (response.verdict()) {
case ClientDownloadResponse::SAFE:
reason = REASON_DOWNLOAD_SAFE;
result = DownloadCheckResult::SAFE;
break;
case ClientDownloadResponse::DANGEROUS:
reason = REASON_DOWNLOAD_DANGEROUS;
result = DownloadCheckResult::DANGEROUS;
token = response.token();
break;
case ClientDownloadResponse::UNCOMMON:
reason = REASON_DOWNLOAD_UNCOMMON;
result = DownloadCheckResult::UNCOMMON;
token = response.token();
break;
case ClientDownloadResponse::DANGEROUS_HOST:
reason = REASON_DOWNLOAD_DANGEROUS_HOST;
result = DownloadCheckResult::DANGEROUS_HOST;
token = response.token();
break;
case ClientDownloadResponse::POTENTIALLY_UNWANTED:
reason = REASON_DOWNLOAD_POTENTIALLY_UNWANTED;
result = DownloadCheckResult::POTENTIALLY_UNWANTED;
token = response.token();
break;
case ClientDownloadResponse::UNKNOWN:
reason = REASON_VERDICT_UNKNOWN;
result = DownloadCheckResult::UNKNOWN;
break;
default:
LOG(DFATAL) << "Unknown download response verdict: "
<< response.verdict();
reason = REASON_INVALID_RESPONSE_VERDICT;
result = DownloadCheckResult::UNKNOWN;
}
}
if (!token.empty())
DownloadProtectionService::SetDownloadPingToken(item_, token);
bool upload_requested = response.upload();
DownloadFeedbackService::MaybeStorePingsForDownload(
result, upload_requested, item_, client_download_request_data_, data);
}
// We don't need the fetcher anymore.
fetcher_.reset();
UMA_HISTOGRAM_TIMES("SBClientDownload.DownloadRequestDuration",
base::TimeTicks::Now() - start_time_);
UMA_HISTOGRAM_TIMES("SBClientDownload.DownloadRequestNetworkDuration",
base::TimeTicks::Now() - request_start_time_);
FinishRequest(result, reason);
}
// static
bool CheckClientDownloadRequest::IsSupportedDownload(
const content::DownloadItem& item,
const base::FilePath& target_path,
DownloadCheckResultReason* reason,
ClientDownloadRequest::DownloadType* type) {
if (item.GetUrlChain().empty()) {
*reason = REASON_EMPTY_URL_CHAIN;
return false;
}
const GURL& final_url = item.GetUrlChain().back();
if (!final_url.is_valid() || final_url.is_empty()) {
*reason = REASON_INVALID_URL;
return false;
}
if (!final_url.IsStandard() && !final_url.SchemeIsBlob() &&
!final_url.SchemeIs(url::kDataScheme)) {
*reason = REASON_UNSUPPORTED_URL_SCHEME;
return false;
}
// TODO(jialiul): Remove duplicated counting of REMOTE_FILE and LOCAL_FILE
// after SBClientDownload.UnsupportedScheme.* metrics become available in
// stable channel.
if (final_url.SchemeIsFile()) {
*reason = final_url.has_host() ? REASON_REMOTE_FILE : REASON_LOCAL_FILE;
return false;
}
// This check should be last, so we know the earlier checks passed.
if (!FileTypePolicies::GetInstance()->IsCheckedBinaryFile(target_path)) {
*reason = REASON_NOT_BINARY_FILE;
return false;
}
*type = download_protection_util::GetDownloadType(target_path);
return true;
}
CheckClientDownloadRequest::~CheckClientDownloadRequest() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(item_ == NULL);
}
void CheckClientDownloadRequest::OnFileFeatureExtractionDone() {
// This can run in any thread, since it just posts more messages.
// TODO(noelutz): DownloadInfo should also contain the IP address of
// every URL in the redirect chain. We also should check whether the
// download URL is hosted on the internal network.
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
base::BindOnce(&CheckClientDownloadRequest::CheckWhitelists, this));
// We wait until after the file checks finish to start the timeout, as
// windows can cause permissions errors if the timeout fired while we were
// checking the file signature and we tried to complete the download.
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::BindOnce(&CheckClientDownloadRequest::StartTimeout, this));
}
void CheckClientDownloadRequest::StartExtractFileFeatures() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(item_); // Called directly from Start(), item should still exist.
// Since we do blocking I/O, offload this to a worker thread.
// The task does not need to block shutdown.
base::PostTaskWithTraits(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::BACKGROUND,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&CheckClientDownloadRequest::ExtractFileFeatures, this,
item_->GetFullPath()));
}
void CheckClientDownloadRequest::ExtractFileFeatures(
const base::FilePath& file_path) {
base::TimeTicks start_time = base::TimeTicks::Now();
binary_feature_extractor_->CheckSignature(file_path, &signature_info_);
bool is_signed = (signature_info_.certificate_chain_size() > 0);
if (is_signed) {
DVLOG(2) << "Downloaded a signed binary: " << file_path.value();
} else {
DVLOG(2) << "Downloaded an unsigned binary: " << file_path.value();
}
UMA_HISTOGRAM_BOOLEAN("SBClientDownload.SignedBinaryDownload", is_signed);
UMA_HISTOGRAM_TIMES("SBClientDownload.ExtractSignatureFeaturesTime",
base::TimeTicks::Now() - start_time);
start_time = base::TimeTicks::Now();
image_headers_.reset(new ClientDownloadRequest_ImageHeaders());
if (!binary_feature_extractor_->ExtractImageFeatures(
file_path, BinaryFeatureExtractor::kDefaultOptions,
image_headers_.get(), nullptr)) {
image_headers_.reset();
}
UMA_HISTOGRAM_TIMES("SBClientDownload.ExtractImageHeadersTime",
base::TimeTicks::Now() - start_time);
OnFileFeatureExtractionDone();
}
void CheckClientDownloadRequest::StartExtractZipFeatures() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(item_); // Called directly from Start(), item should still exist.
zip_analysis_start_time_ = base::TimeTicks::Now();
// We give the zip analyzer a weak pointer to this object. Since the
// analyzer is refcounted, it might outlive the request.
analyzer_ = new SandboxedZipAnalyzer(
item_->GetFullPath(),
base::Bind(&CheckClientDownloadRequest::OnZipAnalysisFinished,
weakptr_factory_.GetWeakPtr()));
analyzer_->Start();
}
void CheckClientDownloadRequest::OnZipAnalysisFinished(
const ArchiveAnalyzerResults& results) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(ClientDownloadRequest::ZIPPED_EXECUTABLE, type_);
if (!service_)
return;
// Even if !results.success, some of the zip may have been parsed.
// Some unzippers will successfully unpack archives that we cannot,
// so we're lenient here.
archive_is_valid_ =
(results.success ? ArchiveValid::VALID : ArchiveValid::INVALID);
archived_executable_ = results.has_executable;
archived_binary_.CopyFrom(results.archived_binary);
DVLOG(1) << "Zip analysis finished for " << item_->GetFullPath().value()
<< ", has_executable=" << results.has_executable
<< ", has_archive=" << results.has_archive
<< ", success=" << results.success;
UMA_HISTOGRAM_BOOLEAN("SBClientDownload.ZipFileSuccess", results.success);
UMA_HISTOGRAM_BOOLEAN("SBClientDownload.ZipFileHasExecutable",
archived_executable_);
UMA_HISTOGRAM_BOOLEAN("SBClientDownload.ZipFileHasArchiveButNoExecutable",
results.has_archive && !archived_executable_);
UMA_HISTOGRAM_TIMES("SBClientDownload.ExtractZipFeaturesTime",
base::TimeTicks::Now() - zip_analysis_start_time_);
for (const auto& file_name : results.archived_archive_filenames)
RecordArchivedArchiveFileExtensionType(file_name);
if (!archived_executable_) {
if (results.has_archive) {
type_ = ClientDownloadRequest::ZIPPED_ARCHIVE;
} else if (!results.success) {
// .zip files that look invalid to Chrome can often be successfully
// unpacked by other archive tools, so they may be a real threat.
type_ = ClientDownloadRequest::INVALID_ZIP;
} else {
// Normal zip w/o EXEs, or invalid zip and not extended-reporting.
PostFinishTask(DownloadCheckResult::UNKNOWN,
REASON_ARCHIVE_WITHOUT_BINARIES);
return;
}
}
OnFileFeatureExtractionDone();
}
#if defined(OS_MACOSX)
// This is called for .DMGs and other files that can be parsed by
// SandboxedDMGAnalyzer.
void CheckClientDownloadRequest::StartExtractDmgFeatures() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(item_);
// Directly use 'dmg' extension since download file may not have any
// extension, but has still been deemed a DMG through file type sniffing.
bool too_big_to_unpack =
base::checked_cast<uint64_t>(item_->GetTotalBytes()) >
FileTypePolicies::GetInstance()->GetMaxFileSizeToAnalyze("dmg");
UMA_HISTOGRAM_BOOLEAN("SBClientDownload.DmgTooBigToUnpack",
too_big_to_unpack);
if (too_big_to_unpack) {
OnFileFeatureExtractionDone();
} else {
dmg_analyzer_ = new SandboxedDMGAnalyzer(
item_->GetFullPath(),
base::Bind(&CheckClientDownloadRequest::OnDmgAnalysisFinished,
weakptr_factory_.GetWeakPtr()));
dmg_analyzer_->Start();
dmg_analysis_start_time_ = base::TimeTicks::Now();
}
}
// Extracts DMG features if file has 'koly' signature, otherwise extracts
// regular file features.
void CheckClientDownloadRequest::ExtractFileOrDmgFeatures(
bool download_file_has_koly_signature) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
UMA_HISTOGRAM_BOOLEAN(
"SBClientDownload."
"DownloadFileWithoutDiskImageExtensionHasKolySignature",
download_file_has_koly_signature);
// Returns if DownloadItem was destroyed during parsing of file metadata.
if (item_ == nullptr)
return;
if (download_file_has_koly_signature)
StartExtractDmgFeatures();
else
StartExtractFileFeatures();
}
void CheckClientDownloadRequest::OnDmgAnalysisFinished(
const ArchiveAnalyzerResults& results) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(ClientDownloadRequest::MAC_EXECUTABLE, type_);
if (!service_)
return;
if (results.signature_blob.size() > 0) {
disk_image_signature_ =
base::MakeUnique<std::vector<uint8_t>>(results.signature_blob);
}
// Even if !results.success, some of the DMG may have been parsed.
archive_is_valid_ =
(results.success ? ArchiveValid::VALID : ArchiveValid::INVALID);
archived_executable_ = results.has_executable;
archived_binary_.CopyFrom(results.archived_binary);
DVLOG(1) << "DMG analysis has finished for " << item_->GetFullPath().value()
<< ", has_executable=" << results.has_executable
<< ", success=" << results.success;
int64_t uma_file_type = FileTypePolicies::GetInstance()->UmaValueForFile(
item_->GetTargetFilePath());
if (results.success) {
UMA_HISTOGRAM_SPARSE_SLOWLY("SBClientDownload.DmgFileSuccessByType",
uma_file_type);
} else {
UMA_HISTOGRAM_SPARSE_SLOWLY("SBClientDownload.DmgFileFailureByType",
uma_file_type);
}
if (archived_executable_) {
UMA_HISTOGRAM_SPARSE_SLOWLY("SBClientDownload.DmgFileHasExecutableByType",
uma_file_type);
} else {
UMA_HISTOGRAM_SPARSE_SLOWLY("SBClientDownload.DmgFileHasNoExecutableByType",
uma_file_type);
}
UMA_HISTOGRAM_TIMES("SBClientDownload.ExtractDmgFeaturesTime",
base::TimeTicks::Now() - dmg_analysis_start_time_);
if (!archived_executable_) {
if (!results.success) {
type_ = ClientDownloadRequest::INVALID_MAC_ARCHIVE;
} else {
PostFinishTask(DownloadCheckResult::SAFE,
REASON_ARCHIVE_WITHOUT_BINARIES);
return;
}
}
OnFileFeatureExtractionDone();
}
#endif // defined(OS_MACOSX)
bool CheckClientDownloadRequest::ShouldSampleWhitelistedDownload() {
// We currently sample 1% whitelisted downloads from users who opted
// in extended reporting and are not in incognito mode.
return service_ && is_extended_reporting_ && !is_incognito_ &&
base::RandDouble() < service_->whitelist_sample_rate();
}
void CheckClientDownloadRequest::CheckWhitelists() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (!database_manager_.get()) {
PostFinishTask(DownloadCheckResult::UNKNOWN, REASON_SB_DISABLED);
return;
}
const GURL& url = url_chain_.back();
// TODO(asanka): This may acquire a lock on the SB DB on the IO thread.
if (url.is_valid() && database_manager_->MatchDownloadWhitelistUrl(url)) {
DVLOG(2) << url << " is on the download whitelist.";
RecordCountOfWhitelistedDownload(URL_WHITELIST);
if (ShouldSampleWhitelistedDownload()) {
skipped_url_whitelist_ = true;
} else {
// TODO(grt): Continue processing without uploading so that
// ClientDownloadRequest callbacks can be run even for this type of safe
// download.
PostFinishTask(DownloadCheckResult::SAFE, REASON_WHITELISTED_URL);
return;
}
}
if (!skipped_url_whitelist_ && signature_info_.trusted()) {
for (int i = 0; i < signature_info_.certificate_chain_size(); ++i) {
if (CertificateChainIsWhitelisted(signature_info_.certificate_chain(i))) {
RecordCountOfWhitelistedDownload(SIGNATURE_WHITELIST);
if (ShouldSampleWhitelistedDownload()) {
skipped_certificate_whitelist_ = true;
break;
} else {
// TODO(grt): Continue processing without uploading so that
// ClientDownloadRequest callbacks can be run even for this type of
// safe download.
PostFinishTask(DownloadCheckResult::SAFE, REASON_TRUSTED_EXECUTABLE);
return;
}
}
}
}
RecordCountOfWhitelistedDownload(NO_WHITELIST_MATCH);
if (!pingback_enabled_) {
PostFinishTask(DownloadCheckResult::UNKNOWN, REASON_PING_DISABLED);
return;
}
// The URLFetcher is owned by the UI thread, so post a message to
// start the pingback.
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::BindOnce(&CheckClientDownloadRequest::GetTabRedirects, this));
}
void CheckClientDownloadRequest::GetTabRedirects() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!service_)
return;
if (!tab_url_.is_valid()) {
SendRequest();
return;
}
Profile* profile = Profile::FromBrowserContext(item_->GetBrowserContext());
history::HistoryService* history = HistoryServiceFactory::GetForProfile(
profile, ServiceAccessType::EXPLICIT_ACCESS);
if (!history) {
SendRequest();
return;
}
history->QueryRedirectsTo(
tab_url_,
base::Bind(&CheckClientDownloadRequest::OnGotTabRedirects,
base::Unretained(this), tab_url_),
&request_tracker_);
}
void CheckClientDownloadRequest::OnGotTabRedirects(
const GURL& url,
const history::RedirectList* redirect_list) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK_EQ(url, tab_url_);
if (!service_)
return;
if (!redirect_list->empty()) {
tab_redirects_.insert(tab_redirects_.end(), redirect_list->rbegin(),
redirect_list->rend());
}
SendRequest();
}
// If the hash of either the original file or any executables within an
// archive matches the blacklist flag, return true.
bool CheckClientDownloadRequest::IsDownloadManuallyBlacklisted(
const ClientDownloadRequest& request) {
if (service_->IsHashManuallyBlacklisted(request.digests().sha256()))
return true;
for (auto bin_itr : request.archived_binary()) {
if (service_->IsHashManuallyBlacklisted(bin_itr.digests().sha256()))
return true;
}
return false;
}
// Prepares URLs to be put into a ping message. Currently this just shortens
// data: URIs, other URLs are included verbatim. If this is a sampled binary,
// we'll send a light-ping which strips PII from the URL.
std::string CheckClientDownloadRequest::SanitizeUrl(const GURL& url) const {
if (type_ == ClientDownloadRequest::SAMPLED_UNSUPPORTED_FILE)
return url.GetOrigin().spec();
return ShortURLForReporting(url);
}
void CheckClientDownloadRequest::SendRequest() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// This is our last chance to check whether the request has been canceled
// before sending it.
if (!service_)
return;
ClientDownloadRequest request;
auto population = is_extended_reporting_
? ChromeUserPopulation::EXTENDED_REPORTING
: ChromeUserPopulation::SAFE_BROWSING;
request.mutable_population()->set_user_population(population);
request.set_url(SanitizeUrl(item_->GetUrlChain().back()));
request.mutable_digests()->set_sha256(item_->GetHash());
request.set_length(item_->GetReceivedBytes());
request.set_skipped_url_whitelist(skipped_url_whitelist_);
request.set_skipped_certificate_whitelist(skipped_certificate_whitelist_);
for (size_t i = 0; i < item_->GetUrlChain().size(); ++i) {
ClientDownloadRequest::Resource* resource = request.add_resources();
resource->set_url(SanitizeUrl(item_->GetUrlChain()[i]));
if (i == item_->GetUrlChain().size() - 1) {
// The last URL in the chain is the download URL.
resource->set_type(ClientDownloadRequest::DOWNLOAD_URL);
resource->set_referrer(SanitizeUrl(item_->GetReferrerUrl()));
DVLOG(2) << "dl url " << resource->url();
if (!item_->GetRemoteAddress().empty()) {
resource->set_remote_ip(item_->GetRemoteAddress());
DVLOG(2) << " dl url remote addr: " << resource->remote_ip();
}
DVLOG(2) << "dl referrer " << resource->referrer();
} else {
DVLOG(2) << "dl redirect " << i << " " << resource->url();
resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT);
}
// TODO(noelutz): fill out the remote IP addresses.
}
// TODO(mattm): fill out the remote IP addresses for tab resources.
for (size_t i = 0; i < tab_redirects_.size(); ++i) {
ClientDownloadRequest::Resource* resource = request.add_resources();
DVLOG(2) << "tab redirect " << i << " " << tab_redirects_[i].spec();
resource->set_url(SanitizeUrl(tab_redirects_[i]));
resource->set_type(ClientDownloadRequest::TAB_REDIRECT);
}
if (tab_url_.is_valid()) {
ClientDownloadRequest::Resource* resource = request.add_resources();
resource->set_url(SanitizeUrl(tab_url_));
DVLOG(2) << "tab url " << resource->url();
resource->set_type(ClientDownloadRequest::TAB_URL);
if (tab_referrer_url_.is_valid()) {
resource->set_referrer(SanitizeUrl(tab_referrer_url_));
DVLOG(2) << "tab referrer " << resource->referrer();
}
}
request.set_user_initiated(item_->HasUserGesture());
request.set_file_basename(
item_->GetTargetFilePath().BaseName().AsUTF8Unsafe());
request.set_download_type(type_);
ReferrerChainData* referrer_chain_data = static_cast<ReferrerChainData*>(
item_->GetUserData(kDownloadReferrerChainDataKey));
if (referrer_chain_data &&
!referrer_chain_data->GetReferrerChain()->empty()) {
request.mutable_referrer_chain()->Swap(
referrer_chain_data->GetReferrerChain());
if (type_ == ClientDownloadRequest::SAMPLED_UNSUPPORTED_FILE)
SafeBrowsingNavigationObserverManager::SanitizeReferrerChain(
request.mutable_referrer_chain());
}
#if defined(OS_MACOSX)
UMA_HISTOGRAM_BOOLEAN(
"SBClientDownload."
"DownloadFileHasDmgSignature",
disk_image_signature_ != nullptr);
if (disk_image_signature_) {
request.set_udif_code_signature(disk_image_signature_->data(),
disk_image_signature_->size());
}
#endif
if (archive_is_valid_ != ArchiveValid::UNSET)
request.set_archive_valid(archive_is_valid_ == ArchiveValid::VALID);
request.mutable_signature()->CopyFrom(signature_info_);
if (image_headers_)
request.set_allocated_image_headers(image_headers_.release());
if (archived_executable_)
request.mutable_archived_binary()->Swap(&archived_binary_);
if (!request.SerializeToString(&client_download_request_data_)) {
FinishRequest(DownloadCheckResult::UNKNOWN, REASON_INVALID_REQUEST_PROTO);
return;
}
// User can manually blacklist a sha256 via flag, for testing.
// This is checked just before the request is sent, to verify the request
// would have been sent. This emmulates the server returning a DANGEROUS
// verdict as closely as possible.
if (IsDownloadManuallyBlacklisted(request)) {
DVLOG(1) << "Download verdict overridden to DANGEROUS by flag.";
PostFinishTask(DownloadCheckResult::DANGEROUS, REASON_MANUAL_BLACKLIST);
return;
}
service_->client_download_request_callbacks_.Notify(item_, &request);
DVLOG(2) << "Sending a request for URL: " << item_->GetUrlChain().back();
DVLOG(2) << "Detected " << request.archived_binary().size() << " archived "
<< "binaries";
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("client_download_request", R"(
semantics {
sender: "Download Protection Service"
description:
"Chromium checks whether a given download is likely to be "
"dangerous by sending this client download request to Google's "
"Safe Browsing servers. Safe Browsing server will respond to "
"this request by sending back a verdict, indicating if this "
"download is safe or the danger type of this download (e.g. "
"dangerous content, uncommon content, potentially harmful, etc)."
trigger:
"This request is triggered when a download is about to complete, "
"the download is not whitelisted, and its file extension is "
"supported by download protection service (e.g. executables, "
"archives). Please refer to https://cs.chromium.org/chromium/src/"
"chrome/browser/resources/safe_browsing/"
"download_file_types.asciipb for the complete list of supported "
"files."
data:
"URL of the file to be downloaded, its referrer chain, digest "
"and other features extracted from the downloaded file. Refer to "
"ClientDownloadRequest message in https://cs.chromium.org/"
"chromium/src/components/safe_browsing/csd.proto for all "
"submitted features."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: true
cookies_store: "Safe Browsing cookies store"
setting:
"Users can enable or disable the entire Safe Browsing service in "
"Chromium's settings by toggling 'Protect you and your device "
"from dangerous sites' under Privacy. This feature is enabled by "
"default."
chrome_policy {
SafeBrowsingEnabled {
policy_options {mode: MANDATORY}
SafeBrowsingEnabled: false
}
}
})");
fetcher_ =
net::URLFetcher::Create(0, PPAPIDownloadRequest::GetDownloadRequestUrl(),
net::URLFetcher::POST, this, traffic_annotation);
data_use_measurement::DataUseUserData::AttachToFetcher(
fetcher_.get(), data_use_measurement::DataUseUserData::SAFE_BROWSING);
fetcher_->SetLoadFlags(net::LOAD_DISABLE_CACHE);
fetcher_->SetAutomaticallyRetryOn5xx(false); // Don't retry on error.
fetcher_->SetRequestContext(service_->request_context_getter_.get());
fetcher_->SetUploadData("application/octet-stream",
client_download_request_data_);
request_start_time_ = base::TimeTicks::Now();
UMA_HISTOGRAM_COUNTS("SBClientDownload.DownloadRequestPayloadSize",
client_download_request_data_.size());
fetcher_->Start();
}
void CheckClientDownloadRequest::PostFinishTask(
DownloadCheckResult result,
DownloadCheckResultReason reason) {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::BindOnce(&CheckClientDownloadRequest::FinishRequest, this, result,
reason));
}
void CheckClientDownloadRequest::FinishRequest(
DownloadCheckResult result,
DownloadCheckResultReason reason) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (finished_) {
return;
}
finished_ = true;
// Ensure the timeout task is cancelled while we still have a non-zero
// refcount. (crbug.com/240449)
weakptr_factory_.InvalidateWeakPtrs();
if (!request_start_time_.is_null()) {
UMA_HISTOGRAM_ENUMERATION("SBClientDownload.DownloadRequestNetworkStats",
reason, REASON_MAX);
}
if (!timeout_start_time_.is_null()) {
UMA_HISTOGRAM_ENUMERATION("SBClientDownload.DownloadRequestTimeoutStats",
reason, REASON_MAX);
if (reason != REASON_REQUEST_CANCELED) {
UMA_HISTOGRAM_TIMES("SBClientDownload.DownloadRequestTimeoutDuration",
base::TimeTicks::Now() - timeout_start_time_);
}
}
if (service_) {
DVLOG(2) << "SafeBrowsing download verdict for: "
<< item_->DebugString(true) << " verdict:" << reason
<< " result:" << static_cast<int>(result);
UMA_HISTOGRAM_ENUMERATION("SBClientDownload.CheckDownloadStats", reason,
REASON_MAX);
callback_.Run(result);
item_->RemoveObserver(this);
item_ = NULL;
DownloadProtectionService* service = service_;
service_ = NULL;
service->RequestFinished(this);
// DownloadProtectionService::RequestFinished will decrement our refcount,
// so we may be deleted now.
} else {
callback_.Run(DownloadCheckResult::UNKNOWN);
}
}
bool CheckClientDownloadRequest::CertificateChainIsWhitelisted(
const ClientDownloadRequest_CertificateChain& chain) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (chain.element_size() < 2) {
// We need to have both a signing certificate and its issuer certificate
// present to construct a whitelist entry.
return false;
}
scoped_refptr<net::X509Certificate> cert =
net::X509Certificate::CreateFromBytes(
chain.element(0).certificate().data(),
chain.element(0).certificate().size());
if (!cert.get()) {
return false;
}
for (int i = 1; i < chain.element_size(); ++i) {
scoped_refptr<net::X509Certificate> issuer =
net::X509Certificate::CreateFromBytes(
chain.element(i).certificate().data(),
chain.element(i).certificate().size());
if (!issuer.get()) {
return false;
}
std::vector<std::string> whitelist_strings;
DownloadProtectionService::GetCertificateWhitelistStrings(
*cert.get(), *issuer.get(), &whitelist_strings);
for (size_t j = 0; j < whitelist_strings.size(); ++j) {
if (database_manager_->MatchDownloadWhitelistString(
whitelist_strings[j])) {
DVLOG(2) << "Certificate matched whitelist, cert="
<< cert->subject().GetDisplayName()
<< " issuer=" << issuer->subject().GetDisplayName();
return true;
}
}
cert = issuer;
}
return false;
}
} // namespace safe_browsing