| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/safe_browsing/content/browser/ui_manager.h" |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/observer_list.h" |
| #include "base/threading/thread.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "build/build_config.h" |
| #include "components/no_state_prefetch/browser/no_state_prefetch_contents.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/safe_browsing/content/browser/async_check_tracker.h" |
| #include "components/safe_browsing/content/browser/client_report_util.h" |
| #include "components/safe_browsing/content/browser/content_unsafe_resource_util.h" |
| #include "components/safe_browsing/content/browser/safe_browsing_blocking_page.h" |
| #include "components/safe_browsing/content/browser/threat_details.h" |
| #include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h" |
| #include "components/safe_browsing/core/browser/ping_manager.h" |
| #include "components/safe_browsing/core/common/features.h" |
| #include "components/safe_browsing/core/common/safe_browsing_prefs.h" |
| #include "components/safe_browsing/core/common/utils.h" |
| #include "components/security_interstitials/content/security_interstitial_tab_helper.h" |
| #include "components/security_interstitials/core/unsafe_resource.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/disallow_activation_reason.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/navigation_entry.h" |
| #include "content/public/browser/web_contents.h" |
| #include "url/gurl.h" |
| |
| using content::BrowserThread; |
| using content::NavigationEntry; |
| using content::WebContents; |
| using safe_browsing::ClientSafeBrowsingReportRequest; |
| using safe_browsing::HitReport; |
| using safe_browsing::SBThreatType; |
| |
| namespace safe_browsing { |
| |
| using enum ExtendedReportingLevel; |
| |
| SafeBrowsingUIManager::SafeBrowsingUIManager( |
| std::unique_ptr<Delegate> delegate, |
| std::unique_ptr<SafeBrowsingBlockingPageFactory> blocking_page_factory, |
| const GURL& default_safe_page) |
| : delegate_(std::move(delegate)), |
| blocking_page_factory_(std::move(blocking_page_factory)), |
| default_safe_page_(default_safe_page) {} |
| |
| SafeBrowsingUIManager::~SafeBrowsingUIManager() = default; |
| |
| void SafeBrowsingUIManager::Stop(bool shutdown) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (shutdown) { |
| shut_down_ = true; |
| } |
| } |
| |
| void SafeBrowsingUIManager::CreateAndSendHitReport( |
| const UnsafeResource& resource) { |
| WebContents* web_contents = |
| unsafe_resource_util::GetWebContentsForResource(resource); |
| DCHECK(web_contents); |
| std::unique_ptr<HitReport> hit_report = std::make_unique<HitReport>(); |
| hit_report->malicious_url = resource.url; |
| hit_report->is_subresource = false; |
| hit_report->threat_type = resource.threat_type; |
| hit_report->threat_source = resource.threat_source; |
| |
| NavigationEntry* entry = |
| unsafe_resource_util::GetNavigationEntryForResource(resource); |
| if (entry) { |
| hit_report->page_url = entry->GetURL(); |
| hit_report->referrer_url = entry->GetReferrer().url; |
| } |
| |
| // When 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.original_url.is_empty() && |
| resource.original_url != resource.url) { |
| hit_report->referrer_url = hit_report->page_url; |
| hit_report->page_url = resource.original_url; |
| } |
| |
| const auto& prefs = *delegate_->GetPrefs(web_contents->GetBrowserContext()); |
| |
| hit_report->extended_reporting_level = GetExtendedReportingLevel(prefs); |
| hit_report->is_enhanced_protection = IsEnhancedProtectionEnabled(prefs); |
| hit_report->is_metrics_reporting_active = |
| delegate_->IsMetricsAndCrashReportingEnabled(); |
| |
| MaybeReportSafeBrowsingHit(std::move(hit_report), web_contents); |
| |
| for (Observer& observer : observer_list_) |
| observer.OnSafeBrowsingHit(resource); |
| } |
| |
| void SafeBrowsingUIManager::CreateAndSendClientSafeBrowsingWarningShownReport( |
| const UnsafeResource& resource) { |
| WebContents* web_contents = |
| unsafe_resource_util::GetWebContentsForResource(resource); |
| DCHECK(web_contents); |
| std::unique_ptr<ClientSafeBrowsingReportRequest> report = |
| std::make_unique<ClientSafeBrowsingReportRequest>(); |
| client_report_utils::FillReportBasicResourceDetails(report.get(), resource); |
| |
| // When 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.original_url.is_empty() && |
| resource.original_url != resource.url) { |
| report->set_referrer_url(report->page_url()); |
| report->set_page_url(resource.original_url.spec()); |
| } |
| |
| report->set_type(ClientSafeBrowsingReportRequest::WARNING_SHOWN); |
| report->set_warning_shown_timestamp_msec( |
| base::Time::Now().InMillisecondsSinceUnixEpoch()); |
| report->mutable_warning_shown_info()->set_warning_type( |
| client_report_utils::GetWarningUXTypeFromSBThreatType( |
| resource.threat_type)); |
| MaybeSendClientSafeBrowsingWarningShownReport(std::move(report), |
| web_contents); |
| } |
| |
| void SafeBrowsingUIManager::StartDisplayingBlockingPage( |
| const security_interstitials::UnsafeResource& resource) { |
| content::WebContents* web_contents = |
| unsafe_resource_util::GetWebContentsForResource(resource); |
| |
| if (!web_contents) { |
| // Tab is gone. |
| resource.DispatchCallback(FROM_HERE, false /*proceed*/, |
| false /*showed_interstitial*/, |
| false /* has_post_commit_interstitial_skipped */); |
| return; |
| } |
| |
| prerender::NoStatePrefetchContents* no_state_prefetch_contents = |
| delegate_->GetNoStatePrefetchContentsIfExists(web_contents); |
| if (no_state_prefetch_contents) { |
| no_state_prefetch_contents->Destroy(prerender::FINAL_STATUS_SAFE_BROWSING); |
| // Tab is being prerendered. |
| resource.DispatchCallback(FROM_HERE, false /*proceed*/, |
| false /*showed_interstitial*/, |
| false /* has_post_commit_interstitial_skipped */); |
| return; |
| } |
| |
| // Whether we have a FrameTreeNode id or a RenderFrameHost id depends on |
| // whether SB was triggered for a frame navigation or a document's subresource |
| // load respectively. We consider both cases here. Also, we need to cancel |
| // corresponding prerenders for both case. |
| content::RenderFrameHost* rfh = nullptr; |
| if (resource.rfh_locator.render_frame_token) { |
| rfh = content::RenderFrameHost::FromFrameToken( |
| content::GlobalRenderFrameHostToken( |
| resource.rfh_locator.render_process_id, |
| blink::LocalFrameToken( |
| resource.rfh_locator.render_frame_token.value()))); |
| } |
| |
| // Handle subresource load in prerendered pages. |
| if (rfh && rfh->GetLifecycleState() == |
| content::RenderFrameHost::LifecycleState::kPrerendering) { |
| // Cancel prerenders directly when its subresource or its subframe’s |
| // subresource is unsafe. |
| bool is_inactive = rfh->IsInactiveAndDisallowActivation( |
| content::DisallowActivationReasonId::kSafeBrowsingUnsafeSubresource); |
| CHECK(is_inactive); |
| |
| resource.DispatchCallback(FROM_HERE, false /*proceed*/, |
| false /*showed_interstitial*/, |
| false /* has_post_commit_interstitial_skipped */); |
| return; |
| } |
| |
| // Handle main frame or its sub frame navigation in prerendered pages. |
| // TODO(crbug.com/40912417): For latter case, the cancellation of prerender is |
| // currently done by canceling them with BLOCKED_BY_CLIENT in loader |
| // throttle, because current implementation of Prerender cancels |
| // prerenders when the navigation of prerender's subframes (not only the |
| // main frame) are canceled with BLOCKED_BY_CLIENT, as the TODO comment |
| // below also mentions. We plan to change the cancellation way from using |
| // BLOCKED_BY_CLIENT as the subresource load case. |
| if (web_contents->IsPrerenderedFrame( |
| content::FrameTreeNodeId(resource.rfh_locator.frame_tree_node_id))) { |
| // TODO(mcnee): If we were to indicate that this does not show an |
| // interstitial, the loader throttle would cancel with ERR_ABORTED to |
| // suppress an error page, instead of blocking using ERR_BLOCKED_BY_CLIENT. |
| // Prerendering code needs to distiguish these cases, so we pretend that |
| // we've shown an interstitial to get a meaningful error code. |
| // Given that the only thing the |showed_interstitial| parameter is used for |
| // is controlling the error code, perhaps this should be renamed to better |
| // indicate its purpose. |
| resource.DispatchCallback(FROM_HERE, false /*proceed*/, |
| true /*showed_interstitial*/, |
| false /* has_post_commit_interstitial_skipped */); |
| return; |
| } |
| |
| // We don't show interstitials for extension triggered SB errors, since they |
| // might not be visible, and cause the extension to hang. The request is just |
| // cancelled instead. |
| if (delegate_->IsHostingExtension(web_contents)) { |
| resource.DispatchCallback(FROM_HERE, false /* proceed */, |
| false /* showed_interstitial */, |
| false /* has_post_commit_interstitial_skipped */); |
| return; |
| } |
| |
| // If the main frame load is still pending, we need to get the navigation URL |
| // and referrer URL from the navigation entry now, since they are required for |
| // threat reporting, and the entry will be destroyed once the request is |
| // failed. |
| if (AsyncCheckTracker::IsMainPageResourceLoadPending(resource)) { |
| content::NavigationEntry* entry = |
| unsafe_resource_util::GetNavigationEntryForResource(resource); |
| if (entry) { |
| security_interstitials::UnsafeResource resource_copy(resource); |
| resource_copy.navigation_url = entry->GetURL(); |
| resource_copy.referrer_url = entry->GetReferrer().url; |
| DisplayBlockingPage(resource_copy); |
| return; |
| } |
| } |
| DisplayBlockingPage(resource); |
| } |
| |
| bool SafeBrowsingUIManager::ShouldSendHitReport(HitReport* hit_report, |
| WebContents* web_contents) { |
| return web_contents && |
| hit_report->extended_reporting_level != SBER_LEVEL_OFF && |
| !web_contents->GetBrowserContext()->IsOffTheRecord(); |
| } |
| |
| bool SafeBrowsingUIManager::ShouldSendClientSafeBrowsingWarningShownReport( |
| WebContents* web_contents) { |
| if (!web_contents || !web_contents->GetBrowserContext()) { |
| return false; |
| } |
| const auto& prefs = *delegate_->GetPrefs(web_contents->GetBrowserContext()); |
| return GetExtendedReportingLevel(prefs) != SBER_LEVEL_OFF && |
| !web_contents->GetBrowserContext()->IsOffTheRecord(); |
| } |
| |
| // 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( |
| std::unique_ptr<HitReport> hit_report, |
| WebContents* web_contents) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // Send report if user opted-in to extended reporting and is not in |
| // incognito mode. |
| if (!ShouldSendHitReport(hit_report.get(), web_contents)) { |
| return; |
| } |
| |
| if (shut_down_) |
| return; |
| |
| DVLOG(1) << "ReportSafeBrowsingHit: " << hit_report->malicious_url << " " |
| << hit_report->page_url << " " << hit_report->referrer_url << " " |
| << hit_report->is_subresource << " " |
| << static_cast<int>(hit_report->threat_type); |
| delegate_->GetPingManager(web_contents->GetBrowserContext()) |
| ->ReportSafeBrowsingHit(std::move(hit_report)); |
| } |
| |
| void SafeBrowsingUIManager::MaybeSendClientSafeBrowsingWarningShownReport( |
| std::unique_ptr<ClientSafeBrowsingReportRequest> report, |
| WebContents* web_contents) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| // Send report if user opted-in to extended reporting and is not in incognito |
| // mode. |
| if (ShouldSendClientSafeBrowsingWarningShownReport(web_contents)) { |
| SendThreatDetails(web_contents->GetBrowserContext(), std::move(report)); |
| } |
| } |
| |
| // Static. |
| void SafeBrowsingUIManager::CreateAllowlistForTesting( |
| content::WebContents* web_contents) { |
| EnsureAllowlistCreated(web_contents); |
| } |
| |
| 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); |
| } |
| |
| const std::string SafeBrowsingUIManager::app_locale() const { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| return delegate_->GetApplicationLocale(); |
| } |
| |
| history::HistoryService* SafeBrowsingUIManager::history_service( |
| content::WebContents* web_contents) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| return delegate_->GetHistoryService(web_contents->GetBrowserContext()); |
| } |
| |
| const GURL SafeBrowsingUIManager::default_safe_page() const { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| return default_safe_page_; |
| } |
| |
| // If the user had opted-in to send ThreatDetails, this gets called |
| // when the report is ready. |
| void SafeBrowsingUIManager::SendThreatDetails( |
| content::BrowserContext* browser_context, |
| std::unique_ptr<ClientSafeBrowsingReportRequest> report) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (shut_down_) |
| return; |
| |
| DVLOG(1) << "Sending threat details."; |
| delegate_->GetPingManager(browser_context) |
| ->ReportThreatDetails(std::move(report)); |
| } |
| |
| // If HaTS surveys are enabled, then this gets called when the report is ready. |
| void SafeBrowsingUIManager::AttachThreatDetailsAndLaunchSurvey( |
| content::BrowserContext* browser_context, |
| std::unique_ptr<ClientSafeBrowsingReportRequest> report) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| if (shut_down_) { |
| return; |
| } |
| |
| DVLOG(1) << "Adding threat details to survey response payload."; |
| delegate_->GetPingManager(browser_context) |
| ->AttachThreatDetailsAndLaunchSurvey(std::move(report)); |
| } |
| |
| void SafeBrowsingUIManager::OnBlockingPageDone( |
| const std::vector<UnsafeResource>& resources, |
| bool proceed, |
| content::WebContents* web_contents, |
| const GURL& main_frame_url, |
| bool showed_interstitial) { |
| BaseUIManager::OnBlockingPageDone(resources, proceed, web_contents, |
| main_frame_url, showed_interstitial); |
| if (proceed && !resources.empty()) { |
| if (resources[0].threat_type == |
| SBThreatType::SB_THREAT_TYPE_MANAGED_POLICY_WARN) { |
| delegate_->TriggerUrlFilteringInterstitialExtensionEventIfDesired( |
| web_contents, main_frame_url, "ENTERPRISE_WARNED_BYPASS", |
| resources[0].rt_lookup_response); |
| return; |
| } |
| delegate_->TriggerSecurityInterstitialProceededExtensionEventIfDesired( |
| web_contents, main_frame_url, |
| GetThreatTypeStringForInterstitial(resources[0].threat_type), |
| /*net_error_code=*/0); |
| } |
| } |
| |
| security_interstitials::SecurityInterstitialPage* |
| SafeBrowsingUIManager::CreateBlockingPage( |
| content::WebContents* contents, |
| const GURL& blocked_url, |
| const UnsafeResource& unsafe_resource, |
| bool forward_extension_event, |
| std::optional<base::TimeTicks> blocked_page_shown_timestamp) { |
| security_interstitials::SecurityInterstitialPage* blocking_page = nullptr; |
| if (unsafe_resource.threat_type == |
| SBThreatType::SB_THREAT_TYPE_MANAGED_POLICY_WARN) { |
| blocking_page = blocking_page_factory_->CreateEnterpriseWarnPage( |
| this, contents, blocked_url, {unsafe_resource}); |
| |
| // Report that we showed an interstitial. |
| if (forward_extension_event) { |
| ForwardUrlFilteringInterstitialExtensionEventToEmbedder( |
| contents, blocked_url, "ENTERPRISE_WARNED_SEEN", |
| unsafe_resource.rt_lookup_response); |
| } |
| return blocking_page; |
| } else if (unsafe_resource.threat_type == |
| SBThreatType::SB_THREAT_TYPE_MANAGED_POLICY_BLOCK) { |
| blocking_page = blocking_page_factory_->CreateEnterpriseBlockPage( |
| this, contents, blocked_url, {unsafe_resource}); |
| |
| // Report that we showed an interstitial. |
| if (forward_extension_event) { |
| ForwardUrlFilteringInterstitialExtensionEventToEmbedder( |
| contents, blocked_url, "ENTERPRISE_BLOCKED_SEEN", |
| unsafe_resource.rt_lookup_response); |
| } |
| return blocking_page; |
| } |
| blocking_page = blocking_page_factory_->CreateSafeBrowsingPage( |
| this, contents, blocked_url, {unsafe_resource}, |
| /*should_trigger_reporting=*/true, blocked_page_shown_timestamp); |
| |
| // Report that we showed an interstitial. |
| if (forward_extension_event) { |
| ForwardSecurityInterstitialShownExtensionEventToEmbedder( |
| contents, blocked_url, |
| GetThreatTypeStringForInterstitial(unsafe_resource.threat_type), |
| /*net_error_code=*/0); |
| } |
| return blocking_page; |
| } |
| |
| void SafeBrowsingUIManager:: |
| ForwardSecurityInterstitialShownExtensionEventToEmbedder( |
| content::WebContents* web_contents, |
| const GURL& page_url, |
| const std::string& reason, |
| int net_error_code) { |
| delegate_->TriggerSecurityInterstitialShownExtensionEventIfDesired( |
| web_contents, page_url, reason, net_error_code); |
| } |
| void SafeBrowsingUIManager:: |
| ForwardUrlFilteringInterstitialExtensionEventToEmbedder( |
| content::WebContents* web_contents, |
| const GURL& page_url, |
| const std::string& threat_type, |
| safe_browsing::RTLookupResponse rt_lookup_response) { |
| delegate_->TriggerUrlFilteringInterstitialExtensionEventIfDesired( |
| web_contents, page_url, threat_type, rt_lookup_response); |
| } |
| } // namespace safe_browsing |