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