| // 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 <stddef.h> |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/command_line.h" |
| #include "base/compiler_specific.h" |
| #include "base/format_macros.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/sparse_histogram.h" |
| #include "base/rand_util.h" |
| #include "base/scoped_observer.h" |
| #include "base/sequenced_task_runner_helpers.h" |
| #include "base/sha1.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task/cancelable_task_tracker.h" |
| #include "base/task_scheduler/post_task.h" |
| #include "base/task_scheduler/task_traits.h" |
| #include "base/threading/sequenced_worker_pool.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/history/history_service_factory.h" |
| #include "chrome/browser/metrics/chrome_metrics_service_accessor.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/safe_browsing/download_feedback_service.h" |
| #include "chrome/browser/safe_browsing/safe_browsing_service.h" |
| #include "chrome/browser/safe_browsing/sandboxed_zip_analyzer.h" |
| #include "chrome/browser/sessions/session_tab_helper.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/safe_browsing/archive_analyzer_results.h" |
| #include "chrome/common/safe_browsing/binary_feature_extractor.h" |
| #include "chrome/common/safe_browsing/download_protection_util.h" |
| #include "chrome/common/safe_browsing/file_type_policies.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/data_use_measurement/core/data_use_user_data.h" |
| #include "components/google/core/browser/google_util.h" |
| #include "components/history/core/browser/history_service.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/safe_browsing/common/safe_browsing_prefs.h" |
| #include "components/safe_browsing/common/safebrowsing_switches.h" |
| #include "components/safe_browsing/common/utils.h" |
| #include "components/safe_browsing/proto/csd.pb.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/url_util.h" |
| #include "net/cert/x509_cert_types.h" |
| #include "net/cert/x509_certificate.h" |
| #include "net/http/http_status_code.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "net/url_request/url_fetcher.h" |
| #include "net/url_request/url_fetcher_delegate.h" |
| #include "net/url_request/url_request_status.h" |
| |
| #if defined(OS_MACOSX) |
| #include "chrome/browser/safe_browsing/disk_image_type_sniffer_mac.h" |
| #include "chrome/browser/safe_browsing/sandboxed_dmg_analyzer_mac.h" |
| #endif |
| |
| using content::BrowserThread; |
| namespace safe_browsing { |
| |
| namespace { |
| |
| const int64_t kDownloadRequestTimeoutMs = 7000; |
| // We sample 1% of whitelisted downloads to still send out download pings. |
| const double kWhitelistDownloadSampleRate = 0.01; |
| |
| // The number of user gestures we trace back for download attribution. |
| const int kDownloadAttributionUserGestureLimit = 2; |
| |
| const char kDownloadExtensionUmaName[] = "SBClientDownload.DownloadExtensions"; |
| const char kUnsupportedSchemeUmaPrefix[] = "SBClientDownload.UnsupportedScheme"; |
| |
| const void* const kDownloadReferrerChainDataKey = |
| &kDownloadReferrerChainDataKey; |
| |
| enum WhitelistType { |
| NO_WHITELIST_MATCH, |
| URL_WHITELIST, |
| SIGNATURE_WHITELIST, |
| WHITELIST_TYPE_MAX |
| }; |
| |
| void RecordCountOfWhitelistedDownload(WhitelistType type) { |
| UMA_HISTOGRAM_ENUMERATION("SBClientDownload.CheckWhitelistResult", type, |
| WHITELIST_TYPE_MAX); |
| } |
| |
| 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"; |
| } |
| |
| // 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 |
| }; |
| |
| void AddEventUrlToReferrerChain(const content::DownloadItem& item, |
| ReferrerChain* out_referrer_chain) { |
| ReferrerChainEntry* event_url_entry = out_referrer_chain->Add(); |
| event_url_entry->set_url(item.GetURL().spec()); |
| event_url_entry->set_type(ReferrerChainEntry::EVENT_URL); |
| event_url_entry->set_referrer_url( |
| item.GetWebContents()->GetLastCommittedURL().spec()); |
| event_url_entry->set_is_retargeting(false); |
| event_url_entry->set_navigation_time_msec(base::Time::Now().ToJavaTime()); |
| for (const GURL& url : item.GetUrlChain()) |
| event_url_entry->add_server_redirect_chain()->set_url(url.spec()); |
| } |
| |
| } // namespace |
| |
| const char DownloadProtectionService::kDownloadRequestUrl[] = |
| "https://sb-ssl.google.com/safebrowsing/clientreport/download"; |
| |
| const void* const DownloadProtectionService::kDownloadPingTokenKey = |
| &kDownloadPingTokenKey; |
| |
| // SafeBrowsing::Client class used to lookup the bad binary URL list. |
| |
| class DownloadUrlSBClient |
| : public SafeBrowsingDatabaseManager::Client, |
| public content::DownloadItem::Observer, |
| public base::RefCountedThreadSafe< |
| DownloadUrlSBClient, |
| BrowserThread::DeleteOnUIThread> { |
| public: |
| DownloadUrlSBClient( |
| content::DownloadItem* item, |
| DownloadProtectionService* service, |
| const DownloadProtectionService::CheckDownloadCallback& callback, |
| const scoped_refptr<SafeBrowsingUIManager>& ui_manager, |
| const scoped_refptr<SafeBrowsingDatabaseManager>& database_manager) |
| : item_(item), |
| sha256_hash_(item->GetHash()), |
| url_chain_(item->GetUrlChain()), |
| referrer_url_(item->GetReferrerUrl()), |
| service_(service), |
| callback_(callback), |
| ui_manager_(ui_manager), |
| start_time_(base::TimeTicks::Now()), |
| total_type_(DOWNLOAD_URL_CHECKS_TOTAL), |
| dangerous_type_(DOWNLOAD_URL_CHECKS_MALWARE), |
| database_manager_(database_manager), |
| download_item_observer_(this) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(item_); |
| DCHECK(service_); |
| download_item_observer_.Add(item_); |
| Profile* profile = Profile::FromBrowserContext(item_->GetBrowserContext()); |
| extended_reporting_level_ = |
| profile ? GetExtendedReportingLevel(*profile->GetPrefs()) |
| : SBER_LEVEL_OFF; |
| } |
| |
| // Implements DownloadItem::Observer. |
| void OnDownloadDestroyed(content::DownloadItem* download) override { |
| download_item_observer_.Remove(item_); |
| item_ = nullptr; |
| } |
| |
| void StartCheck() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| if (!database_manager_.get() || |
| database_manager_->CheckDownloadUrl(url_chain_, this)) { |
| CheckDone(SB_THREAT_TYPE_SAFE); |
| } else { |
| // Add a reference to this object to prevent it from being destroyed |
| // before url checking result is returned. |
| AddRef(); |
| } |
| } |
| |
| bool IsDangerous(SBThreatType threat_type) const { |
| return threat_type == SB_THREAT_TYPE_URL_BINARY_MALWARE; |
| } |
| |
| // Implements SafeBrowsingDatabaseManager::Client. |
| void OnCheckDownloadUrlResult(const std::vector<GURL>& url_chain, |
| SBThreatType threat_type) override { |
| CheckDone(threat_type); |
| UMA_HISTOGRAM_TIMES("SB2.DownloadUrlCheckDuration", |
| base::TimeTicks::Now() - start_time_); |
| Release(); |
| } |
| |
| private: |
| friend class base::RefCountedThreadSafe<DownloadUrlSBClient>; |
| friend struct BrowserThread::DeleteOnThread<BrowserThread::UI>; |
| friend class base::DeleteHelper<DownloadUrlSBClient>; |
| |
| ~DownloadUrlSBClient() override { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| } |
| |
| void CheckDone(SBThreatType threat_type) { |
| DownloadProtectionService::DownloadCheckResult result = |
| IsDangerous(threat_type) ? |
| DownloadProtectionService::DANGEROUS : |
| DownloadProtectionService::SAFE; |
| UpdateDownloadCheckStats(total_type_); |
| if (threat_type != SB_THREAT_TYPE_SAFE) { |
| UpdateDownloadCheckStats(dangerous_type_); |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&DownloadUrlSBClient::ReportMalware, this, |
| threat_type)); |
| } else { |
| // Identify download referrer chain, which will be used in |
| // ClientDownloadRequest. |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&DownloadUrlSBClient::IdentifyReferrerChain, this)); |
| } |
| BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| base::BindOnce(callback_, result)); |
| } |
| |
| void ReportMalware(SBThreatType threat_type) { |
| std::string post_data; |
| if (!sha256_hash_.empty()) { |
| post_data += base::HexEncode(sha256_hash_.data(), |
| sha256_hash_.size()) + "\n"; |
| } |
| for (size_t i = 0; i < url_chain_.size(); ++i) { |
| post_data += url_chain_[i].spec() + "\n"; |
| } |
| |
| safe_browsing::HitReport hit_report; |
| hit_report.malicious_url = url_chain_.back(); |
| hit_report.page_url = url_chain_.front(); |
| hit_report.referrer_url = referrer_url_; |
| hit_report.is_subresource = true; |
| hit_report.threat_type = threat_type; |
| // TODO(nparker) Replace this with database_manager_->GetThreatSource(); |
| hit_report.threat_source = safe_browsing::ThreatSource::LOCAL_PVER3; |
| // TODO(nparker) Populate hit_report.population_id once Pver4 is used here. |
| hit_report.post_data = post_data; |
| hit_report.extended_reporting_level = extended_reporting_level_; |
| hit_report.is_metrics_reporting_active = |
| ChromeMetricsServiceAccessor::IsMetricsAndCrashReportingEnabled(); |
| |
| ui_manager_->MaybeReportSafeBrowsingHit(hit_report, |
| item_->GetWebContents()); |
| } |
| |
| void IdentifyReferrerChain() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!item_) |
| return; |
| |
| item_->SetUserData(kDownloadReferrerChainDataKey, |
| base::MakeUnique<ReferrerChainData>( |
| service_->IdentifyReferrerChain(*item_))); |
| } |
| |
| void UpdateDownloadCheckStats(SBStatsType stat_type) { |
| UMA_HISTOGRAM_ENUMERATION("SB2.DownloadChecks", |
| stat_type, |
| DOWNLOAD_CHECKS_MAX); |
| } |
| |
| // The DownloadItem we are checking. Must be accessed only on UI thread. |
| content::DownloadItem* item_; |
| // Copies of data from |item_| for access on other threads. |
| std::string sha256_hash_; |
| std::vector<GURL> url_chain_; |
| GURL referrer_url_; |
| DownloadProtectionService* service_; |
| DownloadProtectionService::CheckDownloadCallback callback_; |
| scoped_refptr<SafeBrowsingUIManager> ui_manager_; |
| base::TimeTicks start_time_; |
| const SBStatsType total_type_; |
| const SBStatsType dangerous_type_; |
| ExtendedReportingLevel extended_reporting_level_; |
| scoped_refptr<SafeBrowsingDatabaseManager> database_manager_; |
| ScopedObserver<content::DownloadItem, |
| content::DownloadItem::Observer> download_item_observer_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DownloadUrlSBClient); |
| }; |
| |
| class DownloadProtectionService::CheckClientDownloadRequest |
| : public base::RefCountedThreadSafe< |
| DownloadProtectionService::CheckClientDownloadRequest, |
| BrowserThread::DeleteOnUIThread>, |
| public net::URLFetcherDelegate, |
| public content::DownloadItem::Observer { |
| public: |
| 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 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 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(UNKNOWN, reason); |
| return; |
| case REASON_UNSUPPORTED_URL_SCHEME: |
| RecordFileExtensionType( |
| base::StringPrintf( |
| "%s.%s", kUnsupportedSchemeUmaPrefix, |
| GetUnsupportedSchemeName(item_->GetUrlChain().back()) |
| .c_str()), |
| item_->GetTargetFilePath()); |
| PostFinishTask(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(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. |
| BrowserThread::PostTaskAndReplyWithResult( |
| BrowserThread::FILE, FROM_HERE, |
| 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 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 UNKNOWN |
| // unless a pending request is about to call FinishRequest. |
| void 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(UNKNOWN, REASON_REQUEST_CANCELED); |
| // Calling FinishRequest might delete this object, we may be deleted by |
| // this point. |
| } |
| |
| // content::DownloadItem::Observer implementation. |
| void OnDownloadDestroyed(content::DownloadItem* download) override { |
| Cancel(); |
| DCHECK(item_ == NULL); |
| } |
| |
| // From the net::URLFetcherDelegate interface. |
| void OnURLFetchComplete(const net::URLFetcher* source) override { |
| 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 = 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 = 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 = UNKNOWN; |
| } else { |
| switch (response.verdict()) { |
| case ClientDownloadResponse::SAFE: |
| reason = REASON_DOWNLOAD_SAFE; |
| result = SAFE; |
| break; |
| case ClientDownloadResponse::DANGEROUS: |
| reason = REASON_DOWNLOAD_DANGEROUS; |
| result = DANGEROUS; |
| token = response.token(); |
| break; |
| case ClientDownloadResponse::UNCOMMON: |
| reason = REASON_DOWNLOAD_UNCOMMON; |
| result = UNCOMMON; |
| token = response.token(); |
| break; |
| case ClientDownloadResponse::DANGEROUS_HOST: |
| reason = REASON_DOWNLOAD_DANGEROUS_HOST; |
| result = DANGEROUS_HOST; |
| token = response.token(); |
| break; |
| case ClientDownloadResponse::POTENTIALLY_UNWANTED: |
| reason = REASON_DOWNLOAD_POTENTIALLY_UNWANTED; |
| result = POTENTIALLY_UNWANTED; |
| token = response.token(); |
| break; |
| case ClientDownloadResponse::UNKNOWN: |
| reason = REASON_VERDICT_UNKNOWN; |
| result = UNKNOWN; |
| break; |
| default: |
| LOG(DFATAL) << "Unknown download response verdict: " |
| << response.verdict(); |
| reason = REASON_INVALID_RESPONSE_VERDICT; |
| result = UNKNOWN; |
| } |
| } |
| |
| if (!token.empty()) |
| 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 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; |
| } |
| |
| private: |
| friend struct BrowserThread::DeleteOnThread<BrowserThread::UI>; |
| friend class base::DeleteHelper<CheckClientDownloadRequest>; |
| |
| ~CheckClientDownloadRequest() override { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK(item_ == NULL); |
| } |
| |
| void 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 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 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 /* signed_data */)) { |
| image_headers_.reset(); |
| } |
| UMA_HISTOGRAM_TIMES("SBClientDownload.ExtractImageHeadersTime", |
| base::TimeTicks::Now() - start_time); |
| |
| OnFileFeatureExtractionDone(); |
| } |
| |
| void 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 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(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 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 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 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(SAFE, REASON_ARCHIVE_WITHOUT_BINARIES); |
| return; |
| } |
| } |
| |
| OnFileFeatureExtractionDone(); |
| } |
| #endif // defined(OS_MACOSX) |
| |
| bool 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 CheckWhitelists() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| |
| if (!database_manager_.get()) { |
| PostFinishTask(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(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(SAFE, REASON_TRUSTED_EXECUTABLE); |
| return; |
| } |
| } |
| } |
| } |
| |
| RecordCountOfWhitelistedDownload(NO_WHITELIST_MATCH); |
| |
| if (!pingback_enabled_) { |
| PostFinishTask(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 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 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 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 SanitizeUrl(const GURL& url) const { |
| if (type_ == ClientDownloadRequest::SAMPLED_UNSUPPORTED_FILE) |
| return url.GetOrigin().spec(); |
| |
| return ShortURLForReporting(url); |
| } |
| |
| void 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(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(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: YES |
| 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 /* ID used for testing */, 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 PostFinishTask(DownloadCheckResult result, |
| DownloadCheckResultReason reason) { |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&CheckClientDownloadRequest::FinishRequest, this, result, |
| reason)); |
| } |
| |
| void 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:" << 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(UNKNOWN); |
| } |
| } |
| |
| bool 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; |
| } |
| |
| enum class ArchiveValid { UNSET, VALID, INVALID }; |
| |
| // The DownloadItem we are checking. Will be NULL if the request has been |
| // canceled. Must be accessed only on UI thread. |
| content::DownloadItem* item_; |
| // Copies of data from |item_| for access on other threads. |
| std::vector<GURL> url_chain_; |
| GURL referrer_url_; |
| // URL chain of redirects leading to (but not including) |tab_url|. |
| std::vector<GURL> tab_redirects_; |
| // URL and referrer of the window the download was started from. |
| GURL tab_url_; |
| GURL tab_referrer_url_; |
| |
| bool archived_executable_; |
| ArchiveValid archive_is_valid_; |
| |
| #if defined(OS_MACOSX) |
| std::unique_ptr<std::vector<uint8_t>> disk_image_signature_; |
| #endif |
| |
| ClientDownloadRequest_SignatureInfo signature_info_; |
| std::unique_ptr<ClientDownloadRequest_ImageHeaders> image_headers_; |
| google::protobuf::RepeatedPtrField<ClientDownloadRequest_ArchivedBinary> |
| archived_binary_; |
| CheckDownloadCallback callback_; |
| // Will be NULL if the request has been canceled. |
| DownloadProtectionService* service_; |
| scoped_refptr<BinaryFeatureExtractor> binary_feature_extractor_; |
| scoped_refptr<SafeBrowsingDatabaseManager> database_manager_; |
| const bool pingback_enabled_; |
| std::unique_ptr<net::URLFetcher> fetcher_; |
| scoped_refptr<SandboxedZipAnalyzer> analyzer_; |
| base::TimeTicks zip_analysis_start_time_; |
| #if defined(OS_MACOSX) |
| scoped_refptr<SandboxedDMGAnalyzer> dmg_analyzer_; |
| base::TimeTicks dmg_analysis_start_time_; |
| #endif |
| bool finished_; |
| ClientDownloadRequest::DownloadType type_; |
| std::string client_download_request_data_; |
| base::CancelableTaskTracker request_tracker_; // For HistoryService lookup. |
| base::TimeTicks start_time_; // Used for stats. |
| base::TimeTicks timeout_start_time_; |
| base::TimeTicks request_start_time_; |
| bool skipped_url_whitelist_; |
| bool skipped_certificate_whitelist_; |
| bool is_extended_reporting_; |
| bool is_incognito_; |
| base::WeakPtrFactory<CheckClientDownloadRequest> weakptr_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CheckClientDownloadRequest); |
| }; |
| |
| // A request for checking whether a PPAPI initiated download is safe. |
| // |
| // These are considered different from DownloadManager mediated downloads |
| // because: |
| // |
| // * The download bytes are produced by the PPAPI plugin *after* the check |
| // returns due to architectural constraints. |
| // |
| // * Since the download bytes are produced by the PPAPI plugin, there's no |
| // reliable network request information to associate with the download. |
| // |
| // PPAPIDownloadRequest objects are owned by the DownloadProtectionService |
| // indicated by |service|. |
| class DownloadProtectionService::PPAPIDownloadRequest |
| : public net::URLFetcherDelegate { |
| public: |
| // The outcome of the request. These values are used for UMA. New values |
| // should only be added at the end. |
| enum class RequestOutcome : int { |
| UNKNOWN, |
| REQUEST_DESTROYED, |
| UNSUPPORTED_FILE_TYPE, |
| TIMEDOUT, |
| WHITELIST_HIT, |
| REQUEST_MALFORMED, |
| FETCH_FAILED, |
| RESPONSE_MALFORMED, |
| SUCCEEDED |
| }; |
| |
| PPAPIDownloadRequest( |
| const GURL& requestor_url, |
| const GURL& initiating_frame_url, |
| content::WebContents* web_contents, |
| const base::FilePath& default_file_path, |
| const std::vector<base::FilePath::StringType>& alternate_extensions, |
| Profile* profile, |
| const CheckDownloadCallback& callback, |
| DownloadProtectionService* service, |
| scoped_refptr<SafeBrowsingDatabaseManager> database_manager) |
| : requestor_url_(requestor_url), |
| initiating_frame_url_(initiating_frame_url), |
| initiating_main_frame_url_( |
| web_contents ? web_contents->GetLastCommittedURL() : GURL()), |
| tab_id_(SessionTabHelper::IdForTab(web_contents)), |
| default_file_path_(default_file_path), |
| alternate_extensions_(alternate_extensions), |
| callback_(callback), |
| service_(service), |
| database_manager_(database_manager), |
| start_time_(base::TimeTicks::Now()), |
| supported_path_( |
| GetSupportedFilePath(default_file_path, alternate_extensions)), |
| weakptr_factory_(this) { |
| DCHECK(profile); |
| is_extended_reporting_ = IsExtendedReportingEnabled(*profile->GetPrefs()); |
| |
| if (service->navigation_observer_manager()) { |
| has_user_gesture_ = |
| service->navigation_observer_manager()->HasUserGesture(web_contents); |
| if (has_user_gesture_) { |
| service->navigation_observer_manager()->OnUserGestureConsumed( |
| web_contents, base::Time::Now()); |
| } |
| } |
| } |
| |
| ~PPAPIDownloadRequest() override { |
| if (fetcher_ && !callback_.is_null()) |
| Finish(RequestOutcome::REQUEST_DESTROYED, UNKNOWN); |
| } |
| |
| // Start the process of checking the download request. The callback passed as |
| // the |callback| parameter to the constructor will be invoked with the result |
| // of the check at some point in the future. |
| // |
| // From the this point on, the code is arranged to follow the most common |
| // workflow. |
| // |
| // Note that |this| should be added to the list of pending requests in the |
| // associated DownloadProtectionService object *before* calling Start(). |
| // Otherwise a synchronous Finish() call may result in leaking the |
| // PPAPIDownloadRequest object. This is enforced via a DCHECK in |
| // DownloadProtectionService. |
| void Start() { |
| DVLOG(2) << "Starting SafeBrowsing download check for PPAPI download from " |
| << requestor_url_ << " for [" << default_file_path_.value() << "] " |
| << "supported path is [" << supported_path_.value() << "]"; |
| |
| if (supported_path_.empty()) { |
| // Neither the default_file_path_ nor any path resulting of trying out |
| // |alternate_extensions_| are supported by SafeBrowsing. |
| Finish(RequestOutcome::UNSUPPORTED_FILE_TYPE, SAFE); |
| return; |
| } |
| |
| // In case the request take too long, the check will abort with an UNKNOWN |
| // verdict. The weak pointer used for the timeout will be invalidated (and |
| // hence would prevent the timeout) if the check completes on time and |
| // execution reaches Finish(). |
| BrowserThread::PostDelayedTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&PPAPIDownloadRequest::OnRequestTimedOut, |
| weakptr_factory_.GetWeakPtr()), |
| base::TimeDelta::FromMilliseconds( |
| service_->download_request_timeout_ms())); |
| |
| BrowserThread::PostTask( |
| BrowserThread::IO, FROM_HERE, |
| base::BindOnce(&PPAPIDownloadRequest::CheckWhitelistsOnIOThread, |
| requestor_url_, database_manager_, |
| weakptr_factory_.GetWeakPtr())); |
| } |
| |
| private: |
| // Whitelist checking needs to the done on the IO thread. |
| static void CheckWhitelistsOnIOThread( |
| const GURL& requestor_url, |
| scoped_refptr<SafeBrowsingDatabaseManager> database_manager, |
| base::WeakPtr<PPAPIDownloadRequest> download_request) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DVLOG(2) << " checking whitelists for requestor URL:" << requestor_url; |
| |
| bool url_was_whitelisted = |
| requestor_url.is_valid() && database_manager && |
| database_manager->MatchDownloadWhitelistUrl(requestor_url); |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::BindOnce(&PPAPIDownloadRequest::WhitelistCheckComplete, |
| download_request, url_was_whitelisted)); |
| } |
| |
| void WhitelistCheckComplete(bool was_on_whitelist) { |
| DVLOG(2) << __func__ << " was_on_whitelist:" << was_on_whitelist; |
| if (was_on_whitelist) { |
| RecordCountOfWhitelistedDownload(URL_WHITELIST); |
| // TODO(asanka): Should sample whitelisted downloads based on |
| // service_->whitelist_sample_rate(). http://crbug.com/610924 |
| Finish(RequestOutcome::WHITELIST_HIT, SAFE); |
| return; |
| } |
| |
| // Not on whitelist, so we are going to check with the SafeBrowsing |
| // backend. |
| SendRequest(); |
| } |
| |
| void SendRequest() { |
| DVLOG(2) << __func__; |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| ClientDownloadRequest request; |
| auto population = is_extended_reporting_ |
| ? ChromeUserPopulation::EXTENDED_REPORTING |
| : ChromeUserPopulation::SAFE_BROWSING; |
| request.mutable_population()->set_user_population(population); |
| request.set_download_type(ClientDownloadRequest::PPAPI_SAVE_REQUEST); |
| ClientDownloadRequest::Resource* resource = request.add_resources(); |
| resource->set_type(ClientDownloadRequest::PPAPI_DOCUMENT); |
| resource->set_url(requestor_url_.spec()); |
| request.set_url(requestor_url_.spec()); |
| request.set_file_basename(supported_path_.BaseName().AsUTF8Unsafe()); |
| request.set_length(0); |
| request.mutable_digests()->set_md5(std::string()); |
| for (const auto& alternate_extension : alternate_extensions_) { |
| if (alternate_extension.empty()) |
| continue; |
| DCHECK_EQ(base::FilePath::kExtensionSeparator, alternate_extension[0]); |
| *(request.add_alternate_extensions()) = |
| base::FilePath(alternate_extension).AsUTF8Unsafe(); |
| } |
| if (supported_path_ != default_file_path_) { |
| *(request.add_alternate_extensions()) = |
| base::FilePath(default_file_path_.FinalExtension()).AsUTF8Unsafe(); |
| } |
| |
| service_->AddReferrerChainToPPAPIClientDownloadRequest( |
| initiating_frame_url_, initiating_main_frame_url_, tab_id_, |
| has_user_gesture_, &request); |
| |
| if (!request.SerializeToString(&client_download_request_data_)) { |
| // More of an internal error than anything else. Note that the UNKNOWN |
| // verdict gets interpreted as "allowed". |
| Finish(RequestOutcome::REQUEST_MALFORMED, UNKNOWN); |
| return; |
| } |
| |
| service_->ppapi_download_request_callbacks_.Notify(&request); |
| DVLOG(2) << "Sending a PPAPI download request for URL: " << request.url(); |
| |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("ppapi_download_request", R"( |
| semantics { |
| sender: "Download Protection Service" |
| description: |
| "Chromium checks whether a given PPAPI 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: |
| "When user triggers a non-whitelisted PPAPI download, and the " |
| "file extension is supported by download protection service. " |
| "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: |
| "Download's URL, its referrer chain, and digest. Please 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: YES |
| 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, 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); |
| fetcher_->SetRequestContext(service_->request_context_getter_.get()); |
| fetcher_->SetUploadData("application/octet-stream", |
| client_download_request_data_); |
| fetcher_->Start(); |
| } |
| |
| // net::URLFetcherDelegate |
| void OnURLFetchComplete(const net::URLFetcher* source) override { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (!source->GetStatus().is_success() || |
| net::HTTP_OK != source->GetResponseCode()) { |
| Finish(RequestOutcome::FETCH_FAILED, UNKNOWN); |
| return; |
| } |
| |
| ClientDownloadResponse response; |
| std::string response_body; |
| bool got_data = source->GetResponseAsString(&response_body); |
| DCHECK(got_data); |
| |
| if (response.ParseFromString(response_body)) { |
| Finish(RequestOutcome::SUCCEEDED, |
| DownloadCheckResultFromClientDownloadResponse(response.verdict())); |
| } else { |
| Finish(RequestOutcome::RESPONSE_MALFORMED, UNKNOWN); |
| } |
| } |
| |
| void OnRequestTimedOut() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DVLOG(2) << __func__; |
| Finish(RequestOutcome::TIMEDOUT, UNKNOWN); |
| } |
| |
| void Finish(RequestOutcome reason, DownloadCheckResult response) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DVLOG(2) << __func__ << " response: " << response; |
| UMA_HISTOGRAM_SPARSE_SLOWLY( |
| "SBClientDownload.PPAPIDownloadRequest.RequestOutcome", |
| static_cast<int>(reason)); |
| UMA_HISTOGRAM_SPARSE_SLOWLY("SBClientDownload.PPAPIDownloadRequest.Result", |
| response); |
| UMA_HISTOGRAM_TIMES("SBClientDownload.PPAPIDownloadRequest.RequestDuration", |
| start_time_ - base::TimeTicks::Now()); |
| if (!callback_.is_null()) |
| base::ResetAndReturn(&callback_).Run(response); |
| fetcher_.reset(); |
| weakptr_factory_.InvalidateWeakPtrs(); |
| |
| // If the request is being destroyed, don't notify the service_. It already |
| // knows. |
| if (reason == RequestOutcome::REQUEST_DESTROYED) |
| return; |
| |
| service_->PPAPIDownloadCheckRequestFinished(this); |
| // |this| is deleted. |
| } |
| |
| static DownloadCheckResult DownloadCheckResultFromClientDownloadResponse( |
| ClientDownloadResponse::Verdict verdict) { |
| switch (verdict) { |
| case ClientDownloadResponse::SAFE: |
| return SAFE; |
| case ClientDownloadResponse::UNCOMMON: |
| return UNCOMMON; |
| case ClientDownloadResponse::POTENTIALLY_UNWANTED: |
| return POTENTIALLY_UNWANTED; |
| case ClientDownloadResponse::DANGEROUS: |
| return DANGEROUS; |
| case ClientDownloadResponse::DANGEROUS_HOST: |
| return DANGEROUS_HOST; |
| case ClientDownloadResponse::UNKNOWN: |
| return UNKNOWN; |
| } |
| return UNKNOWN; |
| } |
| |
| // Given a |default_file_path| and a list of |alternate_extensions|, |
| // constructs a FilePath with each possible extension and returns one that |
| // satisfies IsCheckedBinaryFile(). If none are supported, returns an |
| // empty FilePath. |
| static base::FilePath GetSupportedFilePath( |
| const base::FilePath& default_file_path, |
| const std::vector<base::FilePath::StringType>& alternate_extensions) { |
| const FileTypePolicies* file_type_policies = |
| FileTypePolicies::GetInstance(); |
| if (file_type_policies->IsCheckedBinaryFile(default_file_path)) |
| return default_file_path; |
| |
| for (const auto& extension : alternate_extensions) { |
| base::FilePath alternative_file_path = |
| default_file_path.ReplaceExtension(extension); |
| if (file_type_policies->IsCheckedBinaryFile(alternative_file_path)) |
| return alternative_file_path; |
| } |
| |
| return base::FilePath(); |
| } |
| |
| std::unique_ptr<net::URLFetcher> fetcher_; |
| std::string client_download_request_data_; |
| |
| // URL of document that requested the PPAPI download. |
| const GURL requestor_url_; |
| |
| // URL of the frame that hosts the PPAPI plugin. |
| const GURL initiating_frame_url_; |
| |
| // URL of the tab that contains the initialting_frame. |
| const GURL initiating_main_frame_url_; |
| |
| // Tab id that associated with the PPAPI plugin, computed by |
| // SessionTabHelper::IdForTab(). |
| int tab_id_; |
| |
| // If the user interacted with this PPAPI plugin to trigger the download. |
| bool has_user_gesture_; |
| |
| // Default download path requested by the PPAPI plugin. |
| const base::FilePath default_file_path_; |
| |
| // List of alternate extensions provided by the PPAPI plugin. Each extension |
| // must begin with a leading extension separator. |
| const std::vector<base::FilePath::StringType> alternate_extensions_; |
| |
| // Callback to invoke with the result of the PPAPI download request check. |
| CheckDownloadCallback callback_; |
| |
| DownloadProtectionService* service_; |
| const scoped_refptr<SafeBrowsingDatabaseManager> database_manager_; |
| |
| // Time request was started. |
| const base::TimeTicks start_time_; |
| |
| // A download path that is supported by SafeBrowsing. This is determined by |
| // invoking GetSupportedFilePath(). If non-empty, |
| // IsCheckedBinaryFile(supported_path_) is always true. This |
| // path is therefore used as the download target when sending the SafeBrowsing |
| // ping. |
| const base::FilePath supported_path_; |
| |
| bool is_extended_reporting_; |
| |
| base::WeakPtrFactory<PPAPIDownloadRequest> weakptr_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(PPAPIDownloadRequest); |
| }; |
| |
| DownloadProtectionService::DownloadProtectionService( |
| SafeBrowsingService* sb_service) |
| : sb_service_(sb_service), |
| navigation_observer_manager_(nullptr), |
| request_context_getter_(sb_service ? sb_service->url_request_context() |
| : nullptr), |
| enabled_(false), |
| binary_feature_extractor_(new BinaryFeatureExtractor()), |
| download_request_timeout_ms_(kDownloadRequestTimeoutMs), |
| feedback_service_(new DownloadFeedbackService( |
| request_context_getter_.get(), |
| base::CreateSequencedTaskRunnerWithTraits( |
| {base::MayBlock(), base::TaskPriority::BACKGROUND}) |
| .get())), |
| whitelist_sample_rate_(kWhitelistDownloadSampleRate) { |
| if (sb_service) { |
| ui_manager_ = sb_service->ui_manager(); |
| database_manager_ = sb_service->database_manager(); |
| navigation_observer_manager_ = sb_service->navigation_observer_manager(); |
| ParseManualBlacklistFlag(); |
| } |
| } |
| |
| DownloadProtectionService::~DownloadProtectionService() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CancelPendingRequests(); |
| } |
| |
| void DownloadProtectionService::SetEnabled(bool enabled) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (enabled == enabled_) { |
| return; |
| } |
| enabled_ = enabled; |
| if (!enabled_) { |
| CancelPendingRequests(); |
| } |
| } |
| |
| void DownloadProtectionService::ParseManualBlacklistFlag() { |
| base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); |
| if (!command_line->HasSwitch( |
| safe_browsing::switches::kSbManualDownloadBlacklist)) |
| return; |
| |
| std::string flag_val = command_line->GetSwitchValueASCII( |
| safe_browsing::switches::kSbManualDownloadBlacklist); |
| for (const std::string& hash_hex : base::SplitString( |
| flag_val, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) { |
| std::vector<uint8_t> bytes; |
| if (base::HexStringToBytes(hash_hex, &bytes) && bytes.size() == 32) { |
| manual_blacklist_hashes_.insert( |
| std::string(bytes.begin(), bytes.end())); |
| } else { |
| LOG(FATAL) << "Bad sha256 hex value '" << hash_hex << "' found in --" |
| << safe_browsing::switches::kSbManualDownloadBlacklist; |
| } |
| } |
| } |
| |
| bool DownloadProtectionService::IsHashManuallyBlacklisted( |
| const std::string& sha256_hash) const { |
| return manual_blacklist_hashes_.count(sha256_hash) > 0; |
| } |
| |
| void DownloadProtectionService::CheckClientDownload( |
| content::DownloadItem* item, |
| const CheckDownloadCallback& callback) { |
| scoped_refptr<CheckClientDownloadRequest> request( |
| new CheckClientDownloadRequest(item, callback, this, |
| database_manager_, |
| binary_feature_extractor_.get())); |
| download_requests_.insert(request); |
| request->Start(); |
| } |
| |
| void DownloadProtectionService::CheckDownloadUrl( |
| content::DownloadItem* item, |
| const CheckDownloadCallback& callback) { |
| DCHECK(!item->GetUrlChain().empty()); |
| scoped_refptr<DownloadUrlSBClient> client( |
| new DownloadUrlSBClient(item, this, callback, ui_manager_, |
| database_manager_)); |
| // The client will release itself once it is done. |
| BrowserThread::PostTask( |
| BrowserThread::IO, FROM_HERE, |
| base::BindOnce(&DownloadUrlSBClient::StartCheck, client)); |
| } |
| |
| bool DownloadProtectionService::IsSupportedDownload( |
| const content::DownloadItem& item, |
| const base::FilePath& target_path) const { |
| DownloadCheckResultReason reason = REASON_MAX; |
| ClientDownloadRequest::DownloadType type = |
| ClientDownloadRequest::WIN_EXECUTABLE; |
| // TODO(nparker): Remove the CRX check here once can support |
| // UNKNOWN types properly. http://crbug.com/581044 |
| return (CheckClientDownloadRequest::IsSupportedDownload( |
| item, target_path, &reason, &type) && |
| (ClientDownloadRequest::CHROME_EXTENSION != type)); |
| } |
| |
| void DownloadProtectionService::CheckPPAPIDownloadRequest( |
| const GURL& requestor_url, |
| const GURL& initiating_frame_url, |
| content::WebContents* web_contents, |
| const base::FilePath& default_file_path, |
| const std::vector<base::FilePath::StringType>& alternate_extensions, |
| Profile* profile, |
| const CheckDownloadCallback& callback) { |
| DVLOG(1) << __func__ << " url:" << requestor_url |
| << " default_file_path:" << default_file_path.value(); |
| std::unique_ptr<PPAPIDownloadRequest> request(new PPAPIDownloadRequest( |
| requestor_url, initiating_frame_url, web_contents, default_file_path, |
| alternate_extensions, profile, callback, this, database_manager_)); |
| PPAPIDownloadRequest* request_copy = request.get(); |
| auto insertion_result = ppapi_download_requests_.insert( |
| std::make_pair(request_copy, std::move(request))); |
| DCHECK(insertion_result.second); |
| insertion_result.first->second->Start(); |
| } |
| |
| DownloadProtectionService::ClientDownloadRequestSubscription |
| DownloadProtectionService::RegisterClientDownloadRequestCallback( |
| const ClientDownloadRequestCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| return client_download_request_callbacks_.Add(callback); |
| } |
| |
| DownloadProtectionService::PPAPIDownloadRequestSubscription |
| DownloadProtectionService::RegisterPPAPIDownloadRequestCallback( |
| const PPAPIDownloadRequestCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| return ppapi_download_request_callbacks_.Add(callback); |
| } |
| |
| void DownloadProtectionService::CancelPendingRequests() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| for (auto 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()); |
| |
| // It is sufficient to delete the list of PPAPI download requests. |
| ppapi_download_requests_.clear(); |
| } |
| |
| void DownloadProtectionService::RequestFinished( |
| CheckClientDownloadRequest* request) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| auto it = download_requests_.find(request); |
| DCHECK(it != download_requests_.end()); |
| download_requests_.erase(*it); |
| } |
| |
| void DownloadProtectionService::PPAPIDownloadCheckRequestFinished( |
| PPAPIDownloadRequest* request) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| auto it = ppapi_download_requests_.find(request); |
| DCHECK(it != ppapi_download_requests_.end()); |
| ppapi_download_requests_.erase(it); |
| } |
| |
| void DownloadProtectionService::ShowDetailsForDownload( |
| const content::DownloadItem& item, |
| content::PageNavigator* navigator) { |
| GURL learn_more_url(chrome::kDownloadScanningLearnMoreURL); |
| learn_more_url = google_util::AppendGoogleLocaleParam( |
| learn_more_url, g_browser_process->GetApplicationLocale()); |
| learn_more_url = net::AppendQueryParameter( |
| learn_more_url, "ctx", |
| base::IntToString(static_cast<int>(item.GetDangerType()))); |
| navigator->OpenURL( |
| content::OpenURLParams(learn_more_url, content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false)); |
| } |
| |
| void DownloadProtectionService::SetDownloadPingToken( |
| content::DownloadItem* item, const std::string& token) { |
| if (item) { |
| item->SetUserData(kDownloadPingTokenKey, |
| base::MakeUnique<DownloadPingToken>(token)); |
| } |
| } |
| |
| std::string DownloadProtectionService::GetDownloadPingToken( |
| const content::DownloadItem* item) { |
| base::SupportsUserData::Data* token_data = |
| item->GetUserData(kDownloadPingTokenKey); |
| if (token_data) |
| return static_cast<DownloadPingToken*>(token_data)->token_string(); |
| else |
| return std::string(); |
| } |
| |
| void DownloadProtectionService::MaybeSendDangerousDownloadOpenedReport( |
| const content::DownloadItem* item, |
| bool show_download_in_folder) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| std::string token = GetDownloadPingToken(item); |
| content::BrowserContext* browser_context = item->GetBrowserContext(); |
| Profile* profile = Profile::FromBrowserContext(browser_context); |
| if (sb_service_ && |
| !token.empty() && // Only dangerous downloads have token stored. |
| !browser_context->IsOffTheRecord() && profile && |
| IsExtendedReportingEnabled(*profile->GetPrefs())) { |
| safe_browsing::ClientSafeBrowsingReportRequest report; |
| report.set_url(item->GetURL().spec()); |
| report.set_type(safe_browsing::ClientSafeBrowsingReportRequest:: |
| DANGEROUS_DOWNLOAD_OPENED); |
| report.set_token(token); |
| report.set_show_download_in_folder(show_download_in_folder); |
| std::string serialized_report; |
| if (report.SerializeToString(&serialized_report)) { |
| sb_service_->SendSerializedDownloadReport(serialized_report); |
| } else { |
| DCHECK(false) |
| << "Unable to serialize the dangerous download opened report."; |
| } |
| } |
| } |
| |
| 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_der; |
| net::X509Certificate::GetDEREncoded(issuer.os_cert_handle(), &issuer_der); |
| std::string hashed = base::SHA1HashString(issuer_der); |
| std::string issuer_fp = base::HexEncode(hashed.data(), hashed.size()); |
| 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 |
| GURL DownloadProtectionService::GetDownloadRequestUrl() { |
| GURL url(kDownloadRequestUrl); |
| std::string api_key = google_apis::GetAPIKey(); |
| if (!api_key.empty()) |
| url = url.Resolve("?key=" + net::EscapeQueryParamValue(api_key, true)); |
| |
| return url; |
| } |
| |
| std::unique_ptr<ReferrerChain> DownloadProtectionService::IdentifyReferrerChain( |
| const content::DownloadItem& item) { |
| // If navigation_observer_manager_ is null, return immediately. This could |
| // happen in tests. |
| if (!navigation_observer_manager_) |
| return nullptr; |
| |
| std::unique_ptr<ReferrerChain> referrer_chain = |
| base::MakeUnique<ReferrerChain>(); |
| content::WebContents* web_contents = item.GetWebContents(); |
| int download_tab_id = SessionTabHelper::IdForTab(web_contents); |
| UMA_HISTOGRAM_BOOLEAN( |
| "SafeBrowsing.ReferrerHasInvalidTabID.DownloadAttribution", |
| download_tab_id == -1); |
| // We look for the referrer chain that leads to the download url first. |
| SafeBrowsingNavigationObserverManager::AttributionResult result = |
| navigation_observer_manager_->IdentifyReferrerChainByEventURL( |
| item.GetURL(), download_tab_id, kDownloadAttributionUserGestureLimit, |
| referrer_chain.get()); |
| |
| // If no navigation event is found, this download is not triggered by regular |
| // navigation (e.g. html5 file apis, etc). We look for the referrer chain |
| // based on relevant WebContents instead. |
| if (result == |
| SafeBrowsingNavigationObserverManager::NAVIGATION_EVENT_NOT_FOUND && |
| web_contents && web_contents->GetLastCommittedURL().is_valid()) { |
| AddEventUrlToReferrerChain(item, referrer_chain.get()); |
| result = navigation_observer_manager_->IdentifyReferrerChainByWebContents( |
| web_contents, kDownloadAttributionUserGestureLimit, |
| referrer_chain.get()); |
| } |
| |
| UMA_HISTOGRAM_COUNTS_100( |
| "SafeBrowsing.ReferrerURLChainSize.DownloadAttribution", |
| referrer_chain->size()); |
| UMA_HISTOGRAM_ENUMERATION( |
| "SafeBrowsing.ReferrerAttributionResult.DownloadAttribution", result, |
| SafeBrowsingNavigationObserverManager::ATTRIBUTION_FAILURE_TYPE_MAX); |
| return referrer_chain; |
| } |
| |
| void DownloadProtectionService::AddReferrerChainToPPAPIClientDownloadRequest( |
| const GURL& initiating_frame_url, |
| const GURL& initiating_main_frame_url, |
| int tab_id, |
| bool has_user_gesture, |
| ClientDownloadRequest* out_request) { |
| if (!navigation_observer_manager_) |
| return; |
| |
| UMA_HISTOGRAM_BOOLEAN( |
| "SafeBrowsing.ReferrerHasInvalidTabID.DownloadAttribution", |
| tab_id == -1); |
| SafeBrowsingNavigationObserverManager::AttributionResult result = |
| navigation_observer_manager_->IdentifyReferrerChainByHostingPage( |
| initiating_frame_url, initiating_main_frame_url, tab_id, |
| has_user_gesture, kDownloadAttributionUserGestureLimit, |
| out_request->mutable_referrer_chain()); |
| UMA_HISTOGRAM_COUNTS_100( |
| "SafeBrowsing.ReferrerURLChainSize.PPAPIDownloadAttribution", |
| out_request->referrer_chain_size()); |
| UMA_HISTOGRAM_ENUMERATION( |
| "SafeBrowsing.ReferrerAttributionResult.PPAPIDownloadAttribution", result, |
| SafeBrowsingNavigationObserverManager::ATTRIBUTION_FAILURE_TYPE_MAX); |
| } |
| |
| } // namespace safe_browsing |