blob: 9073b6564c65d5dbec0df649c1001ce438ecf4c7 [file] [log] [blame]
// 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 <optional>
#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/string_util.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 "chrome/common/pref_names.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 "components/prefs/pref_service.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "net/base/url_util.h"
#include "services/network/public/cpp/is_potentially_trustworthy.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",
"");
const char kSafeExtensions[] =
("txt,css,json,csv,tsv,jpg,jpeg,png,gif,tif,tiff,ico,webp,aac,midi,ogg,"
"wav,webm,mp3,webm,mp4,mpeg,mov,wmv");
// 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();
case InsecureDownloadSecurityStatus::kInitiatorInsecureNonUniqueFileSecure:
return GetDLBlockingHistogramName(
kInsecureDownloadExtensionInitiatorInsecureNonUnique,
kInsecureDownloadHistogramTargetSecure);
case InsecureDownloadSecurityStatus::
kInitiatorInsecureNonUniqueFileInsecure:
return GetDLBlockingHistogramName(
kInsecureDownloadExtensionInitiatorInsecureNonUnique,
kInsecureDownloadHistogramTargetInsecure);
}
NOTREACHED();
}
// 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.
// |insecure_nonunique| indicates whether the download was initiated by an
// insecure non-unique hostname.
InsecureDownloadSecurityStatus GetDownloadBlockingEnum(
std::optional<url::Origin> initiator,
bool dl_secure,
bool inferred,
bool insecure_nonunique) {
if (insecure_nonunique) {
if (dl_secure) {
return InsecureDownloadSecurityStatus::
kInitiatorInsecureNonUniqueFileSecure;
}
return InsecureDownloadSecurityStatus::
kInitiatorInsecureNonUniqueFileInsecure;
}
if (inferred) {
if (network::IsUrlPotentiallyTrustworthy(initiator->GetURL())) {
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 (network::IsUrlPotentiallyTrustworthy(initiator->GetURL())) {
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());
#elif BUILDFLAG(IS_ANDROID)
// If the file path is a content URI, extension should come from the file
// name.
if (path.IsContentUri()) {
extension_ = item->GetFileNameToReportUser().FinalExtension();
} else {
extension_ = 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());
// Check if the initiator is insecure and non-unique.
bool insecure_nonunique = false;
if (initiator_.has_value() &&
!network::IsUrlPotentiallyTrustworthy(initiator_->GetURL()) &&
net::IsHostnameNonUnique(initiator_->GetURL().host())) {
insecure_nonunique = true;
}
// 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(crbug.com/40661154): 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, insecure_nonunique);
std::string metric_name =
GetDownloadBlockingExtensionMetricName(security_status);
base::UmaHistogramEnumeration(metric_name,
GetExtensionEnumFromString(extension_));
base::UmaHistogramEnumeration(kInsecureDownloadHistogramName,
security_status);
download::RecordDownloadValidationMetrics(
download::DownloadMetricsCallsite::kMixContentDownloadBlocking,
download::CheckDownloadConnectionSecurity(item->GetURL(),
item->GetUrlChain()),
download::DownloadContentFromMimeType(item->GetMimeType(), false));
// Mixed downloads are those initiated by a secure initiator but not
// delivered securely.
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/40857867): 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_->opaque() &&
!network::IsUrlPotentiallyTrustworthy(initiator_->GetURL()))) ||
!download_delivered_securely) &&
!net::IsLocalhost(dl_url);
}
is_user_initiated_on_webui_ =
item->GetTabUrl().SchemeIs(content::kChromeUIScheme) &&
download_source == DownloadSource::CONTEXT_MENU;
}
std::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_;
// Was the download initiated by a user on a chrome:// WebUI?
bool is_user_initiated_on_webui_;
};
// 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) {
content::RenderFrameHost* rfh =
content::DownloadItemUtils::GetRenderFrameHost(data.item_);
if (!rfh) {
return;
}
if (data.is_mixed_content_) {
rfh->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. "
"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")));
return;
}
// The user can right-click and save a HTTP link from a chrome:// WebUI
// (e.g. NTP or history). This is arguably a valid use case unless we
// completely ban users from visiting HTTP sites, so don't warn. Otherwise,
// an error will be generated and uploaded to the crash server.
if (data.is_user_initiated_on_webui_) {
return;
}
rfh->AddMessageToConsole(
blink::mojom::ConsoleMessageLevel::kError,
base::StringPrintf(
"The file at '%s' was %s an insecure connection. "
"This file should be served over HTTPS.",
data.item_->GetURL().spec().c_str(),
(data.is_redirect_chain_secure_ ? "loaded over"
: "redirected through")));
}
bool IsDownloadPermittedByContentSettings(
Profile* profile,
const std::optional<url::Origin>& initiator) {
// TODO(crbug.com/40117459): Checking content settings crashes unit tests on
// Android. It shouldn't.
#if BUILDFLAG(IS_ANDROID)
return false;
#else
HostContentSettingsMap* host_content_settings_map =
HostContentSettingsMapFactory::GetForProfile(profile);
ContentSettingsForOneType settings =
host_content_settings_map->GetSettingsForOneType(
ContentSettingsType::MIXEDSCRIPT);
// 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
}
bool IsHttpsFirstModeEnabled(Profile* profile) {
PrefService* prefs = profile->GetPrefs();
return prefs && prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled);
}
} // namespace
InsecureDownloadStatus GetInsecureDownloadStatusForDownload(
Profile* profile,
const base::FilePath& path,
const download::DownloadItem* item) {
InsecureDownloadData data(path, item);
// If the download is fully secure, early abort.
if (!data.is_insecure_download_) {
return InsecureDownloadStatus::SAFE;
}
// Print a console message for all varieties of insecure downloads.
PrintConsoleMessage(data);
if (IsDownloadPermittedByContentSettings(profile, data.initiator_)) {
return InsecureDownloadStatus::SAFE;
}
// 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 (data.is_insecure_download_ && !data.is_mixed_content_) {
// Except when using HFM, don't warn on files that are likely to be safe.
if (!IsHttpsFirstModeEnabled(profile) &&
ContainsExtension(kSafeExtensions, data.extension_)) {
return InsecureDownloadStatus::SAFE;
}
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)) {
return InsecureDownloadStatus::SAFE;
}
if (ContainsExtension(kSilentBlockExtensionList.Get(), data.extension_) !=
kTreatSilentBlockListAsAllowlist.Get()) {
// 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()) {
return InsecureDownloadStatus::BLOCK;
}
if (ContainsExtension(kWarnExtensionList.Get(), data.extension_) !=
kTreatWarnListAsAllowlist.Get()) {
return InsecureDownloadStatus::WARN;
}
// The download is still mixed content, but we're not blocking it yet.
return InsecureDownloadStatus::SAFE;
}