blob: 048adc80d3ebc2e50679a8468dba3f5cd127d9c5 [file] [log] [blame]
// Copyright (c) 2012 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_service.h"
#include "base/bind.h"
#include "base/compiler_specific.h"
#include "base/format_macros.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram.h"
#include "base/sequenced_task_runner_helpers.h"
#include "base/stl_util.h"
#include "base/string_number_conversions.h"
#include "base/string_util.h"
#include "base/stringprintf.h"
#include "base/threading/sequenced_worker_pool.h"
#include "base/time.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/safe_browsing/signature_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/common/safe_browsing/csd.pb.h"
#include "chrome/common/url_constants.h"
#include "chrome/common/zip_reader.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_item.h"
#include "content/public/browser/page_navigator.h"
#include "google_apis/google_api_keys.h"
#include "net/base/escape.h"
#include "net/base/load_flags.h"
#include "net/base/x509_cert_types.h"
#include "net/base/x509_certificate.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_fetcher_delegate.h"
#include "net/url_request/url_request_context_getter.h"
#include "net/url_request/url_request_status.h"
using content::BrowserThread;
namespace {
static const int64 kDownloadRequestTimeoutMs = 3000;
} // namespace
namespace safe_browsing {
const char DownloadProtectionService::kDownloadRequestUrl[] =
"https://sb-ssl.google.com/safebrowsing/clientreport/download";
namespace {
bool IsArchiveFile(const FilePath& file) {
return file.MatchesExtension(FILE_PATH_LITERAL(".zip"));
}
bool IsBinaryFile(const FilePath& file) {
return (
// Executable extensions for MS Windows.
file.MatchesExtension(FILE_PATH_LITERAL(".bas")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".bat")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".cab")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".cmd")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".com")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".exe")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".hta")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".msi")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".pif")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".reg")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".scr")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".vb")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".vbs")) ||
// Chrome extensions and android APKs are also reported.
file.MatchesExtension(FILE_PATH_LITERAL(".crx")) ||
file.MatchesExtension(FILE_PATH_LITERAL(".apk")) ||
// Archives _may_ contain binaries, we'll check in ExtractFileFeatures.
IsArchiveFile(file));
}
ClientDownloadRequest::DownloadType GetDownloadType(const FilePath& file) {
DCHECK(IsBinaryFile(file));
if (file.MatchesExtension(FILE_PATH_LITERAL(".apk")))
return ClientDownloadRequest::ANDROID_APK;
else if (file.MatchesExtension(FILE_PATH_LITERAL(".crx")))
return ClientDownloadRequest::CHROME_EXTENSION;
// For zip files, we use the ZIPPED_EXECUTABLE type since we will only send
// the pingback if we find an executable inside the zip archive.
else if (file.MatchesExtension(FILE_PATH_LITERAL(".zip")))
return ClientDownloadRequest::ZIPPED_EXECUTABLE;
return ClientDownloadRequest::WIN_EXECUTABLE;
}
// List of extensions for which we track some UMA stats.
enum MaliciousExtensionType {
EXTENSION_EXE,
EXTENSION_MSI,
EXTENSION_CAB,
EXTENSION_SYS,
EXTENSION_SCR,
EXTENSION_DRV,
EXTENSION_BAT,
EXTENSION_ZIP,
EXTENSION_RAR,
EXTENSION_DLL,
EXTENSION_PIF,
EXTENSION_COM,
EXTENSION_JAR,
EXTENSION_CLASS,
EXTENSION_PDF,
EXTENSION_VB,
EXTENSION_REG,
EXTENSION_GRP,
EXTENSION_OTHER, // Groups all other extensions into one bucket.
EXTENSION_CRX,
EXTENSION_APK,
EXTENSION_MAX,
};
MaliciousExtensionType GetExtensionType(const FilePath& f) {
if (f.MatchesExtension(FILE_PATH_LITERAL(".exe"))) return EXTENSION_EXE;
if (f.MatchesExtension(FILE_PATH_LITERAL(".msi"))) return EXTENSION_MSI;
if (f.MatchesExtension(FILE_PATH_LITERAL(".cab"))) return EXTENSION_CAB;
if (f.MatchesExtension(FILE_PATH_LITERAL(".sys"))) return EXTENSION_SYS;
if (f.MatchesExtension(FILE_PATH_LITERAL(".scr"))) return EXTENSION_SCR;
if (f.MatchesExtension(FILE_PATH_LITERAL(".drv"))) return EXTENSION_DRV;
if (f.MatchesExtension(FILE_PATH_LITERAL(".bat"))) return EXTENSION_BAT;
if (f.MatchesExtension(FILE_PATH_LITERAL(".zip"))) return EXTENSION_ZIP;
if (f.MatchesExtension(FILE_PATH_LITERAL(".rar"))) return EXTENSION_RAR;
if (f.MatchesExtension(FILE_PATH_LITERAL(".dll"))) return EXTENSION_DLL;
if (f.MatchesExtension(FILE_PATH_LITERAL(".pif"))) return EXTENSION_PIF;
if (f.MatchesExtension(FILE_PATH_LITERAL(".com"))) return EXTENSION_COM;
if (f.MatchesExtension(FILE_PATH_LITERAL(".jar"))) return EXTENSION_JAR;
if (f.MatchesExtension(FILE_PATH_LITERAL(".class"))) return EXTENSION_CLASS;
if (f.MatchesExtension(FILE_PATH_LITERAL(".pdf"))) return EXTENSION_PDF;
if (f.MatchesExtension(FILE_PATH_LITERAL(".vb"))) return EXTENSION_VB;
if (f.MatchesExtension(FILE_PATH_LITERAL(".reg"))) return EXTENSION_REG;
if (f.MatchesExtension(FILE_PATH_LITERAL(".grp"))) return EXTENSION_GRP;
if (f.MatchesExtension(FILE_PATH_LITERAL(".crx"))) return EXTENSION_CRX;
if (f.MatchesExtension(FILE_PATH_LITERAL(".apk"))) return EXTENSION_APK;
return EXTENSION_OTHER;
}
void RecordFileExtensionType(const FilePath& file) {
UMA_HISTOGRAM_ENUMERATION("SBClientDownload.DownloadExtensions",
GetExtensionType(file),
EXTENSION_MAX);
}
// Enumerate for histogramming purposes.
// DO NOT CHANGE THE ORDERING OF THESE VALUES (different histogram data will
// be mixed together based on their values).
enum SBStatsType {
DOWNLOAD_URL_CHECKS_TOTAL,
DOWNLOAD_URL_CHECKS_CANCELED,
DOWNLOAD_URL_CHECKS_MALWARE,
DOWNLOAD_HASH_CHECKS_TOTAL,
DOWNLOAD_HASH_CHECKS_MALWARE,
// Memory space for histograms is determined by the max.
// ALWAYS ADD NEW VALUES BEFORE THIS ONE.
DOWNLOAD_CHECKS_MAX
};
} // namespace
DownloadProtectionService::DownloadInfo::DownloadInfo()
: total_bytes(0), user_initiated(false), zipped_executable(false) {}
DownloadProtectionService::DownloadInfo::~DownloadInfo() {}
std::string DownloadProtectionService::DownloadInfo::DebugString() const {
std::string chain;
for (size_t i = 0; i < download_url_chain.size(); ++i) {
chain += download_url_chain[i].spec();
if (i < download_url_chain.size() - 1) {
chain += " -> ";
}
}
return base::StringPrintf(
"DownloadInfo {addr:0x%p, download_url_chain:[%s], local_file:%"
PRFilePath ", target_file:%" PRFilePath ", referrer_url:%s, "
"sha256_hash:%s, total_bytes:%" PRId64 ", user_initiated: %s, "
"zipped_executable: %s}",
reinterpret_cast<const void*>(this),
chain.c_str(),
local_file.value().c_str(),
target_file.value().c_str(),
referrer_url.spec().c_str(),
base::HexEncode(sha256_hash.data(), sha256_hash.size()).c_str(),
total_bytes,
user_initiated ? "true" : "false",
zipped_executable ? "true" : "false");
}
// static
DownloadProtectionService::DownloadInfo
DownloadProtectionService::DownloadInfo::FromDownloadItem(
const content::DownloadItem& item) {
DownloadInfo download_info;
download_info.target_file = item.GetTargetFilePath();
download_info.sha256_hash = item.GetHash();
download_info.local_file = item.GetFullPath();
download_info.download_url_chain = item.GetUrlChain();
download_info.referrer_url = item.GetReferrerUrl();
download_info.total_bytes = item.GetTotalBytes();
download_info.remote_address = item.GetRemoteAddress();
download_info.user_initiated = item.HasUserGesture();
return download_info;
}
// Parent SafeBrowsing::Client class used to lookup the bad binary
// URL and digest list. There are two sub-classes (one for each list).
class DownloadSBClient
: public SafeBrowsingService::Client,
public base::RefCountedThreadSafe<DownloadSBClient> {
public:
DownloadSBClient(
const DownloadProtectionService::DownloadInfo& info,
const DownloadProtectionService::CheckDownloadCallback& callback,
SafeBrowsingService* sb_service,
SBStatsType total_type,
SBStatsType dangerous_type)
: info_(info),
callback_(callback),
sb_service_(sb_service),
start_time_(base::TimeTicks::Now()),
total_type_(total_type),
dangerous_type_(dangerous_type) {}
virtual void StartCheck() = 0;
virtual bool IsDangerous(SafeBrowsingService::UrlCheckResult res) const = 0;
protected:
friend class base::RefCountedThreadSafe<DownloadSBClient>;
virtual ~DownloadSBClient() {}
void CheckDone(SafeBrowsingService::UrlCheckResult sb_result) {
DownloadProtectionService::DownloadCheckResult result =
IsDangerous(sb_result) ?
DownloadProtectionService::DANGEROUS :
DownloadProtectionService::SAFE;
BrowserThread::PostTask(BrowserThread::UI,
FROM_HERE,
base::Bind(callback_, result));
UpdateDownloadCheckStats(total_type_);
if (sb_result != SafeBrowsingService::SAFE) {
UpdateDownloadCheckStats(dangerous_type_);
BrowserThread::PostTask(
BrowserThread::UI,
FROM_HERE,
base::Bind(&DownloadSBClient::ReportMalware,
this, sb_result));
}
}
void ReportMalware(SafeBrowsingService::UrlCheckResult result) {
std::string post_data;
if (!info_.sha256_hash.empty())
post_data += base::HexEncode(info_.sha256_hash.data(),
info_.sha256_hash.size()) + "\n";
for (size_t i = 0; i < info_.download_url_chain.size(); ++i) {
post_data += info_.download_url_chain[i].spec() + "\n";
}
sb_service_->ReportSafeBrowsingHit(
info_.download_url_chain.back(), // malicious_url
info_.download_url_chain.front(), // page_url
info_.referrer_url,
true, // is_subresource
result,
post_data);
}
void UpdateDownloadCheckStats(SBStatsType stat_type) {
UMA_HISTOGRAM_ENUMERATION("SB2.DownloadChecks",
stat_type,
DOWNLOAD_CHECKS_MAX);
}
DownloadProtectionService::DownloadInfo info_;
DownloadProtectionService::CheckDownloadCallback callback_;
scoped_refptr<SafeBrowsingService> sb_service_;
base::TimeTicks start_time_;
private:
const SBStatsType total_type_;
const SBStatsType dangerous_type_;
DISALLOW_COPY_AND_ASSIGN(DownloadSBClient);
};
class DownloadUrlSBClient : public DownloadSBClient {
public:
DownloadUrlSBClient(
const DownloadProtectionService::DownloadInfo& info,
const DownloadProtectionService::CheckDownloadCallback& callback,
SafeBrowsingService* sb_service)
: DownloadSBClient(info, callback, sb_service,
DOWNLOAD_URL_CHECKS_TOTAL,
DOWNLOAD_URL_CHECKS_MALWARE) {}
virtual void StartCheck() OVERRIDE {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
if (!sb_service_ ||
sb_service_->CheckDownloadUrl(info_.download_url_chain, this)) {
CheckDone(SafeBrowsingService::SAFE);
} else {
AddRef(); // SafeBrowsingService takes a pointer not a scoped_refptr.
}
}
virtual bool IsDangerous(
SafeBrowsingService::UrlCheckResult result) const OVERRIDE {
return result == SafeBrowsingService::BINARY_MALWARE_URL;
}
virtual void OnDownloadUrlCheckResult(
const std::vector<GURL>& url_chain,
SafeBrowsingService::UrlCheckResult sb_result) OVERRIDE {
CheckDone(sb_result);
UMA_HISTOGRAM_TIMES("SB2.DownloadUrlCheckDuration",
base::TimeTicks::Now() - start_time_);
Release();
}
protected:
virtual ~DownloadUrlSBClient() {}
private:
DISALLOW_COPY_AND_ASSIGN(DownloadUrlSBClient);
};
class DownloadProtectionService::CheckClientDownloadRequest
: public base::RefCountedThreadSafe<
DownloadProtectionService::CheckClientDownloadRequest,
BrowserThread::DeleteOnUIThread>,
public net::URLFetcherDelegate {
public:
CheckClientDownloadRequest(const DownloadInfo& info,
const CheckDownloadCallback& callback,
DownloadProtectionService* service,
SafeBrowsingService* sb_service,
SignatureUtil* signature_util)
: info_(info),
callback_(callback),
service_(service),
signature_util_(signature_util),
sb_service_(sb_service),
pingback_enabled_(service_->enabled()),
finished_(false),
type_(ClientDownloadRequest::WIN_EXECUTABLE),
ALLOW_THIS_IN_INITIALIZER_LIST(timeout_weakptr_factory_(this)),
start_time_(base::TimeTicks::Now()) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}
void Start() {
VLOG(2) << "Starting SafeBrowsing download check for: "
<< info_.DebugString();
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// TODO(noelutz): implement some cache to make sure we don't issue the same
// request over and over again if a user downloads the same binary multiple
// times.
DownloadCheckResultReason reason = REASON_MAX;
if (!IsSupportedDownload(info_, &reason, &type_)) {
switch (reason) {
case REASON_EMPTY_URL_CHAIN:
case REASON_INVALID_URL:
RecordImprovedProtectionStats(reason);
PostFinishTask(SAFE);
return;
case REASON_NOT_BINARY_FILE:
RecordFileExtensionType(info_.target_file);
RecordImprovedProtectionStats(reason);
PostFinishTask(SAFE);
return;
default:
// We only expect the reasons explicitly handled above.
NOTREACHED();
}
}
RecordFileExtensionType(info_.target_file);
// 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. Since we do blocking I/O, offload this to a worker thread.
// The task does not need to block shutdown.
BrowserThread::GetBlockingPool()->PostWorkerTaskWithShutdownBehavior(
FROM_HERE,
base::Bind(&CheckClientDownloadRequest::ExtractFileFeatures, this),
base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN);
}
// 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 StartTimeout() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (!service_) {
// Request has already been cancelled.
return;
}
BrowserThread::PostDelayedTask(
BrowserThread::UI,
FROM_HERE,
base::Bind(&CheckClientDownloadRequest::Cancel,
timeout_weakptr_factory_.GetWeakPtr()),
base::TimeDelta::FromMilliseconds(
service_->download_request_timeout_ms()));
}
// Canceling a request will cause us to always report the result as SAFE
// unless a pending request is about to call FinishRequest.
void Cancel() {
// Calling FinishRequest might delete this object if we don't keep a
// reference around until Cancel() is finished running.
scoped_refptr<CheckClientDownloadRequest> request(this);
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
FinishRequest(SAFE);
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.
}
// From the net::URLFetcherDelegate interface.
virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK_EQ(source, fetcher_.get());
VLOG(2) << "Received a response for URL: "
<< info_.download_url_chain.back() << ": success="
<< source->GetStatus().is_success() << " response_code="
<< source->GetResponseCode();
DownloadCheckResultReason reason = REASON_SERVER_PING_FAILED;
DownloadCheckResult result = SAFE;
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;
} else if (response.verdict() == ClientDownloadResponse::SAFE) {
reason = REASON_DOWNLOAD_SAFE;
} else if (service_ && !service_->IsSupportedDownload(info_)) {
// The client of the download protection service assumes that we don't
// support this download so we cannot return any other verdict than
// SAFE even if the server says it's dangerous to download this file.
// Note: if service_ is NULL we already cancelled the request and
// returned SAFE.
reason = REASON_DOWNLOAD_NOT_SUPPORTED;
} else if (response.verdict() == ClientDownloadResponse::DANGEROUS) {
reason = REASON_DOWNLOAD_DANGEROUS;
result = DANGEROUS;
} else if (response.verdict() == ClientDownloadResponse::UNCOMMON) {
reason = REASON_DOWNLOAD_UNCOMMON;
result = UNCOMMON;
} else {
LOG(DFATAL) << "Unknown download response verdict: "
<< response.verdict();
reason = REASON_INVALID_RESPONSE_VERDICT;
}
}
// We don't need the fetcher anymore.
fetcher_.reset();
RecordImprovedProtectionStats(reason);
UMA_HISTOGRAM_TIMES("SBClientDownload.DownloadRequestDuration",
base::TimeTicks::Now() - start_time_);
FinishRequest(result);
}
static bool IsSupportedDownload(const DownloadInfo& info,
DownloadCheckResultReason* reason,
ClientDownloadRequest::DownloadType* type) {
if (info.download_url_chain.empty()) {
*reason = REASON_EMPTY_URL_CHAIN;
return false;
}
const GURL& final_url = info.download_url_chain.back();
if (!final_url.is_valid() || final_url.is_empty() ||
!final_url.IsStandard() || final_url.SchemeIsFile()) {
*reason = REASON_INVALID_URL;
return false;
}
if (!IsBinaryFile(info.target_file)) {
*reason = REASON_NOT_BINARY_FILE;
return false;
}
*type = GetDownloadType(info.target_file);
return true;
}
private:
friend struct BrowserThread::DeleteOnThread<BrowserThread::UI>;
friend class base::DeleteHelper<CheckClientDownloadRequest>;
virtual ~CheckClientDownloadRequest() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}
void ExtractFileFeatures() {
// If we're checking an archive file, look to see if there are any
// executables inside. If not, we will skip the pingback for this
// download.
if (info_.target_file.MatchesExtension(FILE_PATH_LITERAL(".zip"))) {
ExtractZipFeatures();
if (!info_.zipped_executable) {
RecordImprovedProtectionStats(REASON_ARCHIVE_WITHOUT_BINARIES);
PostFinishTask(SAFE);
return;
}
} else {
DCHECK(!IsArchiveFile(info_.target_file));
ExtractSignatureFeatures();
}
// 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::Bind(&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::Bind(&CheckClientDownloadRequest::StartTimeout, this));
}
void ExtractSignatureFeatures() {
base::TimeTicks start_time = base::TimeTicks::Now();
signature_util_->CheckSignature(info_.local_file, &signature_info_);
bool is_signed = (signature_info_.certificate_chain_size() > 0);
if (is_signed) {
VLOG(2) << "Downloaded a signed binary: " << info_.local_file.value();
} else {
VLOG(2) << "Downloaded an unsigned binary: " << info_.local_file.value();
}
UMA_HISTOGRAM_BOOLEAN("SBClientDownload.SignedBinaryDownload", is_signed);
UMA_HISTOGRAM_TIMES("SBClientDownload.ExtractSignatureFeaturesTime",
base::TimeTicks::Now() - start_time);
}
void ExtractZipFeatures() {
base::TimeTicks start_time = base::TimeTicks::Now();
zip::ZipReader reader;
bool zip_file_has_archive = false;
if (reader.Open(info_.local_file)) {
for (; reader.HasMore(); reader.AdvanceToNextEntry()) {
if (!reader.OpenCurrentEntryInZip()) {
VLOG(1) << "Failed to open current entry in zip file: "
<< info_.local_file.value();
continue;
}
const FilePath& file = reader.current_entry_info()->file_path();
if (IsBinaryFile(file)) {
// Don't consider an archived archive to be executable, but record
// a histogram.
if (IsArchiveFile(file)) {
zip_file_has_archive = true;
} else {
VLOG(2) << "Downloaded a zipped executable: "
<< info_.local_file.value();
info_.zipped_executable = true;
break;
}
} else {
VLOG(3) << "Ignoring non-binary file: " << file.value();
}
}
} else {
VLOG(1) << "Failed to open zip file: " << info_.local_file.value();
}
UMA_HISTOGRAM_BOOLEAN("SBClientDownload.ZipFileHasExecutable",
info_.zipped_executable);
UMA_HISTOGRAM_BOOLEAN("SBClientDownload.ZipFileHasArchiveButNoExecutable",
zip_file_has_archive && !info_.zipped_executable);
UMA_HISTOGRAM_TIMES("SBClientDownload.ExtractZipFeaturesTime",
base::TimeTicks::Now() - start_time);
}
void CheckWhitelists() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
DownloadCheckResultReason reason = REASON_MAX;
if (!sb_service_.get()) {
reason = REASON_SB_DISABLED;
} else {
for (size_t i = 0; i < info_.download_url_chain.size(); ++i) {
const GURL& url = info_.download_url_chain[i];
if (url.is_valid() && sb_service_->MatchDownloadWhitelistUrl(url)) {
VLOG(2) << url << " is on the download whitelist.";
reason = REASON_WHITELISTED_URL;
break;
}
}
if (info_.referrer_url.is_valid() && reason == REASON_MAX &&
sb_service_->MatchDownloadWhitelistUrl(info_.referrer_url)) {
VLOG(2) << "Referrer url " << info_.referrer_url
<< " is on the download whitelist.";
reason = REASON_WHITELISTED_REFERRER;
}
if (reason != REASON_MAX || signature_info_.trusted()) {
UMA_HISTOGRAM_COUNTS("SBClientDownload.SignedOrWhitelistedDownload", 1);
}
}
if (reason == REASON_MAX && signature_info_.trusted()) {
for (int i = 0; i < signature_info_.certificate_chain_size(); ++i) {
if (CertificateChainIsWhitelisted(
signature_info_.certificate_chain(i))) {
reason = REASON_TRUSTED_EXECUTABLE;
break;
}
}
}
if (reason != REASON_MAX) {
RecordImprovedProtectionStats(reason);
PostFinishTask(SAFE);
} else if (!pingback_enabled_) {
RecordImprovedProtectionStats(REASON_PING_DISABLED);
PostFinishTask(SAFE);
} else {
// Currently, the UI only works on Windows so we don't even bother
// with pinging the server if we're not on Windows. TODO(noelutz):
// change this code once the UI is done for Linux and Mac.
#if defined(OS_WIN)
// The URLFetcher is owned by the UI thread, so post a message to
// start the pingback.
BrowserThread::PostTask(
BrowserThread::UI,
FROM_HERE,
base::Bind(&CheckClientDownloadRequest::SendRequest, this));
#else
RecordImprovedProtectionStats(REASON_OS_NOT_SUPPORTED);
PostFinishTask(SAFE);
#endif
}
}
void SendRequest() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// This is our last chance to check whether the request has been canceled
// before sending it.
if (!service_) {
RecordImprovedProtectionStats(REASON_REQUEST_CANCELED);
FinishRequest(SAFE);
return;
}
ClientDownloadRequest request;
request.set_url(info_.download_url_chain.back().spec());
request.mutable_digests()->set_sha256(info_.sha256_hash);
request.set_length(info_.total_bytes);
for (size_t i = 0; i < info_.download_url_chain.size(); ++i) {
ClientDownloadRequest::Resource* resource = request.add_resources();
resource->set_url(info_.download_url_chain[i].spec());
if (i == info_.download_url_chain.size() - 1) {
// The last URL in the chain is the download URL.
resource->set_type(ClientDownloadRequest::DOWNLOAD_URL);
resource->set_referrer(info_.referrer_url.spec());
if (!info_.remote_address.empty()) {
resource->set_remote_ip(info_.remote_address);
}
} else {
resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT);
}
// TODO(noelutz): fill out the remote IP addresses.
}
request.set_user_initiated(info_.user_initiated);
request.set_file_basename(info_.target_file.BaseName().AsUTF8Unsafe());
request.set_download_type(type_);
request.mutable_signature()->CopyFrom(signature_info_);
std::string request_data;
if (!request.SerializeToString(&request_data)) {
RecordImprovedProtectionStats(REASON_INVALID_REQUEST_PROTO);
FinishRequest(SAFE);
return;
}
VLOG(2) << "Sending a request for URL: "
<< info_.download_url_chain.back();
fetcher_.reset(net::URLFetcher::Create(0 /* ID used for testing */,
GURL(GetDownloadRequestUrl()),
net::URLFetcher::POST,
this));
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", request_data);
fetcher_->Start();
}
void PostFinishTask(DownloadCheckResult result) {
BrowserThread::PostTask(
BrowserThread::UI,
FROM_HERE,
base::Bind(&CheckClientDownloadRequest::FinishRequest, this, result));
}
void FinishRequest(DownloadCheckResult result) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (finished_) {
return;
}
finished_ = true;
if (service_) {
callback_.Run(result);
DownloadProtectionService* service = service_;
service_ = NULL;
service->RequestFinished(this);
// DownloadProtectionService::RequestFinished will decrement our refcount,
// so we may be deleted now.
} else {
callback_.Run(SAFE);
}
}
void RecordImprovedProtectionStats(DownloadCheckResultReason reason) {
VLOG(2) << "SafeBrowsing download verdict for: "
<< info_.DebugString() << " verdict:" << reason;
UMA_HISTOGRAM_ENUMERATION("SBClientDownload.CheckDownloadStats",
reason,
REASON_MAX);
}
bool CertificateChainIsWhitelisted(
const ClientDownloadRequest_CertificateChain& chain) {
DCHECK(BrowserThread::CurrentlyOn(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, *issuer, &whitelist_strings);
for (size_t j = 0; j < whitelist_strings.size(); ++j) {
if (sb_service_->MatchDownloadWhitelistString(whitelist_strings[j])) {
VLOG(2) << "Certificate matched whitelist, cert="
<< cert->subject().GetDisplayName()
<< " issuer=" << issuer->subject().GetDisplayName();
return true;
}
}
cert = issuer;
}
return false;
}
DownloadInfo info_;
ClientDownloadRequest_SignatureInfo signature_info_;
CheckDownloadCallback callback_;
// Will be NULL if the request has been canceled.
DownloadProtectionService* service_;
scoped_refptr<SignatureUtil> signature_util_;
scoped_refptr<SafeBrowsingService> sb_service_;
const bool pingback_enabled_;
scoped_ptr<net::URLFetcher> fetcher_;
bool finished_;
ClientDownloadRequest::DownloadType type_;
base::WeakPtrFactory<CheckClientDownloadRequest> timeout_weakptr_factory_;
base::TimeTicks start_time_; // Used for stats.
DISALLOW_COPY_AND_ASSIGN(CheckClientDownloadRequest);
};
DownloadProtectionService::DownloadProtectionService(
SafeBrowsingService* sb_service,
net::URLRequestContextGetter* request_context_getter)
: sb_service_(sb_service),
request_context_getter_(request_context_getter),
enabled_(false),
signature_util_(new SignatureUtil()),
download_request_timeout_ms_(kDownloadRequestTimeoutMs) {}
DownloadProtectionService::~DownloadProtectionService() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
CancelPendingRequests();
}
void DownloadProtectionService::SetEnabled(bool enabled) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (enabled == enabled_) {
return;
}
enabled_ = enabled;
if (!enabled_) {
CancelPendingRequests();
}
}
void DownloadProtectionService::CheckClientDownload(
const DownloadProtectionService::DownloadInfo& info,
const CheckDownloadCallback& callback) {
scoped_refptr<CheckClientDownloadRequest> request(
new CheckClientDownloadRequest(info, callback, this,
sb_service_, signature_util_.get()));
download_requests_.insert(request);
request->Start();
}
void DownloadProtectionService::CheckDownloadUrl(
const DownloadProtectionService::DownloadInfo& info,
const CheckDownloadCallback& callback) {
DCHECK(!info.download_url_chain.empty());
scoped_refptr<DownloadUrlSBClient> client(
new DownloadUrlSBClient(info, callback, sb_service_));
// The client will release itself once it is done.
BrowserThread::PostTask(
BrowserThread::IO,
FROM_HERE,
base::Bind(&DownloadUrlSBClient::StartCheck, client));
}
bool DownloadProtectionService::IsSupportedDownload(
const DownloadInfo& info) const {
// Currently, the UI only works on Windows. On Linux and Mac we still
// want to show the dangerous file type warning if the file is possibly
// dangerous which means we have to always return false here.
#if defined(OS_WIN)
DownloadCheckResultReason reason = REASON_MAX;
ClientDownloadRequest::DownloadType type =
ClientDownloadRequest::WIN_EXECUTABLE;
return (CheckClientDownloadRequest::IsSupportedDownload(info,
&reason,
&type) &&
(ClientDownloadRequest::ANDROID_APK == type ||
ClientDownloadRequest::WIN_EXECUTABLE == type ||
ClientDownloadRequest::ZIPPED_EXECUTABLE == type));
#else
return false;
#endif
}
void DownloadProtectionService::CancelPendingRequests() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
for (std::set<scoped_refptr<CheckClientDownloadRequest> >::iterator it =
download_requests_.begin();
it != download_requests_.end();) {
// We need to advance the iterator before we cancel because canceling
// the request will invalidate it when RequestFinished is called below.
scoped_refptr<CheckClientDownloadRequest> tmp = *it++;
tmp->Cancel();
}
DCHECK(download_requests_.empty());
}
void DownloadProtectionService::RequestFinished(
CheckClientDownloadRequest* request) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
std::set<scoped_refptr<CheckClientDownloadRequest> >::iterator it =
download_requests_.find(request);
DCHECK(it != download_requests_.end());
download_requests_.erase(*it);
}
void DownloadProtectionService::ShowDetailsForDownload(
const DownloadProtectionService::DownloadInfo& info,
content::PageNavigator* navigator) {
navigator->OpenURL(
content::OpenURLParams(GURL(chrome::kDownloadScanningLearnMoreURL),
content::Referrer(),
NEW_FOREGROUND_TAB,
content::PAGE_TRANSITION_LINK,
false));
}
namespace {
// Escapes a certificate attribute so that it can be used in a whitelist
// entry. Currently, we only escape slashes, since they are used as a
// separator between attributes.
std::string EscapeCertAttribute(const std::string& attribute) {
std::string escaped;
for (size_t i = 0; i < attribute.size(); ++i) {
if (attribute[i] == '%') {
escaped.append("%25");
} else if (attribute[i] == '/') {
escaped.append("%2F");
} else {
escaped.push_back(attribute[i]);
}
}
return escaped;
}
} // namespace
// static
void DownloadProtectionService::GetCertificateWhitelistStrings(
const net::X509Certificate& certificate,
const net::X509Certificate& issuer,
std::vector<std::string>* whitelist_strings) {
// The whitelist paths are in the format:
// cert/<ascii issuer fingerprint>[/CN=common_name][/O=org][/OU=unit]
//
// Any of CN, O, or OU may be omitted from the whitelist entry, in which
// case they match anything. However, the attributes that do appear will
// always be in the order shown above. At least one attribute will always
// be present.
const net::CertPrincipal& subject = certificate.subject();
std::vector<std::string> ou_tokens;
for (size_t i = 0; i < subject.organization_unit_names.size(); ++i) {
ou_tokens.push_back(
"/OU=" + EscapeCertAttribute(subject.organization_unit_names[i]));
}
std::vector<std::string> o_tokens;
for (size_t i = 0; i < subject.organization_names.size(); ++i) {
o_tokens.push_back(
"/O=" + EscapeCertAttribute(subject.organization_names[i]));
}
std::string cn_token;
if (!subject.common_name.empty()) {
cn_token = "/CN=" + EscapeCertAttribute(subject.common_name);
}
std::set<std::string> paths_to_check;
if (!cn_token.empty()) {
paths_to_check.insert(cn_token);
}
for (size_t i = 0; i < o_tokens.size(); ++i) {
paths_to_check.insert(cn_token + o_tokens[i]);
paths_to_check.insert(o_tokens[i]);
for (size_t j = 0; j < ou_tokens.size(); ++j) {
paths_to_check.insert(cn_token + o_tokens[i] + ou_tokens[j]);
paths_to_check.insert(o_tokens[i] + ou_tokens[j]);
}
}
for (size_t i = 0; i < ou_tokens.size(); ++i) {
paths_to_check.insert(cn_token + ou_tokens[i]);
paths_to_check.insert(ou_tokens[i]);
}
std::string issuer_fp = base::HexEncode(issuer.fingerprint().data,
sizeof(issuer.fingerprint().data));
for (std::set<std::string>::iterator it = paths_to_check.begin();
it != paths_to_check.end(); ++it) {
whitelist_strings->push_back("cert/" + issuer_fp + *it);
}
}
// static
std::string DownloadProtectionService::GetDownloadRequestUrl() {
std::string url = kDownloadRequestUrl;
std::string api_key = google_apis::GetAPIKey();
if (!api_key.empty()) {
base::StringAppendF(&url, "?key=%s",
net::EscapeQueryParamValue(api_key, true).c_str());
}
return url;
}
} // namespace safe_browsing