| // Copyright 2017 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/subresource_filter/content/browser/child_frame_navigation_filtering_throttle.h" |
| |
| #include <optional> |
| #include <sstream> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/debug/alias.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/stringprintf.h" |
| #include "components/subresource_filter/content/browser/ad_tagging_utils.h" |
| #include "components/subresource_filter/content/browser/subresource_filter_observer_manager.h" |
| #include "components/subresource_filter/content/shared/common/subresource_filter_utils.h" |
| #include "components/subresource_filter/core/browser/subresource_filter_constants.h" |
| #include "components/subresource_filter/core/common/common_features.h" |
| #include "components/subresource_filter/core/common/time_measurements.h" |
| #include "content/public/browser/global_routing_id.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "third_party/blink/public/mojom/devtools/console_message.mojom.h" |
| |
| namespace features { |
| |
| // Enables or disables performing SubresourceFilter checks from the Browser |
| // against any aliases for the requested URL found from DNS CNAME records. |
| BASE_FEATURE(kSendCnameAliasesToSubresourceFilterFromBrowser, |
| "SendCnameAliasesToSubresourceFilterFromBrowser", |
| base::FEATURE_DISABLED_BY_DEFAULT); |
| |
| } // namespace features |
| |
| namespace subresource_filter { |
| |
| ChildFrameNavigationFilteringThrottle::ChildFrameNavigationFilteringThrottle( |
| content::NavigationHandle* handle, |
| AsyncDocumentSubresourceFilter* parent_frame_filter, |
| bool bypass_alias_check, |
| base::RepeatingCallback<std::string(const GURL& url)> |
| disallow_message_callback, |
| std::optional<blink::FrameAdEvidence> ad_evidence) |
| : content::NavigationThrottle(handle), |
| parent_frame_filter_(parent_frame_filter), |
| alias_check_enabled_( |
| !bypass_alias_check && |
| base::FeatureList::IsEnabled( |
| features::kSendCnameAliasesToSubresourceFilterFromBrowser)), |
| disallow_message_callback_(std::move(disallow_message_callback)), |
| ad_evidence_(std::move(ad_evidence)) { |
| DCHECK(!IsInSubresourceFilterRoot(handle)); |
| DCHECK(parent_frame_filter_); |
| if (ad_evidence_.has_value()) { |
| // Complete the ad evidence as it will be used to make best-effort tagging |
| // decisions by request time for ongoing subframe navs. |
| ad_evidence_->set_is_complete(); |
| } |
| } |
| |
| ChildFrameNavigationFilteringThrottle:: |
| ~ChildFrameNavigationFilteringThrottle() { |
| switch (load_policy_) { |
| case LoadPolicy::EXPLICITLY_ALLOW: |
| [[fallthrough]]; |
| case LoadPolicy::ALLOW: |
| // TODO(crbug.com/40280666): Split metrics for different filter |
| // implementations. |
| UMA_HISTOGRAM_CUSTOM_MICRO_TIMES( |
| "SubresourceFilter.DocumentLoad.SubframeFilteringDelay.Allowed", |
| total_defer_time_, base::Microseconds(1), base::Seconds(10), 50); |
| break; |
| case LoadPolicy::WOULD_DISALLOW: |
| UMA_HISTOGRAM_CUSTOM_MICRO_TIMES( |
| "SubresourceFilter.DocumentLoad.SubframeFilteringDelay.WouldDisallow", |
| total_defer_time_, base::Microseconds(1), base::Seconds(10), 50); |
| break; |
| case LoadPolicy::DISALLOW: |
| UMA_HISTOGRAM_CUSTOM_MICRO_TIMES( |
| "SubresourceFilter.DocumentLoad.SubframeFilteringDelay.Disallowed2", |
| total_defer_time_, base::Microseconds(1), base::Seconds(10), 50); |
| break; |
| } |
| } |
| |
| content::NavigationThrottle::ThrottleCheckResult |
| ChildFrameNavigationFilteringThrottle::WillStartRequest() { |
| return MaybeDeferToCalculateLoadPolicy(); |
| } |
| |
| content::NavigationThrottle::ThrottleCheckResult |
| ChildFrameNavigationFilteringThrottle::WillRedirectRequest() { |
| return MaybeDeferToCalculateLoadPolicy(); |
| } |
| |
| content::NavigationThrottle::ThrottleCheckResult |
| ChildFrameNavigationFilteringThrottle::WillProcessResponse() { |
| DCHECK_NE(load_policy_, LoadPolicy::DISALLOW); |
| |
| if (alias_check_enabled_) { |
| std::vector<GURL> alias_urls; |
| const GURL& base_url = navigation_handle()->GetURL(); |
| |
| for (const auto& alias : navigation_handle()->GetDnsAliases()) { |
| if (alias == navigation_handle()->GetURL().host_piece()) { |
| continue; |
| } |
| |
| GURL::Replacements replacements; |
| replacements.SetHostStr(alias); |
| GURL alias_url = base_url.ReplaceComponents(replacements); |
| |
| if (alias_url.is_valid()) { |
| alias_urls.push_back(alias_url); |
| } |
| } |
| |
| if (!alias_urls.empty()) { |
| pending_load_policy_calculations_++; |
| parent_frame_filter_->GetLoadPolicyForSubdocumentURLs( |
| alias_urls, base::BindOnce(&ChildFrameNavigationFilteringThrottle:: |
| OnCalculatedLoadPoliciesFromAliasUrls, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| } |
| |
| // Load policy notifications should go out by WillProcessResponse, unless |
| // we received CNAME aliases in the response and alias checking is enabled. |
| // Defer if we are still performing any ruleset checks. If we are here, |
| // and there are outstanding load policy calculations, we are either in dry |
| // run mode or checking aliases. |
| if (pending_load_policy_calculations_ > 0) { |
| DCHECK(parent_frame_filter_->activation_state().activation_level == |
| mojom::ActivationLevel::kDryRun || |
| navigation_handle()->GetDnsAliases().size() > 0); |
| DeferStart(DeferStage::kWillProcessResponse); |
| return DEFER; |
| } |
| |
| NotifyLoadPolicy(); |
| return PROCEED; |
| } |
| |
| const char* ChildFrameNavigationFilteringThrottle::GetNameForLogging() { |
| return "ChildFrameNavigationFilteringThrottle"; |
| } |
| |
| void ChildFrameNavigationFilteringThrottle::HandleDisallowedLoad() { |
| if (parent_frame_filter_->activation_state().enable_logging) { |
| std::string console_message = |
| disallow_message_callback_.Run(navigation_handle()->GetURL()); |
| |
| // Use the parent's Page to log a message to the console so that if this |
| // frame is the root of a nested frame tree (e.g. fenced frame), the log |
| // message won't be associated with a to-be-destroyed Page. |
| navigation_handle() |
| ->GetParentFrameOrOuterDocument() |
| ->GetPage() |
| .GetMainDocument() |
| .AddMessageToConsole(blink::mojom::ConsoleMessageLevel::kError, |
| console_message); |
| } |
| |
| parent_frame_filter_->ReportDisallowedLoad(); |
| } |
| |
| content::NavigationThrottle::ThrottleCheckResult |
| ChildFrameNavigationFilteringThrottle::MaybeDeferToCalculateLoadPolicy() { |
| DCHECK_NE(load_policy_, LoadPolicy::DISALLOW); |
| if (load_policy_ == LoadPolicy::WOULD_DISALLOW) { |
| return PROCEED; |
| } |
| |
| pending_load_policy_calculations_ += 1; |
| parent_frame_filter_->GetLoadPolicyForSubdocument( |
| navigation_handle()->GetURL(), |
| base::BindOnce( |
| &ChildFrameNavigationFilteringThrottle::OnCalculatedLoadPolicy, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| // If the embedder document has activation enabled, we calculate frame load |
| // policy before proceeding with navigation as filtered navigations are not |
| // allowed to get a response. As a result, we must defer while |
| // we wait for the ruleset check to complete and pass handling the navigation |
| // decision to the callback. |
| // |
| // If `kTPCDAdHeuristicSubframeRequestTagging`, we always need to defer |
| // navigation start to ensure we have the load policy calculated in order |
| // to properly tag the navigation handle as an ad before it goes to the |
| // network. |
| if (parent_frame_filter_->activation_state().activation_level == |
| mojom::ActivationLevel::kEnabled || |
| base::FeatureList::IsEnabled(kTPCDAdHeuristicSubframeRequestTagging)) { |
| DeferStart(DeferStage::kWillStartOrRedirectRequest); |
| return DEFER; |
| } |
| |
| // Otherwise, issue the ruleset request in parallel as an optimization. |
| return PROCEED; |
| } |
| |
| void ChildFrameNavigationFilteringThrottle::OnCalculatedLoadPolicy( |
| LoadPolicy policy) { |
| // TODO(crbug.com/40116607): Modify this call in cases where the new |
| // |policy| matches an explicitly allowed rule, rather than using the most |
| // restrictive policy for the redirect chain. |
| load_policy_ = MoreRestrictiveLoadPolicy(policy, load_policy_); |
| pending_load_policy_calculations_ -= 1; |
| |
| // Callback is not responsible for handling navigation if we are not deferred. |
| if (defer_stage_ == DeferStage::kNotDeferring) { |
| return; |
| } |
| |
| DCHECK(defer_stage_ == DeferStage::kWillProcessResponse || |
| defer_stage_ == DeferStage::kWillStartOrRedirectRequest); |
| |
| // If we have an activation enabled and `load_policy_` is DISALLOW, we need |
| // to cancel the navigation. |
| if (parent_frame_filter_->activation_state().activation_level == |
| mojom::ActivationLevel::kEnabled && |
| load_policy_ == LoadPolicy::DISALLOW) { |
| CancelNavigation(); |
| return; |
| } |
| |
| // If there are still pending load calculations, then don't resume. |
| if (pending_load_policy_calculations_ > 0) { |
| return; |
| } |
| |
| if (defer_stage_ == DeferStage::kWillStartOrRedirectRequest && |
| ad_evidence_.has_value()) { |
| // Tag the navigation handle based on the current load policy + evidence |
| // before the request starts. |
| ad_evidence_->UpdateFilterListResult( |
| InterpretLoadPolicyAsEvidence(load_policy_)); |
| if (ad_evidence_->IndicatesAdFrame()) { |
| navigation_handle()->SetIsAdTagged(); |
| } |
| } |
| |
| ResumeNavigation(); |
| } |
| |
| void ChildFrameNavigationFilteringThrottle:: |
| OnCalculatedLoadPoliciesFromAliasUrls(std::vector<LoadPolicy> policies) { |
| // We deferred to check aliases in WillProcessResponse. |
| DCHECK(defer_stage_ == DeferStage::kWillProcessResponse); |
| DCHECK(!policies.empty()); |
| |
| LoadPolicy most_restricive_alias_policy = LoadPolicy::EXPLICITLY_ALLOW; |
| |
| for (LoadPolicy policy : policies) { |
| most_restricive_alias_policy = |
| MoreRestrictiveLoadPolicy(most_restricive_alias_policy, policy); |
| } |
| |
| OnCalculatedLoadPolicy(most_restricive_alias_policy); |
| } |
| |
| void ChildFrameNavigationFilteringThrottle::DeferStart(DeferStage stage) { |
| DCHECK(defer_stage_ == DeferStage::kNotDeferring); |
| DCHECK(stage != DeferStage::kNotDeferring); |
| defer_stage_ = stage; |
| last_defer_timestamp_ = base::TimeTicks::Now(); |
| } |
| |
| void ChildFrameNavigationFilteringThrottle::NotifyLoadPolicy() const { |
| // TODO(crbug.com/40280666): Separate the notification mechanism from the |
| // current ContentSubresourceFilterThrottleManager to allow multiple filters |
| // with different throttle managers to use this class. |
| auto* observer_manager = SubresourceFilterObserverManager::FromWebContents( |
| navigation_handle()->GetWebContents()); |
| if (!observer_manager) { |
| return; |
| } |
| |
| observer_manager->NotifyChildFrameNavigationEvaluated(navigation_handle(), |
| load_policy_); |
| } |
| |
| void ChildFrameNavigationFilteringThrottle::UpdateDeferInfo() { |
| DCHECK(defer_stage_ != DeferStage::kNotDeferring); |
| DCHECK(!last_defer_timestamp_.is_null()); |
| total_defer_time_ += base::TimeTicks::Now() - last_defer_timestamp_; |
| defer_stage_ = DeferStage::kNotDeferring; |
| } |
| |
| void ChildFrameNavigationFilteringThrottle::CancelNavigation() { |
| bool defer_stage_was_will_process_response = |
| defer_stage_ == DeferStage::kWillProcessResponse; |
| |
| UpdateDeferInfo(); |
| HandleDisallowedLoad(); |
| NotifyLoadPolicy(); |
| |
| if (defer_stage_was_will_process_response) { |
| CancelDeferredNavigation(CANCEL); |
| } else { |
| CancelDeferredNavigation(BLOCK_REQUEST_AND_COLLAPSE); |
| } |
| } |
| |
| void ChildFrameNavigationFilteringThrottle::ResumeNavigation() { |
| // There are no more pending load calculations. We can toggle back to not |
| // being deferred. |
| bool defer_stage_was_will_process_response = |
| defer_stage_ == DeferStage::kWillProcessResponse; |
| UpdateDeferInfo(); |
| |
| // If the defer stage was WillProcessResponse, then this is the last |
| // LoadPolicy that we will calculate. |
| if (defer_stage_was_will_process_response) { |
| NotifyLoadPolicy(); |
| } |
| |
| Resume(); |
| } |
| |
| } // namespace subresource_filter |