| // 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/ppapi_download_request.h" |
| |
| #include "base/metrics/histogram_macros.h" |
| #include "chrome/browser/profiles/profile_manager.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/sessions/session_tab_helper.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_db/database_manager.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_contents.h" |
| #include "google_apis/google_api_keys.h" |
| #include "net/base/escape.h" |
| #include "net/http/http_cache.h" |
| #include "net/http/http_status_code.h" |
| #include "net/url_request/url_fetcher.h" |
| |
| using content::BrowserThread; |
| |
| namespace safe_browsing { |
| |
| const char PPAPIDownloadRequest::kDownloadRequestUrl[] = |
| "https://sb-ssl.google.com/safebrowsing/clientreport/download"; |
| |
| PPAPIDownloadRequest::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::~PPAPIDownloadRequest() { |
| if (fetcher_ && !callback_.is_null()) |
| Finish(RequestOutcome::REQUEST_DESTROYED, DownloadCheckResult::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 PPAPIDownloadRequest::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, DownloadCheckResult::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())); |
| } |
| |
| // static |
| GURL PPAPIDownloadRequest::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; |
| } |
| |
| // Whitelist checking needs to the done on the IO thread. |
| void PPAPIDownloadRequest::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 PPAPIDownloadRequest::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, DownloadCheckResult::SAFE); |
| return; |
| } |
| |
| // Not on whitelist, so we are going to check with the SafeBrowsing |
| // backend. |
| SendRequest(); |
| } |
| |
| void PPAPIDownloadRequest::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, DownloadCheckResult::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 PPAPIDownloadRequest::OnURLFetchComplete(const net::URLFetcher* source) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (!source->GetStatus().is_success() || |
| net::HTTP_OK != source->GetResponseCode()) { |
| Finish(RequestOutcome::FETCH_FAILED, DownloadCheckResult::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, DownloadCheckResult::UNKNOWN); |
| } |
| } |
| |
| void PPAPIDownloadRequest::OnRequestTimedOut() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DVLOG(2) << __func__; |
| Finish(RequestOutcome::TIMEDOUT, DownloadCheckResult::UNKNOWN); |
| } |
| |
| void PPAPIDownloadRequest::Finish(RequestOutcome reason, |
| DownloadCheckResult response) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DVLOG(2) << __func__ << " response: " << static_cast<int>(response); |
| UMA_HISTOGRAM_SPARSE_SLOWLY( |
| "SBClientDownload.PPAPIDownloadRequest.RequestOutcome", |
| static_cast<int>(reason)); |
| UMA_HISTOGRAM_SPARSE_SLOWLY("SBClientDownload.PPAPIDownloadRequest.Result", |
| static_cast<int>(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. |
| } |
| |
| DownloadCheckResult |
| PPAPIDownloadRequest::DownloadCheckResultFromClientDownloadResponse( |
| ClientDownloadResponse::Verdict verdict) { |
| switch (verdict) { |
| case ClientDownloadResponse::SAFE: |
| return DownloadCheckResult::SAFE; |
| case ClientDownloadResponse::UNCOMMON: |
| return DownloadCheckResult::UNCOMMON; |
| case ClientDownloadResponse::POTENTIALLY_UNWANTED: |
| return DownloadCheckResult::POTENTIALLY_UNWANTED; |
| case ClientDownloadResponse::DANGEROUS: |
| return DownloadCheckResult::DANGEROUS; |
| case ClientDownloadResponse::DANGEROUS_HOST: |
| return DownloadCheckResult::DANGEROUS_HOST; |
| case ClientDownloadResponse::UNKNOWN: |
| return DownloadCheckResult::UNKNOWN; |
| } |
| return DownloadCheckResult::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 TODO: put above description in .h |
| base::FilePath PPAPIDownloadRequest::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(); |
| } |
| |
| } // namespace safe_browsing |