| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/download/insecure_download_blocking.h" |
| |
| #include "base/debug/crash_logging.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/common/chrome_features.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/content_settings/core/common/content_settings.h" |
| #include "components/download/public/common/download_stats.h" |
| #include "content/public/browser/download_item_utils.h" |
| #include "content/public/browser/web_contents.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/blink/public/mojom/devtools/console_message.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| using download::DownloadSource; |
| using InsecureDownloadStatus = download::DownloadItem::InsecureDownloadStatus; |
| |
| namespace { |
| |
| // Configuration for which extensions to warn/block. These parameters are set |
| // differently for testing, so the listed defaults are only used when the flag |
| // is manually enabled (and in unit tests). |
| // |
| // Extensions must be in lower case! Extensions are compared against save path |
| // determined by Chrome prior to the user seeing a file picker. |
| // |
| // The extension list for each type (warn, block, silent block) can be |
| // configured in two ways: as an allowlist, or as a blocklist. When the |
| // extension list is a blocklist, extensions listed will trigger a |
| // warning/block. If the extension list is configured as an allowlist, all |
| // extensions EXCEPT those listed will trigger a warning/block. |
| // |
| // To make manual testing easier, the defaults are to have a small blocklist for |
| // block/silent block, and a small allowlist for warnings. This means that |
| // every mixed content download will at *least* generate a warning. |
| const base::FeatureParam<bool> kTreatSilentBlockListAsAllowlist( |
| &features::kTreatUnsafeDownloadsAsActive, |
| "TreatSilentBlockListAsAllowlist", |
| true); |
| const base::FeatureParam<std::string> kSilentBlockExtensionList( |
| &features::kTreatUnsafeDownloadsAsActive, |
| "SilentBlockExtensionList", |
| "silently_unblocked_for_testing"); |
| |
| const base::FeatureParam<bool> kTreatBlockListAsAllowlist( |
| &features::kTreatUnsafeDownloadsAsActive, |
| "TreatBlockListAsAllowlist", |
| false); |
| const base::FeatureParam<std::string> kBlockExtensionList( |
| &features::kTreatUnsafeDownloadsAsActive, |
| "BlockExtensionList", |
| ""); |
| |
| // Note: this is an allowlist, so acts as a catch-all. |
| const base::FeatureParam<bool> kTreatWarnListAsAllowlist( |
| &features::kTreatUnsafeDownloadsAsActive, |
| "TreatWarnListAsAllowlist", |
| false); |
| const base::FeatureParam<std::string> kWarnExtensionList( |
| &features::kTreatUnsafeDownloadsAsActive, |
| "WarnExtensionList", |
| ""); |
| |
| // Map the string file extension to the corresponding histogram enum. |
| InsecureDownloadExtensions GetExtensionEnumFromString( |
| const std::string& extension) { |
| if (extension.empty()) |
| return InsecureDownloadExtensions::kNone; |
| |
| auto lower_extension = base::ToLowerASCII(extension); |
| for (auto candidate : kExtensionsToEnum) { |
| if (candidate.extension == lower_extension) |
| return candidate.value; |
| } |
| return InsecureDownloadExtensions::kUnknown; |
| } |
| |
| // Get the appropriate histogram metric name for the initiator/download security |
| // state combo. |
| std::string GetDownloadBlockingExtensionMetricName( |
| InsecureDownloadSecurityStatus status) { |
| switch (status) { |
| case InsecureDownloadSecurityStatus::kInitiatorUnknownFileSecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorUnknown, |
| kInsecureDownloadHistogramTargetSecure); |
| case InsecureDownloadSecurityStatus::kInitiatorUnknownFileInsecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorUnknown, |
| kInsecureDownloadHistogramTargetInsecure); |
| case InsecureDownloadSecurityStatus::kInitiatorSecureFileSecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorSecure, |
| kInsecureDownloadHistogramTargetSecure); |
| case InsecureDownloadSecurityStatus::kInitiatorSecureFileInsecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorSecure, |
| kInsecureDownloadHistogramTargetInsecure); |
| case InsecureDownloadSecurityStatus::kInitiatorInsecureFileSecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorInsecure, |
| kInsecureDownloadHistogramTargetSecure); |
| case InsecureDownloadSecurityStatus::kInitiatorInsecureFileInsecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorInsecure, |
| kInsecureDownloadHistogramTargetInsecure); |
| case InsecureDownloadSecurityStatus::kInitiatorInferredSecureFileSecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorInferredSecure, |
| kInsecureDownloadHistogramTargetSecure); |
| case InsecureDownloadSecurityStatus::kInitiatorInferredSecureFileInsecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorInferredSecure, |
| kInsecureDownloadHistogramTargetInsecure); |
| case InsecureDownloadSecurityStatus::kInitiatorInferredInsecureFileSecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorInferredInsecure, |
| kInsecureDownloadHistogramTargetSecure); |
| case InsecureDownloadSecurityStatus::kInitiatorInferredInsecureFileInsecure: |
| return GetDLBlockingHistogramName( |
| kInsecureDownloadExtensionInitiatorInferredInsecure, |
| kInsecureDownloadHistogramTargetInsecure); |
| case InsecureDownloadSecurityStatus::kDownloadIgnored: |
| NOTREACHED(); |
| } |
| NOTREACHED(); |
| return std::string(); |
| } |
| |
| // Get appropriate enum value for the initiator/download security state combo |
| // for histogram reporting. |dl_secure| signifies whether the download was |
| // a secure source. |inferred| is whether the initiator value is our best guess. |
| InsecureDownloadSecurityStatus GetDownloadBlockingEnum( |
| absl::optional<url::Origin> initiator, |
| bool dl_secure, |
| bool inferred) { |
| if (inferred) { |
| if (initiator->GetURL().SchemeIsCryptographic()) { |
| if (dl_secure) { |
| return InsecureDownloadSecurityStatus:: |
| kInitiatorInferredSecureFileSecure; |
| } |
| return InsecureDownloadSecurityStatus:: |
| kInitiatorInferredSecureFileInsecure; |
| } |
| |
| if (dl_secure) { |
| return InsecureDownloadSecurityStatus:: |
| kInitiatorInferredInsecureFileSecure; |
| } |
| return InsecureDownloadSecurityStatus:: |
| kInitiatorInferredInsecureFileInsecure; |
| } |
| |
| if (!initiator.has_value()) { |
| if (dl_secure) |
| return InsecureDownloadSecurityStatus::kInitiatorUnknownFileSecure; |
| return InsecureDownloadSecurityStatus::kInitiatorUnknownFileInsecure; |
| } |
| |
| if (initiator->GetURL().SchemeIsCryptographic()) { |
| if (dl_secure) |
| return InsecureDownloadSecurityStatus::kInitiatorSecureFileSecure; |
| return InsecureDownloadSecurityStatus::kInitiatorSecureFileInsecure; |
| } |
| |
| if (dl_secure) |
| return InsecureDownloadSecurityStatus::kInitiatorInsecureFileSecure; |
| return InsecureDownloadSecurityStatus::kInitiatorInsecureFileInsecure; |
| } |
| |
| struct InsecureDownloadData { |
| InsecureDownloadData(const base::FilePath& path, |
| const download::DownloadItem* item) |
| : item_(item) { |
| // Configure initiator. |
| bool initiator_inferred = false; |
| initiator_ = item->GetRequestInitiator(); |
| if (!initiator_.has_value() && item->GetTabUrl().is_valid()) { |
| initiator_inferred = true; |
| initiator_ = url::Origin::Create(item->GetTabUrl()); |
| } |
| |
| // Extract extension. |
| #if BUILDFLAG(IS_WIN) |
| extension_ = base::WideToUTF8(path.FinalExtension()); |
| #else |
| extension_ = path.FinalExtension(); |
| #endif |
| if (!extension_.empty()) { |
| DCHECK_EQ(extension_[0], '.'); |
| extension_ = extension_.substr(1); // Omit leading dot. |
| } |
| |
| // Evaluate download security. |
| is_redirect_chain_secure_ = true; |
| // Skip over the final URL so that we can investigate it separately below. |
| // The redirect chain always contains the final URL, so this is always safe |
| // in Chrome, but some tests don't plan for it, so we check here. |
| const auto& chain = item->GetUrlChain(); |
| if (chain.size() > 1) { |
| for (unsigned i = 0; i < chain.size() - 1; ++i) { |
| const GURL& url = chain[i]; |
| if (!network::IsUrlPotentiallyTrustworthy(url)) { |
| is_redirect_chain_secure_ = false; |
| break; |
| } |
| } |
| } |
| const GURL& dl_url = item->GetURL(); |
| // Whether or not the download was securely delivered, ignoring where we got |
| // the download URL from (i.e. ignoring the initiator). |
| bool download_delivered_securely = |
| is_redirect_chain_secure_ && |
| (network::IsUrlPotentiallyTrustworthy(dl_url) || |
| dl_url.SchemeIsBlob() || dl_url.SchemeIsFile()); |
| |
| // Configure mixed content status. |
| // Some downloads don't qualify for blocking, and are thus never |
| // mixed-content. At a minimum, this includes: |
| // - retries/reloads (since the original DL would have been blocked, and |
| // initiating context is lost on retry anyway), |
| // - anything triggered directly from the address bar or similar. |
| // - internal-Chrome downloads (e.g. downloading profile photos), |
| // - webview/CCT, |
| // - anything extension related, |
| // - etc. |
| // |
| // TODO(1029062): INTERNAL_API is also used for background fetch. That |
| // probably isn't the correct behavior, since INTERNAL_API is otherwise used |
| // for Chrome stuff. Background fetch should probably be HTTPS-only. |
| auto download_source = item->GetDownloadSource(); |
| auto transition_type = item->GetTransitionType(); |
| if (download_source == DownloadSource::RETRY || |
| (transition_type & ui::PAGE_TRANSITION_RELOAD) || |
| (transition_type & ui::PAGE_TRANSITION_TYPED) || |
| (transition_type & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) || |
| (transition_type & ui::PAGE_TRANSITION_FORWARD_BACK) || |
| (transition_type & ui::PAGE_TRANSITION_AUTO_TOPLEVEL) || |
| (transition_type & ui::PAGE_TRANSITION_AUTO_BOOKMARK) || |
| (transition_type & ui::PAGE_TRANSITION_FROM_API) || |
| download_source == DownloadSource::OFFLINE_PAGE || |
| download_source == DownloadSource::INTERNAL_API || |
| download_source == DownloadSource::EXTENSION_API || |
| download_source == DownloadSource::EXTENSION_INSTALLER) { |
| base::UmaHistogramEnumeration( |
| kInsecureDownloadHistogramName, |
| InsecureDownloadSecurityStatus::kDownloadIgnored); |
| is_mixed_content_ = false; |
| } else { // Not ignorable download. |
| // Record some metrics first. |
| auto security_status = GetDownloadBlockingEnum( |
| initiator_, download_delivered_securely, initiator_inferred); |
| base::UmaHistogramEnumeration( |
| GetDownloadBlockingExtensionMetricName(security_status), |
| GetExtensionEnumFromString(extension_)); |
| base::UmaHistogramEnumeration(kInsecureDownloadHistogramName, |
| security_status); |
| download::RecordDownloadValidationMetrics( |
| download::DownloadMetricsCallsite::kMixContentDownloadBlocking, |
| download::CheckDownloadConnectionSecurity(item->GetURL(), |
| item->GetUrlChain()), |
| download::DownloadContentFromMimeType(item->GetMimeType(), false)); |
| |
| is_mixed_content_ = (initiator_.has_value() && |
| initiator_->GetURL().SchemeIsCryptographic() && |
| !download_delivered_securely); |
| } |
| |
| // Configure insecure download status. |
| // Exclude download sources needed by Chrome from blocking. While this is |
| // similar to MIX-DL above, it intentionally blocks more user-initiated |
| // downloads. For example, downloads are blocked even if they're initiated |
| // from the omnibox. |
| if (download_source == DownloadSource::RETRY || |
| (transition_type & ui::PAGE_TRANSITION_RELOAD) || |
| (transition_type & ui::PAGE_TRANSITION_FROM_API) || |
| download_source == DownloadSource::OFFLINE_PAGE || |
| download_source == DownloadSource::INTERNAL_API || |
| download_source == DownloadSource::EXTENSION_API || |
| download_source == DownloadSource::EXTENSION_INSTALLER) { |
| is_insecure_download_ = false; |
| } else { // Not ignorable download. |
| // TODO(crbug.com/1352598): Add blocking metrics. |
| // insecure downloads are either delivered insecurely, or we can't trust |
| // who told us to download them (i.e. have an insecure initiator). |
| is_insecure_download_ = (initiator_.has_value() && |
| !initiator_->GetURL().SchemeIsCryptographic()) || |
| !download_delivered_securely; |
| } |
| } |
| |
| absl::optional<url::Origin> initiator_; |
| std::string extension_; |
| raw_ptr<const download::DownloadItem> item_; |
| |
| // Was the download redirected only through secure URLs? |
| bool is_redirect_chain_secure_; |
| // Was the download initiated by a secure origin, but delivered insecurely? |
| bool is_mixed_content_; |
| // Was the download initiated by an insecure origin or delivered insecurely? |
| bool is_insecure_download_; |
| }; |
| |
| // Check if |extension| is contained in the comma separated |extension_list|. |
| bool ContainsExtension(const std::string& extension_list, |
| const std::string& extension) { |
| for (const auto& item : |
| base::SplitStringPiece(extension_list, ",", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| DCHECK_EQ(base::ToLowerASCII(item), item); |
| if (base::EqualsCaseInsensitiveASCII(extension, item)) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| // Just print a descriptive message to the console about the blocked download. |
| // |is_blocked| indicates whether this download will be blocked now. |
| void PrintConsoleMessage(const InsecureDownloadData& data, bool is_blocked) { |
| content::WebContents* web_contents = |
| content::DownloadItemUtils::GetWebContents(data.item_); |
| if (!web_contents) { |
| return; |
| } |
| |
| web_contents->GetPrimaryMainFrame()->AddMessageToConsole( |
| blink::mojom::ConsoleMessageLevel::kError, |
| base::StringPrintf( |
| "Mixed Content: The site at '%s' was loaded over a secure " |
| "connection, but the file at '%s' was %s an insecure " |
| "connection. This file should be served over HTTPS. " |
| "This download %s. See " |
| "https://blog.chromium.org/2020/02/" |
| "protecting-users-from-insecure.html" |
| " for more details.", |
| data.initiator_->GetURL().spec().c_str(), |
| data.item_->GetURL().spec().c_str(), |
| (data.is_redirect_chain_secure_ ? "loaded over" |
| : "redirected through"), |
| (is_blocked ? "has been blocked" |
| : "will be blocked in future versions of Chrome"))); |
| } |
| |
| bool IsDownloadPermittedByContentSettings( |
| Profile* profile, |
| const absl::optional<url::Origin>& initiator) { |
| // TODO(crbug.com/1048957): Checking content settings crashes unit tests on |
| // Android. It shouldn't. |
| #if !BUILDFLAG(IS_ANDROID) |
| ContentSettingsForOneType settings; |
| HostContentSettingsMap* host_content_settings_map = |
| HostContentSettingsMapFactory::GetForProfile(profile); |
| host_content_settings_map->GetSettingsForOneType( |
| ContentSettingsType::MIXEDSCRIPT, &settings); |
| |
| // When there's only one rule, it's the default wildcard rule. |
| if (settings.size() == 1) { |
| DCHECK(settings[0].primary_pattern == ContentSettingsPattern::Wildcard()); |
| DCHECK(settings[0].secondary_pattern == ContentSettingsPattern::Wildcard()); |
| return settings[0].GetContentSetting() == CONTENT_SETTING_ALLOW; |
| } |
| |
| for (const auto& setting : settings) { |
| if (setting.primary_pattern.Matches(initiator->GetURL())) { |
| return setting.GetContentSetting() == CONTENT_SETTING_ALLOW; |
| } |
| } |
| NOTREACHED(); |
| #endif |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| InsecureDownloadStatus GetInsecureDownloadStatusForDownload( |
| Profile* profile, |
| const base::FilePath& path, |
| const download::DownloadItem* item) { |
| InsecureDownloadData data(path, item); |
| |
| // When enabled, show a visible (bypassable) warning on insecure downloads. |
| // Since mixed download blocking is more severe, exclude mixed downloads from |
| // this early-return to let the mixed download logic below apply. |
| if (base::FeatureList::IsEnabled(features::kBlockInsecureDownloads) && |
| data.is_insecure_download_ && !data.is_mixed_content_) { |
| PrintConsoleMessage(data, true); |
| return InsecureDownloadStatus::BLOCK; |
| } |
| |
| if (!data.is_mixed_content_) { |
| return InsecureDownloadStatus::SAFE; |
| } |
| |
| // As of M81, print a console message even if no other blocking is enabled. |
| if (!base::FeatureList::IsEnabled(features::kTreatUnsafeDownloadsAsActive)) { |
| PrintConsoleMessage(data, false); |
| return InsecureDownloadStatus::SAFE; |
| } |
| |
| if (IsDownloadPermittedByContentSettings(profile, data.initiator_)) { |
| PrintConsoleMessage(data, false); |
| return InsecureDownloadStatus::SAFE; |
| } |
| |
| if (ContainsExtension(kSilentBlockExtensionList.Get(), data.extension_) != |
| kTreatSilentBlockListAsAllowlist.Get()) { |
| PrintConsoleMessage(data, true); |
| |
| // Only permit silent blocking when not initiated by an explicit user |
| // action. Otherwise, fall back to visible blocking. |
| auto download_source = data.item_->GetDownloadSource(); |
| if (download_source == DownloadSource::CONTEXT_MENU || |
| download_source == DownloadSource::WEB_CONTENTS_API) { |
| return InsecureDownloadStatus::BLOCK; |
| } |
| |
| return InsecureDownloadStatus::SILENT_BLOCK; |
| } |
| |
| if (ContainsExtension(kBlockExtensionList.Get(), data.extension_) != |
| kTreatBlockListAsAllowlist.Get()) { |
| PrintConsoleMessage(data, true); |
| return InsecureDownloadStatus::BLOCK; |
| } |
| |
| if (ContainsExtension(kWarnExtensionList.Get(), data.extension_) != |
| kTreatWarnListAsAllowlist.Get()) { |
| PrintConsoleMessage(data, true); |
| return InsecureDownloadStatus::WARN; |
| } |
| |
| // The download is still mixed content, but we're not blocking it yet. |
| PrintConsoleMessage(data, false); |
| return InsecureDownloadStatus::SAFE; |
| } |