blob: 5aedb64158b777d6abe5e131ea96286758c67c63 [file] [log] [blame]
// 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/ui_manager.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/debug/leak_tracker.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/threading/thread.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/ping_manager.h"
#include "chrome/browser/safe_browsing/safe_browsing_blocking_page.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/safe_browsing/threat_details.h"
#include "chrome/browser/tab_contents/tab_util.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing_db/metadata.pb.h"
#include "components/safe_browsing_db/safe_browsing_prefs.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/web_contents.h"
#include "ipc/ipc_message.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/ssl/ssl_info.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"
using content::BrowserThread;
using content::NavigationEntry;
using content::WebContents;
using safe_browsing::HitReport;
using safe_browsing::SBThreatType;
namespace {
const void* const kWhitelistKey = &kWhitelistKey;
// A WhitelistUrlSet holds the set of URLs that have been whitelisted
// for a specific WebContents, along with pending entries that are still
// undecided. Each URL is associated with the first SBThreatType that
// was seen for that URL. The URLs in this set should come from
// GetWhitelistUrl() or GetMainFrameWhitelistUrlForResource().
class WhitelistUrlSet : public base::SupportsUserData::Data {
public:
WhitelistUrlSet() {}
bool Contains(const GURL url, SBThreatType* threat_type) {
auto found = map_.find(url);
if (found == map_.end())
return false;
if (threat_type)
*threat_type = found->second;
return true;
}
void RemovePending(const GURL& url) { pending_.erase(url); }
void Insert(const GURL url, SBThreatType threat_type) {
if (Contains(url, nullptr))
return;
map_[url] = threat_type;
RemovePending(url);
}
bool ContainsPending(const GURL& url, SBThreatType* threat_type) {
auto found = pending_.find(url);
if (found == pending_.end())
return false;
if (threat_type)
*threat_type = found->second;
return true;
}
void InsertPending(const GURL url, SBThreatType threat_type) {
if (ContainsPending(url, nullptr))
return;
pending_[url] = threat_type;
}
private:
std::map<GURL, SBThreatType> map_;
std::map<GURL, SBThreatType> pending_;
DISALLOW_COPY_AND_ASSIGN(WhitelistUrlSet);
};
// Returns the URL that should be used in a WhitelistUrlSet for the given
// |resource|.
GURL GetMainFrameWhitelistUrlForResource(
const security_interstitials::UnsafeResource& resource) {
if (resource.is_subresource) {
NavigationEntry* entry = resource.GetNavigationEntryForResource();
if (!entry)
return GURL();
return entry->GetURL().GetWithEmptyPath();
}
return resource.url.GetWithEmptyPath();
}
// Returns the URL that should be used in a WhitelistUrlSet for the
// resource loaded from |url| on a navigation |entry|.
GURL GetWhitelistUrl(const GURL& url,
bool is_subresource,
NavigationEntry* entry) {
if (is_subresource) {
if (!entry)
return GURL();
return entry->GetURL().GetWithEmptyPath();
}
return url.GetWithEmptyPath();
}
WhitelistUrlSet* GetOrCreateWhitelist(content::WebContents* web_contents) {
WhitelistUrlSet* site_list =
static_cast<WhitelistUrlSet*>(web_contents->GetUserData(kWhitelistKey));
if (!site_list) {
site_list = new WhitelistUrlSet;
web_contents->SetUserData(kWhitelistKey, site_list);
}
return site_list;
}
} // namespace
namespace safe_browsing {
SafeBrowsingUIManager::SafeBrowsingUIManager(
const scoped_refptr<SafeBrowsingService>& service)
: sb_service_(service) {}
SafeBrowsingUIManager::~SafeBrowsingUIManager() {}
void SafeBrowsingUIManager::StopOnIOThread(bool shutdown) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
if (shutdown)
sb_service_ = NULL;
}
void SafeBrowsingUIManager::LogPauseDelay(base::TimeDelta time) {
UMA_HISTOGRAM_LONG_TIMES("SB2.Delay", time);
}
void SafeBrowsingUIManager::OnBlockingPageDone(
const std::vector<UnsafeResource>& resources,
bool proceed,
content::WebContents* web_contents,
const GURL& main_frame_url) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
for (const auto& resource : resources) {
if (!resource.callback.is_null()) {
DCHECK(resource.callback_thread);
resource.callback_thread->PostTask(
FROM_HERE, base::Bind(resource.callback, proceed));
}
GURL whitelist_url = GetWhitelistUrl(
main_frame_url, false /* is subresource */,
nullptr /* no navigation entry needed for main resource */);
if (proceed) {
AddToWhitelistUrlSet(whitelist_url, web_contents,
false /* Pending -> permanent */,
resource.threat_type);
} else if (web_contents) {
// |web_contents| doesn't exist if the tab has been closed.
RemoveFromPendingWhitelistUrlSet(whitelist_url, web_contents);
}
}
}
void SafeBrowsingUIManager::DisplayBlockingPage(
const UnsafeResource& resource) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (resource.is_subresource && !resource.is_subframe) {
// Sites tagged as serving Unwanted Software should only show a warning for
// main-frame or sub-frame resource. Similar warning restrictions should be
// applied to malware sites tagged as "landing sites" (see "Types of
// Malware sites" under
// https://developers.google.com/safe-browsing/developers_guide_v3#UserWarnings).
MalwarePatternType proto;
if (resource.threat_type == SB_THREAT_TYPE_URL_UNWANTED ||
(resource.threat_type == SB_THREAT_TYPE_URL_MALWARE &&
resource.threat_metadata.threat_pattern_type ==
ThreatPatternType::MALWARE_LANDING)) {
if (!resource.callback.is_null()) {
DCHECK(resource.callback_thread);
resource.callback_thread->PostTask(FROM_HERE,
base::Bind(resource.callback, true));
}
return;
}
}
// The tab might have been closed. If it was closed, just act as if "Don't
// Proceed" had been chosen.
WebContents* web_contents = resource.web_contents_getter.Run();
if (!web_contents) {
std::vector<UnsafeResource> resources;
resources.push_back(resource);
OnBlockingPageDone(resources, false, web_contents,
GetMainFrameWhitelistUrlForResource(resource));
return;
}
// Check if the user has already ignored a SB warning for the same WebContents
// and top-level domain.
if (IsWhitelisted(resource)) {
if (!resource.callback.is_null()) {
DCHECK(resource.callback_thread);
resource.callback_thread->PostTask(FROM_HERE,
base::Bind(resource.callback, true));
}
return;
}
if (resource.threat_type != SB_THREAT_TYPE_SAFE) {
HitReport hit_report;
hit_report.malicious_url = resource.url;
hit_report.is_subresource = resource.is_subresource;
hit_report.threat_type = resource.threat_type;
hit_report.threat_source = resource.threat_source;
hit_report.population_id = resource.threat_metadata.population_id;
NavigationEntry* entry = resource.GetNavigationEntryForResource();
if (entry) {
hit_report.page_url = entry->GetURL();
hit_report.referrer_url = entry->GetReferrer().url;
}
// When the malicious url is on the main frame, and resource.original_url
// is not the same as the resource.url, that means we have a redirect from
// resource.original_url to resource.url.
// Also, at this point, page_url points to the _previous_ page that we
// were on. We replace page_url with resource.original_url and referrer
// with page_url.
if (!resource.is_subresource &&
!resource.original_url.is_empty() &&
resource.original_url != resource.url) {
hit_report.referrer_url = hit_report.page_url;
hit_report.page_url = resource.original_url;
}
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
hit_report.extended_reporting_level =
profile ? GetExtendedReportingLevel(*profile->GetPrefs())
: SBER_LEVEL_OFF;
hit_report.is_metrics_reporting_active =
ChromeMetricsServiceAccessor::IsMetricsAndCrashReportingEnabled();
MaybeReportSafeBrowsingHit(hit_report);
}
if (resource.threat_type != SB_THREAT_TYPE_SAFE) {
for (Observer& observer : observer_list_)
observer.OnSafeBrowsingHit(resource);
}
AddToWhitelistUrlSet(GetMainFrameWhitelistUrlForResource(resource),
resource.web_contents_getter.Run(),
true /* A decision is now pending */,
resource.threat_type);
SafeBrowsingBlockingPage::ShowBlockingPage(this, resource);
}
// A safebrowsing hit is sent after a blocking page for malware/phishing
// or after the warning dialog for download urls, only for
// extended-reporting users.
void SafeBrowsingUIManager::MaybeReportSafeBrowsingHit(
const HitReport& hit_report) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// Send report if user opted-in extended reporting.
if (hit_report.extended_reporting_level != SBER_LEVEL_OFF) {
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
base::Bind(&SafeBrowsingUIManager::ReportSafeBrowsingHitOnIOThread,
this, hit_report));
}
}
void SafeBrowsingUIManager::ReportSafeBrowsingHitOnIOThread(
const HitReport& hit_report) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// The service may delete the ping manager (i.e. when user disabling service,
// etc). This happens on the IO thread.
if (!sb_service_ || !sb_service_->ping_manager())
return;
DVLOG(1) << "ReportSafeBrowsingHit: " << hit_report.malicious_url << " "
<< hit_report.page_url << " " << hit_report.referrer_url << " "
<< hit_report.is_subresource << " " << hit_report.threat_type;
sb_service_->ping_manager()->ReportSafeBrowsingHit(hit_report);
}
void SafeBrowsingUIManager::ReportInvalidCertificateChain(
const std::string& serialized_report,
const base::Closure& callback) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
BrowserThread::PostTaskAndReply(
BrowserThread::IO, FROM_HERE,
base::Bind(
&SafeBrowsingUIManager::ReportInvalidCertificateChainOnIOThread, this,
serialized_report),
callback);
}
void SafeBrowsingUIManager::ReportPermissionAction(
const PermissionReportInfo& report_info) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE,
base::Bind(&SafeBrowsingUIManager::ReportPermissionActionOnIOThread, this,
report_info));
}
void SafeBrowsingUIManager::AddObserver(Observer* observer) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
observer_list_.AddObserver(observer);
}
void SafeBrowsingUIManager::RemoveObserver(Observer* observer) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
observer_list_.RemoveObserver(observer);
}
// Static.
void SafeBrowsingUIManager::CreateWhitelistForTesting(
content::WebContents* web_contents) {
GetOrCreateWhitelist(web_contents);
}
void SafeBrowsingUIManager::ReportInvalidCertificateChainOnIOThread(
const std::string& serialized_report) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// The service may delete the ping manager (i.e. when user disabling service,
// etc). This happens on the IO thread.
if (!sb_service_ || !sb_service_->ping_manager())
return;
sb_service_->ping_manager()->ReportInvalidCertificateChain(serialized_report);
}
void SafeBrowsingUIManager::ReportPermissionActionOnIOThread(
const PermissionReportInfo& report_info) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// The service may delete the ping manager (i.e. when user disabling service,
// etc). This happens on the IO thread.
if (!sb_service_ || !sb_service_->ping_manager())
return;
sb_service_->ping_manager()->ReportPermissionAction(report_info);
}
// If the user had opted-in to send ThreatDetails, this gets called
// when the report is ready.
void SafeBrowsingUIManager::SendSerializedThreatDetails(
const std::string& serialized) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// The service may delete the ping manager (i.e. when user disabling service,
// etc). This happens on the IO thread.
if (sb_service_.get() == NULL || sb_service_->ping_manager() == NULL)
return;
if (!serialized.empty()) {
DVLOG(1) << "Sending serialized threat details.";
sb_service_->ping_manager()->ReportThreatDetails(serialized);
}
}
// Record this domain in the given WebContents as either whitelisted or
// pending whitelisting (if an interstitial is currently displayed). If an
// existing WhitelistUrlSet does not yet exist, create a new WhitelistUrlSet.
void SafeBrowsingUIManager::AddToWhitelistUrlSet(
const GURL& whitelist_url,
content::WebContents* web_contents,
bool pending,
SBThreatType threat_type) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// A WebContents might not exist if the tab has been closed.
if (!web_contents)
return;
WhitelistUrlSet* site_list = GetOrCreateWhitelist(web_contents);
if (whitelist_url.is_empty())
return;
if (pending) {
site_list->InsertPending(whitelist_url, threat_type);
} else {
site_list->Insert(whitelist_url, threat_type);
}
// Notify security UI that security state has changed.
web_contents->DidChangeVisibleSecurityState();
}
void SafeBrowsingUIManager::RemoveFromPendingWhitelistUrlSet(
const GURL& whitelist_url,
content::WebContents* web_contents) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
// A WebContents might not exist if the tab has been closed.
if (!web_contents)
return;
// Use |web_contents| rather than |resource.web_contents_getter|
// here. By this point, a "Back" navigation could have already been
// committed, so the page loading |resource| might be gone and
// |web_contents_getter| may no longer be valid.
WhitelistUrlSet* site_list =
static_cast<WhitelistUrlSet*>(web_contents->GetUserData(kWhitelistKey));
if (whitelist_url.is_empty())
return;
// Note that this function does not DCHECK that |whitelist_url|
// appears in the pending whitelist. In the common case, it's expected
// that a URL is in the pending whitelist when it is removed, but it's
// not always the case. For example, if there are several blocking
// pages queued up for different resources on the same page, and the
// user goes back to dimiss the first one, the subsequent blocking
// pages get dismissed as well (as if the user had clicked "Back to
// safety" on each of them). In this case, the first dismissal will
// remove the main-frame URL from the pending whitelist, so the
// main-frame URL will have already been removed when the subsequent
// blocking pages are dismissed.
if (site_list->ContainsPending(whitelist_url, nullptr))
site_list->RemovePending(whitelist_url);
// Notify security UI that security state has changed.
web_contents->DidChangeVisibleSecurityState();
}
bool SafeBrowsingUIManager::IsWhitelisted(const UnsafeResource& resource) {
NavigationEntry* entry = nullptr;
if (resource.is_subresource) {
entry = resource.GetNavigationEntryForResource();
}
SBThreatType unused_threat_type;
return IsUrlWhitelistedOrPendingForWebContents(
resource.url, resource.is_subresource, entry,
resource.web_contents_getter.Run(), true, &unused_threat_type);
}
// Check if the user has already seen and/or ignored a SB warning for this
// WebContents and top-level domain.
bool SafeBrowsingUIManager::IsUrlWhitelistedOrPendingForWebContents(
const GURL& url,
bool is_subresource,
NavigationEntry* entry,
content::WebContents* web_contents,
bool whitelist_only,
SBThreatType* threat_type) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
GURL lookup_url = GetWhitelistUrl(url, is_subresource, entry);
if (lookup_url.is_empty())
return false;
WhitelistUrlSet* site_list =
static_cast<WhitelistUrlSet*>(web_contents->GetUserData(kWhitelistKey));
if (!site_list)
return false;
bool whitelisted = site_list->Contains(lookup_url, threat_type);
if (whitelist_only) {
return whitelisted;
} else {
return whitelisted || site_list->ContainsPending(lookup_url, threat_type);
}
}
// Static.
GURL SafeBrowsingUIManager::GetMainFrameWhitelistUrlForResourceForTesting(
const security_interstitials::UnsafeResource& resource) {
return GetMainFrameWhitelistUrlForResource(resource);
}
} // namespace safe_browsing