| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/fenced_frame/fenced_frame_reporter.h" |
| |
| #include <iterator> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <set> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "base/atomic_sequence_num.h" |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/containers/flat_map.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/notreached.h" |
| #include "base/strings/strcat.h" |
| #include "base/types/pass_key.h" |
| #include "content/browser/attribution_reporting/attribution_beacon_id.h" |
| #include "content/browser/attribution_reporting/attribution_data_host_manager.h" |
| #include "content/browser/attribution_reporting/attribution_host.h" |
| #include "content/browser/attribution_reporting/attribution_manager.h" |
| #include "content/browser/attribution_reporting/attribution_suitable_context.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/devtools/network_service_devtools_observer.h" |
| #include "content/browser/devtools/protocol/network_handler.h" |
| #include "content/browser/devtools/render_frame_devtools_agent_host.h" |
| #include "content/browser/fenced_frame/fenced_frame_config.h" |
| #include "content/browser/interest_group/interest_group_pa_report_util.h" |
| #include "content/browser/private_aggregation/private_aggregation_budget_key.h" |
| #include "content/browser/private_aggregation/private_aggregation_manager.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/browser/web_contents/web_contents_impl.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_client.h" |
| #include "content/services/auction_worklet/public/mojom/private_aggregation_request.mojom.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "net/url_request/redirect_info.h" |
| #include "services/network/public/cpp/attribution_utils.h" |
| #include "services/network/public/cpp/devtools_observer_util.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/public/mojom/attribution.mojom.h" |
| #include "services/network/public/mojom/fetch_api.mojom.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h" |
| #include "third_party/blink/public/common/fenced_frame/redacted_fenced_frame_config.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| #include "url/url_constants.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr net::NetworkTrafficAnnotationTag kReportingBeaconNetworkTag = |
| net::DefineNetworkTrafficAnnotation("fenced_frame_reporting_beacon", |
| R"( |
| semantics { |
| sender: "Fenced frame reportEvent API" |
| description: |
| "This request sends out reporting beacon data in an HTTP POST " |
| "request. This is initiated by window.fence.reportEvent API." |
| trigger: |
| "When there are events such as impressions, user interactions and " |
| "clicks, fenced frames can invoke window.fence.reportEvent API. It " |
| "tells the browser to send a beacon with event data to a URL " |
| "registered by the worklet in registerAdBeacon. Please see " |
| "https://github.com/WICG/turtledove/blob/main/Fenced_Frames_Ads_Reporting.md#reportevent" |
| data: |
| "Event data given by fenced frame reportEvent API. Please see " |
| "https://github.com/WICG/turtledove/blob/main/Fenced_Frames_Ads_Reporting.md#parameters" |
| destination: OTHER |
| destination_other: "The reporting destination given by FLEDGE's " |
| "registerAdBeacon API or selectURL's inputs." |
| internal { |
| contacts { |
| email: "chrome-fenced-frames@google.com" |
| } |
| } |
| user_data { |
| type: NONE |
| } |
| last_reviewed: "2023-01-04" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: "To use reportEvent API, users need to enable selectURL, " |
| "FLEDGE and FencedFrames features by enabling the Privacy Sandbox " |
| "Ads APIs experiment flag at " |
| "chrome://flags/#privacy-sandbox-ads-apis " |
| policy_exception_justification: "This beacon is sent by fenced frame " |
| "calling window.fence.reportEvent when there are events like user " |
| "interactions." |
| } |
| )"); |
| |
| std::string_view ReportingDestinationAsString( |
| const blink::FencedFrame::ReportingDestination& destination) { |
| switch (destination) { |
| case blink::FencedFrame::ReportingDestination::kBuyer: |
| return "Buyer"; |
| case blink::FencedFrame::ReportingDestination::kSeller: |
| return "Seller"; |
| case blink::FencedFrame::ReportingDestination::kComponentSeller: |
| return "ComponentSeller"; |
| case blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl: |
| return "SharedStorageSelectUrl"; |
| case blink::FencedFrame::ReportingDestination::kDirectSeller: |
| return "DirectSeller"; |
| } |
| NOTREACHED(); |
| } |
| |
| std::string_view InvokingAPIAsString( |
| const PrivacySandboxInvokingAPI invoking_api) { |
| switch (invoking_api) { |
| case PrivacySandboxInvokingAPI::kProtectedAudience: |
| return "Protected Audience"; |
| case PrivacySandboxInvokingAPI::kSharedStorage: |
| return "Shared Storage"; |
| } |
| NOTREACHED(); |
| } |
| |
| std::string AutomaticBeaconTypeAsString( |
| const blink::mojom::AutomaticBeaconType type) { |
| switch (type) { |
| case blink::mojom::AutomaticBeaconType::kDeprecatedTopNavigation: |
| return blink::kDeprecatedFencedFrameTopNavigationBeaconType; |
| case blink::mojom::AutomaticBeaconType::kTopNavigationStart: |
| return blink::kFencedFrameTopNavigationStartBeaconType; |
| case blink::mojom::AutomaticBeaconType::kTopNavigationCommit: |
| return blink::kFencedFrameTopNavigationCommitBeaconType; |
| default: |
| return ""; |
| } |
| } |
| |
| blink::FencedFrameBeaconReportingResult CreateBeaconReportingResultEnum( |
| const FencedFrameReporter::DestinationVariant& event_variant, |
| std::optional<int> http_response_code) { |
| // Unfortunately std::visit can't make this more compact, because each |
| // combination of results produces a unique output enum. |
| if (std::holds_alternative<DestinationEnumEvent>(event_variant)) { |
| if (!http_response_code.has_value()) { |
| return blink::FencedFrameBeaconReportingResult::kDestinationEnumInvalid; |
| } |
| if (*http_response_code != 200) { |
| return blink::FencedFrameBeaconReportingResult::kDestinationEnumFailure; |
| } |
| return blink::FencedFrameBeaconReportingResult::kDestinationEnumSuccess; |
| } |
| |
| if (std::holds_alternative<DestinationURLEvent>(event_variant)) { |
| if (!http_response_code.has_value()) { |
| return blink::FencedFrameBeaconReportingResult::kDestinationUrlInvalid; |
| } |
| if (*http_response_code != 200) { |
| return blink::FencedFrameBeaconReportingResult::kDestinationUrlFailure; |
| } |
| return blink::FencedFrameBeaconReportingResult::kDestinationUrlSuccess; |
| } |
| |
| if (std::holds_alternative<AutomaticBeaconEvent>(event_variant)) { |
| if (!http_response_code.has_value()) { |
| return blink::FencedFrameBeaconReportingResult::kAutomaticInvalid; |
| } |
| if (*http_response_code != 200) { |
| return blink::FencedFrameBeaconReportingResult::kAutomaticFailure; |
| } |
| return blink::FencedFrameBeaconReportingResult::kAutomaticSuccess; |
| } |
| |
| return blink::FencedFrameBeaconReportingResult::kUnknownResult; |
| } |
| |
| void RecordBeaconReportingResultHistogram( |
| const FencedFrameReporter::DestinationVariant& event_variant, |
| net::HttpResponseHeaders* headers) { |
| std::optional<int> http_response_code; |
| |
| if (headers != nullptr) { |
| http_response_code = headers->response_code(); |
| } |
| |
| base::UmaHistogramEnumeration( |
| blink::kFencedFrameBeaconReportingHttpResultUMA, |
| CreateBeaconReportingResultEnum(event_variant, http_response_code)); |
| } |
| |
| } // namespace |
| |
| FencedFrameReporter::PendingEvent::PendingEvent( |
| const DestinationVariant& event, |
| const url::Origin& request_initiator, |
| const net::ReferrerPolicy request_referrer_policy, |
| std::optional<AttributionReportingData> attribution_reporting_data, |
| FrameTreeNodeId initiator_frame_tree_node_id) |
| : event(event), |
| request_initiator(request_initiator), |
| request_referrer_policy(request_referrer_policy), |
| attribution_reporting_data(std::move(attribution_reporting_data)), |
| initiator_frame_tree_node_id(initiator_frame_tree_node_id) {} |
| |
| FencedFrameReporter::PendingEvent::PendingEvent(const PendingEvent&) = default; |
| |
| FencedFrameReporter::PendingEvent::PendingEvent(PendingEvent&&) = default; |
| |
| FencedFrameReporter::PendingEvent& FencedFrameReporter::PendingEvent::operator=( |
| const PendingEvent&) = default; |
| |
| FencedFrameReporter::PendingEvent& FencedFrameReporter::PendingEvent::operator=( |
| PendingEvent&&) = default; |
| |
| FencedFrameReporter::PendingEvent::~PendingEvent() = default; |
| |
| FencedFrameReporter::ReportingDestinationInfo::ReportingDestinationInfo( |
| std::optional<url::Origin> reporting_url_declarer_origin, |
| std::optional<ReportingUrlMap> reporting_url_map) |
| : reporting_url_declarer_origin(reporting_url_declarer_origin), |
| reporting_url_map(std::move(reporting_url_map)) {} |
| |
| FencedFrameReporter::ReportingDestinationInfo::ReportingDestinationInfo( |
| ReportingDestinationInfo&&) = default; |
| |
| FencedFrameReporter::ReportingDestinationInfo::~ReportingDestinationInfo() = |
| default; |
| |
| FencedFrameReporter::ReportingDestinationInfo& |
| FencedFrameReporter::ReportingDestinationInfo::operator=( |
| ReportingDestinationInfo&&) = default; |
| |
| scoped_refptr<FencedFrameReporter> FencedFrameReporter::CreateForSharedStorage( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| BrowserContext* browser_context, |
| const std::optional<url::Origin>& reporting_url_declarer_origin, |
| ReportingUrlMap reporting_url_map, |
| const url::Origin& main_frame_origin) { |
| // `private_aggregation_manager_` and `winner_origin_` |
| // are only needed by FLEDGE. |
| scoped_refptr<FencedFrameReporter> reporter = |
| base::MakeRefCounted<FencedFrameReporter>( |
| base::PassKey<FencedFrameReporter>(), |
| PrivacySandboxInvokingAPI::kSharedStorage, |
| std::move(url_loader_factory), browser_context, main_frame_origin); |
| reporter->reporting_metadata_.emplace( |
| blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl, |
| ReportingDestinationInfo(reporting_url_declarer_origin, |
| std::move(reporting_url_map))); |
| return reporter; |
| } |
| |
| scoped_refptr<FencedFrameReporter> FencedFrameReporter::CreateForFledge( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| BrowserContext* browser_context, |
| bool direct_seller_is_seller, |
| PrivateAggregationManager* private_aggregation_manager, |
| const url::Origin& main_frame_origin, |
| const url::Origin& winner_origin, |
| const std::optional<url::Origin>& aggregation_coordinator_origin, |
| const std::optional<std::vector<url::Origin>>& allowed_reporting_origins) { |
| scoped_refptr<FencedFrameReporter> reporter = |
| base::MakeRefCounted<FencedFrameReporter>( |
| base::PassKey<FencedFrameReporter>(), |
| PrivacySandboxInvokingAPI::kProtectedAudience, |
| std::move(url_loader_factory), browser_context, main_frame_origin, |
| private_aggregation_manager, winner_origin, |
| aggregation_coordinator_origin, allowed_reporting_origins); |
| reporter->direct_seller_is_seller_ = direct_seller_is_seller; |
| reporter->reporting_metadata_.emplace( |
| blink::FencedFrame::ReportingDestination::kBuyer, |
| ReportingDestinationInfo()); |
| reporter->reporting_metadata_.emplace( |
| blink::FencedFrame::ReportingDestination::kSeller, |
| ReportingDestinationInfo()); |
| reporter->reporting_metadata_.emplace( |
| blink::FencedFrame::ReportingDestination::kComponentSeller, |
| ReportingDestinationInfo()); |
| return reporter; |
| } |
| |
| FencedFrameReporter::FencedFrameReporter( |
| base::PassKey<FencedFrameReporter> pass_key, |
| PrivacySandboxInvokingAPI invoking_api, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory, |
| BrowserContext* browser_context, |
| const url::Origin& main_frame_origin, |
| PrivateAggregationManager* private_aggregation_manager, |
| const std::optional<url::Origin>& winner_origin, |
| const std::optional<url::Origin>& winner_aggregation_coordinator_origin, |
| const std::optional<std::vector<url::Origin>>& allowed_reporting_origins) |
| : url_loader_factory_(std::move(url_loader_factory)), |
| attribution_manager_( |
| AttributionManager::FromBrowserContext(browser_context)), |
| browser_context_(browser_context), |
| main_frame_origin_(main_frame_origin), |
| private_aggregation_manager_(private_aggregation_manager), |
| winner_origin_(winner_origin), |
| winner_aggregation_coordinator_origin_( |
| winner_aggregation_coordinator_origin), |
| allowed_reporting_origins_(allowed_reporting_origins), |
| invoking_api_(invoking_api) { |
| DCHECK(url_loader_factory_); |
| DCHECK(browser_context_); |
| |
| // `winner_origin` should have a value if and only if this a Protected |
| // Audience reporter. |
| DCHECK_EQ(invoking_api == PrivacySandboxInvokingAPI::kProtectedAudience, |
| winner_origin.has_value()); |
| } |
| |
| FencedFrameReporter::~FencedFrameReporter() { |
| for (const auto& [destination, destination_info] : reporting_metadata_) { |
| for (const auto& pending_event : destination_info.pending_events) { |
| NotifyFencedFrameReportingBeaconFailed( |
| pending_event.attribution_reporting_data); |
| } |
| } |
| |
| base::UmaHistogramCustomCounts(blink::kFencedFrameBeaconReportingCountUMA, |
| beacons_sent_same_origin_, /*min=*/1, |
| /*exclusive_max=*/20, /*buckets=*/20); |
| base::UmaHistogramCustomCounts( |
| blink::kFencedFrameBeaconReportingCountCrossOriginUMA, |
| beacons_sent_cross_origin_, /*min=*/1, /*exclusive_max=*/20, |
| /*buckets=*/20); |
| } |
| |
| void FencedFrameReporter::OnUrlMappingReady( |
| blink::FencedFrame::ReportingDestination reporting_destination, |
| const std::optional<url::Origin>& reporting_url_declarer_origin, |
| ReportingUrlMap reporting_url_map, |
| std::optional<ReportingMacros> reporting_ad_macros) { |
| auto it = reporting_metadata_.find(reporting_destination); |
| CHECK(it != reporting_metadata_.end()); |
| DCHECK(!it->second.reporting_url_map); |
| DCHECK(!it->second.reporting_ad_macros); |
| |
| it->second.reporting_url_declarer_origin = reporting_url_declarer_origin; |
| it->second.reporting_url_map = std::move(reporting_url_map); |
| it->second.reporting_ad_macros = std::move(reporting_ad_macros); |
| auto pending_events = std::exchange(it->second.pending_events, {}); |
| for (const auto& pending_event : pending_events) { |
| std::string ignored_error_message; |
| blink::mojom::ConsoleMessageLevel ignored_console_message_level = |
| blink::mojom::ConsoleMessageLevel::kError; |
| const std::string devtools_request_id = |
| base::UnguessableToken::Create().ToString(); |
| SendReportInternal( |
| it->second, pending_event.event, reporting_destination, |
| pending_event.request_initiator, pending_event.request_referrer_policy, |
| pending_event.attribution_reporting_data, |
| pending_event.initiator_frame_tree_node_id, ignored_error_message, |
| ignored_console_message_level, devtools_request_id); |
| } |
| } |
| |
| bool FencedFrameReporter::SendReport( |
| const DestinationVariant& event_variant, |
| blink::FencedFrame::ReportingDestination reporting_destination, |
| RenderFrameHostImpl* request_initiator_frame, |
| std::string& error_message, |
| blink::mojom::ConsoleMessageLevel& console_message_level, |
| FrameTreeNodeId initiator_frame_tree_node_id, |
| std::optional<int64_t> navigation_id) { |
| DCHECK(request_initiator_frame); |
| |
| if (reporting_destination == |
| blink::FencedFrame::ReportingDestination::kDirectSeller) { |
| if (direct_seller_is_seller_) { |
| reporting_destination = blink::FencedFrame::ReportingDestination::kSeller; |
| } else { |
| reporting_destination = |
| blink::FencedFrame::ReportingDestination::kComponentSeller; |
| } |
| } |
| auto it = reporting_metadata_.find(reporting_destination); |
| // Check metadata registration for given destination. If there's no map, or |
| // the map is empty, can't send a request. An entry with a null (not empty) |
| // map means the map is pending, and is handled below. |
| if (it == reporting_metadata_.end() || |
| (std::holds_alternative<DestinationEnumEvent>(event_variant) && |
| it->second.reporting_url_map && it->second.reporting_url_map->empty())) { |
| error_message = base::StrCat( |
| {"This frame did not register reporting metadata for destination '", |
| ReportingDestinationAsString(reporting_destination), "'."}); |
| console_message_level = blink::mojom::ConsoleMessageLevel::kWarning; |
| return false; |
| } |
| |
| static base::AtomicSequenceNumber unique_id_counter; |
| |
| std::optional<AttributionReportingData> attribution_reporting_data; |
| |
| const std::string devtools_request_id = |
| base::UnguessableToken::Create().ToString(); |
| |
| WebContents* web_contents = |
| WebContents::FromRenderFrameHost(request_initiator_frame); |
| if (web_contents) { |
| network::mojom::AttributionSupport attribution_reporting_support = |
| static_cast<WebContentsImpl*>(web_contents)->GetAttributionSupport(); |
| auto suitable_context = |
| AttributionSuitableContext::Create(request_initiator_frame); |
| if (suitable_context.has_value()) { |
| BeaconId beacon_id(unique_id_counter.GetNext()); |
| |
| AttributionDataHostManager* manager = |
| suitable_context->data_host_manager(); |
| manager->NotifyFencedFrameReportingBeaconStarted( |
| beacon_id, std::move(*suitable_context), navigation_id, |
| devtools_request_id); |
| |
| attribution_reporting_data.emplace(AttributionReportingData{ |
| .beacon_id = beacon_id, |
| .is_automatic_beacon = navigation_id.has_value(), |
| .attribution_reporting_support = attribution_reporting_support, |
| }); |
| } |
| } |
| |
| url::Origin request_initiator = |
| request_initiator_frame->GetLastCommittedOrigin(); |
| net::ReferrerPolicy request_referrer_policy = net::ReferrerPolicy::ORIGIN; |
| |
| if (request_initiator_frame->policy_container_host()) { |
| request_referrer_policy = Referrer::ReferrerPolicyForUrlRequest( |
| request_initiator_frame->policy_container_host()->referrer_policy()); |
| } |
| |
| // Automatic beacons that originate from component ads shouldn't expose the ad |
| // component's origin in the referrer for the beacon or the frame's referrer |
| // policy. Instead, use the origin and referrer policy of the ad frame root. |
| if (base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesReportEventHeaderChanges) && |
| request_initiator_frame->frame_tree_node()->GetFencedFrameProperties() && |
| request_initiator_frame->frame_tree_node() |
| ->GetFencedFrameProperties() |
| ->is_ad_component()) { |
| FrameTreeNode* ad_component_root = |
| request_initiator_frame->frame_tree_node() |
| ->GetClosestAncestorWithFencedFrameProperties(); |
| FrameTreeNode* ad_root = |
| ad_component_root->GetParentOrOuterDocument() |
| ->frame_tree_node() |
| ->GetClosestAncestorWithFencedFrameProperties(); |
| CHECK(std::holds_alternative<AutomaticBeaconEvent>(event_variant)); |
| request_initiator = ad_root->current_frame_host()->GetLastCommittedOrigin(); |
| request_referrer_policy = |
| Referrer::ReferrerPolicyForUrlRequest(ad_root->current_frame_host() |
| ->policy_container_host() |
| ->referrer_policy()); |
| } |
| |
| // If the reporting URL map is pending, queue the event. |
| NotifyIsBeaconQueued( |
| event_variant, |
| /*is_queued=*/it->second.reporting_url_map == std::nullopt); |
| |
| if (it->second.reporting_url_map == std::nullopt) { |
| it->second.pending_events.emplace_back( |
| event_variant, request_initiator, request_referrer_policy, |
| std::move(attribution_reporting_data), initiator_frame_tree_node_id); |
| return true; |
| } |
| |
| return SendReportInternal(it->second, event_variant, reporting_destination, |
| request_initiator, request_referrer_policy, |
| attribution_reporting_data, |
| initiator_frame_tree_node_id, error_message, |
| console_message_level, devtools_request_id); |
| } |
| |
| bool FencedFrameReporter::SendReportInternal( |
| const ReportingDestinationInfo& reporting_destination_info, |
| const DestinationVariant& event_variant, |
| blink::FencedFrame::ReportingDestination reporting_destination, |
| const url::Origin& request_initiator, |
| const net::ReferrerPolicy request_referrer_policy, |
| const std::optional<AttributionReportingData>& attribution_reporting_data, |
| FrameTreeNodeId initiator_frame_tree_node_id, |
| std::string& error_message, |
| blink::mojom::ConsoleMessageLevel& console_message_level, |
| const std::string& devtools_request_id) { |
| // The URL map should not be pending at this point. |
| CHECK(reporting_destination_info.reporting_url_map.has_value()); |
| |
| // Compute the destination url for the report, and the origin that we will |
| // use as the initiator for the report's network request. |
| GURL destination_url; |
| url::Origin network_request_initiator = request_initiator; |
| if (std::holds_alternative<DestinationEnumEvent>(event_variant) || |
| std::holds_alternative<AutomaticBeaconEvent>(event_variant)) { |
| std::string event_type; |
| |
| if (std::holds_alternative<DestinationEnumEvent>(event_variant)) { |
| event_type = std::get<DestinationEnumEvent>(event_variant).type; |
| } else { |
| event_type = AutomaticBeaconTypeAsString( |
| std::get<AutomaticBeaconEvent>(event_variant).type); |
| } |
| |
| // Since the event references a destination enum, resolve the lookup based |
| // on the given destination and event type using the reporting metadata. |
| const auto url_iter = |
| reporting_destination_info.reporting_url_map->find(event_type); |
| if (url_iter == reporting_destination_info.reporting_url_map->end()) { |
| error_message = base::StrCat( |
| {"This frame did not register reporting url for destination '", |
| ReportingDestinationAsString(reporting_destination), |
| "' and event_type '", event_type, "'."}); |
| console_message_level = blink::mojom::ConsoleMessageLevel::kWarning; |
| NotifyFencedFrameReportingBeaconFailed(attribution_reporting_data); |
| return false; |
| } |
| |
| // Validate the reporting URL. |
| destination_url = url_iter->second; |
| if (!destination_url.is_valid() || !destination_url.SchemeIsHTTPOrHTTPS()) { |
| error_message = base::StrCat( |
| {"This frame registered invalid reporting url for destination '", |
| ReportingDestinationAsString(reporting_destination), |
| "' and event_type '", event_type, "'."}); |
| console_message_level = blink::mojom::ConsoleMessageLevel::kError; |
| NotifyFencedFrameReportingBeaconFailed(attribution_reporting_data); |
| return false; |
| } |
| |
| // Because the destination URL was chosen by the worklet and is unknown to |
| // the reportEvent caller, set `network_request_initiator` to the worklet's |
| // origin to prevent CSRF. |
| if (base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesAutomaticBeaconCredentials)) { |
| CHECK( |
| reporting_destination_info.reporting_url_declarer_origin.has_value()); |
| network_request_initiator = |
| reporting_destination_info.reporting_url_declarer_origin.value(); |
| } |
| } else { |
| // Since the event references a destination URL, use it directly. |
| // The URL should have been validated previously, to be a valid HTTPS URL. |
| CHECK(std::holds_alternative<DestinationURLEvent>(event_variant)); |
| |
| // Check that reportEvent to custom destination URLs with macro |
| // substitution is allowed in this context. (i.e., The macro map has a |
| // value.) |
| if (!reporting_destination_info.reporting_ad_macros.has_value()) { |
| error_message = |
| "This frame attempted to send a report to a custom destination URL " |
| "with macro substitution, which is not supported by the API that " |
| "created this frame's fenced frame config."; |
| console_message_level = blink::mojom::ConsoleMessageLevel::kError; |
| NotifyFencedFrameReportingBeaconFailed(attribution_reporting_data); |
| return false; |
| } |
| |
| // If there is no allowlist, or the allowlist is empty, provide a more |
| // specific error message. |
| if (!allowed_reporting_origins_.has_value() || |
| allowed_reporting_origins_->empty()) { |
| error_message = |
| "This frame attempted to send a report to a custom destination URL " |
| "with macro substitution, but no origins are allowed by its " |
| "allowlist."; |
| console_message_level = blink::mojom::ConsoleMessageLevel::kError; |
| NotifyFencedFrameReportingBeaconFailed(attribution_reporting_data); |
| return false; |
| } |
| |
| // If the origin allowlist has previously been violated, this feature is |
| // disabled for the lifetime of the FencedFrameReporter. This prevents |
| // an interest group from encoding cross-site data about a user in binary |
| // with its choices of allowed/disallowed origins. |
| if (attempted_custom_url_report_to_disallowed_origin_) { |
| error_message = |
| "This frame attempted to send a report to a custom destination URL " |
| "with macro substitution, but this functionality is disabled because " |
| "a request was previously attempted to a disallowed origin."; |
| console_message_level = blink::mojom::ConsoleMessageLevel::kError; |
| NotifyFencedFrameReportingBeaconFailed(attribution_reporting_data); |
| return false; |
| } |
| |
| const GURL& original_url = std::get<DestinationURLEvent>(event_variant).url; |
| if (!original_url.is_valid() || !original_url.SchemeIs(url::kHttpsScheme)) { |
| attempted_custom_url_report_to_disallowed_origin_ = true; |
| error_message = |
| "This frame attempted to send a report to an invalid custom " |
| "destination URL. No further reports to custom destination URLs will " |
| "be allowed for this fenced frame config."; |
| console_message_level = blink::mojom::ConsoleMessageLevel::kError; |
| NotifyFencedFrameReportingBeaconFailed(attribution_reporting_data); |
| return false; |
| } |
| |
| // Substitute macros in the specified URL using the macros. |
| destination_url = GURL(SubstituteMappedStrings( |
| original_url.spec(), |
| reporting_destination_info.reporting_ad_macros.value())); |
| if (!destination_url.is_valid() || |
| !destination_url.SchemeIs(url::kHttpsScheme)) { |
| attempted_custom_url_report_to_disallowed_origin_ = true; |
| error_message = |
| "This frame attempted to send a report to a custom destination URL " |
| "that is invalid after macro substitution. No further reports to " |
| "custom destination URLs will be allowed for this fenced frame " |
| "config."; |
| console_message_level = blink::mojom::ConsoleMessageLevel::kError; |
| NotifyFencedFrameReportingBeaconFailed(attribution_reporting_data); |
| return false; |
| } |
| |
| // Check whether the destination URL has an allowed origin. |
| url::Origin destination_origin = url::Origin::Create(destination_url); |
| bool is_allowed_origin = false; |
| for (auto& origin : allowed_reporting_origins_.value()) { |
| if (origin.IsSameOriginWith(destination_origin)) { |
| is_allowed_origin = true; |
| break; |
| } |
| } |
| |
| // If the destination URL has a disallowed origin, disable this feature for |
| // the lifetime of the FencedFrameReporter and return. |
| if (!is_allowed_origin) { |
| attempted_custom_url_report_to_disallowed_origin_ = true; |
| error_message = |
| "This frame attempted to send a report to a custom destination URL " |
| "with macro substitution to a disallowed origin. No further reports " |
| "to custom destination URLs will be allowed for this fenced frame " |
| "config."; |
| console_message_level = blink::mojom::ConsoleMessageLevel::kError; |
| NotifyFencedFrameReportingBeaconFailed(attribution_reporting_data); |
| return false; |
| } |
| } |
| |
| if (!GetContentClient() |
| ->browser() |
| ->IsPrivacySandboxReportingDestinationAttested( |
| browser_context_, url::Origin::Create(destination_url), |
| invoking_api_)) { |
| error_message = base::StrCat( |
| {"The reporting destination '", |
| ReportingDestinationAsString(reporting_destination), |
| "' is not attested for '", InvokingAPIAsString(invoking_api_), "'"}); |
| console_message_level = blink::mojom::ConsoleMessageLevel::kError; |
| NotifyFencedFrameReportingBeaconFailed(attribution_reporting_data); |
| return false; |
| } |
| |
| // Construct the resource request. |
| auto request = std::make_unique<network::ResourceRequest>(); |
| |
| request->url = destination_url; |
| request->mode = network::mojom::RequestMode::kCors; |
| request->request_initiator = network_request_initiator; |
| |
| request->credentials_mode = network::mojom::CredentialsMode::kOmit; |
| // Allow cookies on automatic beacons while third party cookies are enabled |
| // to help with adoption/debugging. |
| // (https://github.com/WICG/turtledove/issues/866) |
| // TODO(crbug.com/40286778): After 3PCD, this will be dead code and should be |
| // removed. |
| if (base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesAutomaticBeaconCredentials) && |
| std::holds_alternative<AutomaticBeaconEvent>(event_variant) && |
| GetContentClient() |
| ->browser() |
| ->AreDeprecatedAutomaticBeaconCredentialsAllowed( |
| browser_context_, destination_url, main_frame_origin_)) { |
| request->credentials_mode = network::mojom::CredentialsMode::kInclude; |
| } |
| if (std::holds_alternative<DestinationURLEvent>(event_variant)) { |
| request->method = net::HttpRequestHeaders::kGetMethod; |
| } else { |
| request->method = net::HttpRequestHeaders::kPostMethod; |
| } |
| if (base::FeatureList::IsEnabled( |
| blink::features::kFencedFramesReportEventHeaderChanges)) { |
| // For automatic beacons initiating from component ad frames, the |
| // request_initiator will have already been set to the root ad frame's |
| // origin by this point. For all cases, the request initiator will always be |
| // sanitized to just its origin. |
| request->referrer_policy = request_referrer_policy; |
| request->referrer = request_initiator.GetURL(); |
| } |
| request->trusted_params = network::ResourceRequest::TrustedParams(); |
| // We can't use the fenced frame's nonce here because it will force a |
| // transient opaque CookiePartitionKey as well. If we're enabling automatic |
| // beacon credentials, the correct credientials would not be attached. |
| request->trusted_params->isolation_info = |
| net::IsolationInfo::CreateTransient(/*nonce=*/std::nullopt); |
| |
| // `attribution_reporting_data` is guaranteed to be set iff attribution |
| // reporting is allowed in the initiator frame. |
| const bool is_attribution_reporting_allowed = |
| attribution_reporting_data.has_value(); |
| |
| if (attribution_manager_ && is_attribution_reporting_allowed) { |
| request->attribution_reporting_eligibility = |
| attribution_reporting_data->is_automatic_beacon |
| ? network::mojom::AttributionReportingEligibility::kNavigationSource |
| : network::mojom::AttributionReportingEligibility::kEventSource; |
| |
| request->attribution_reporting_support = |
| attribution_reporting_data->attribution_reporting_support; |
| } |
| |
| request->devtools_request_id = devtools_request_id; |
| FrameTreeNode* initiator_frame_tree_node = |
| FrameTreeNode::GloballyFindByID(initiator_frame_tree_node_id); |
| if (initiator_frame_tree_node) { |
| request->trusted_params->devtools_observer = |
| NetworkServiceDevToolsObserver::MakeSelfOwned( |
| initiator_frame_tree_node); |
| } |
| |
| std::optional<std::string> event_data; |
| if (std::holds_alternative<DestinationEnumEvent>(event_variant)) { |
| event_data.emplace(std::get<DestinationEnumEvent>(event_variant).data); |
| } |
| if (std::holds_alternative<AutomaticBeaconEvent>(event_variant)) { |
| event_data.emplace(std::get<AutomaticBeaconEvent>(event_variant).data); |
| } |
| |
| devtools_instrumentation::OnFencedFrameReportRequestSent( |
| initiator_frame_tree_node_id, devtools_request_id, *request, |
| event_data.value_or("")); |
| |
| // Create and configure `SimpleURLLoader` instance. |
| std::unique_ptr<network::SimpleURLLoader> simple_url_loader = |
| network::SimpleURLLoader::Create(std::move(request), |
| kReportingBeaconNetworkTag); |
| if (event_data.has_value()) { |
| simple_url_loader->AttachStringForUpload( |
| event_data.value(), /*upload_content_type=*/"text/plain;charset=UTF-8"); |
| } |
| |
| network::SimpleURLLoader* simple_url_loader_ptr = simple_url_loader.get(); |
| |
| AttributionDataHostManager* attribution_data_host_manager = |
| attribution_manager_ ? attribution_manager_->GetDataHostManager() |
| : nullptr; |
| |
| if (attribution_data_host_manager && is_attribution_reporting_allowed) { |
| // Notify Attribution Reporting API for the beacons. |
| simple_url_loader_ptr->SetOnRedirectCallback(base::BindRepeating( |
| [](base::WeakPtr<AttributionDataHostManager> |
| attribution_data_host_manager, |
| BeaconId beacon_id, const GURL& url_before_redirect, |
| const net::RedirectInfo& redirect_info, |
| const network::mojom::URLResponseHead& response_head, |
| std::vector<std::string>* removed_headers) { |
| if (attribution_data_host_manager) { |
| attribution_data_host_manager->NotifyFencedFrameReportingBeaconData( |
| beacon_id, url_before_redirect, response_head.headers.get(), |
| /*is_final_response=*/false); |
| } |
| }, |
| attribution_data_host_manager->AsWeakPtr(), |
| attribution_reporting_data->beacon_id)); |
| |
| // Send out the reporting beacon. |
| simple_url_loader_ptr->DownloadHeadersOnly( |
| url_loader_factory_.get(), |
| base::BindOnce( |
| [](DestinationVariant event_variant, |
| base::WeakPtr<AttributionDataHostManager> |
| attribution_data_host_manager, |
| BeaconId beacon_id, |
| std::unique_ptr<network::SimpleURLLoader> loader, |
| FrameTreeNodeId initiator_frame_tree_node_id, |
| std::string devtools_request_id, |
| scoped_refptr<net::HttpResponseHeaders> headers) { |
| if (attribution_data_host_manager) { |
| attribution_data_host_manager |
| ->NotifyFencedFrameReportingBeaconData( |
| beacon_id, loader->GetFinalURL(), headers.get(), |
| /*is_final_response=*/true); |
| } |
| // Set up DevTools integration for the response. |
| devtools_instrumentation::OnFencedFrameReportResponseReceived( |
| initiator_frame_tree_node_id, devtools_request_id, |
| loader->GetFinalURL(), headers); |
| |
| // Record UMA metrics for the destination. |
| RecordBeaconReportingResultHistogram(event_variant, |
| headers.get()); |
| }, |
| event_variant, attribution_data_host_manager->AsWeakPtr(), |
| attribution_reporting_data->beacon_id, std::move(simple_url_loader), |
| initiator_frame_tree_node_id, devtools_request_id)); |
| } else { |
| // Send out the reporting beacon. |
| simple_url_loader_ptr->DownloadHeadersOnly( |
| url_loader_factory_.get(), |
| base::BindOnce( |
| [](DestinationVariant event_variant, |
| std::unique_ptr<network::SimpleURLLoader> loader, |
| FrameTreeNodeId initiator_frame_tree_node_id, |
| std::string devtools_request_id, |
| scoped_refptr<net::HttpResponseHeaders> headers) { |
| // Set up DevTools integration for the response. |
| devtools_instrumentation::OnFencedFrameReportResponseReceived( |
| initiator_frame_tree_node_id, devtools_request_id, |
| loader->GetFinalURL(), headers); |
| |
| // Record UMA metrics for the destination. |
| RecordBeaconReportingResultHistogram(event_variant, |
| headers.get()); |
| }, |
| event_variant, std::move(simple_url_loader), |
| initiator_frame_tree_node_id, devtools_request_id)); |
| } |
| |
| // The associated histograms will be sent out in the FencedFrameReporter |
| // destructor. |
| std::visit( |
| [&](const auto& event) { |
| using Event = std::decay_t<decltype(event)>; |
| if constexpr (std::is_same_v<Event, DestinationEnumEvent> || |
| std::is_same_v<Event, DestinationURLEvent>) { |
| if (event.cross_origin_exposed) { |
| beacons_sent_cross_origin_++; |
| } else { |
| beacons_sent_same_origin_++; |
| } |
| } |
| }, |
| event_variant); |
| |
| return true; |
| } |
| |
| void FencedFrameReporter::AddObserverForTesting(ObserverForTesting* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void FencedFrameReporter::RemoveObserverForTesting( |
| const ObserverForTesting* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void FencedFrameReporter::OnForEventPrivateAggregationRequestsReceived( |
| std::map<std::string, FinalizedPrivateAggregationRequests> |
| private_aggregation_event_map) { |
| for (auto& [event_type, requests] : private_aggregation_event_map) { |
| FinalizedPrivateAggregationRequests& destination_vector = |
| private_aggregation_event_map_[event_type]; |
| destination_vector.insert(destination_vector.end(), |
| std::move_iterator(requests.begin()), |
| std::move_iterator(requests.end())); |
| } |
| |
| for (const std::string& pa_event_type : received_pa_events_) { |
| SendPrivateAggregationRequestsForEventInternal(pa_event_type); |
| } |
| } |
| |
| void FencedFrameReporter::SendPrivateAggregationRequestsForEvent( |
| const std::string& pa_event_type) { |
| if (!private_aggregation_manager_) { |
| // `private_aggregation_manager_` is nullptr when private aggregation |
| // feature flag is disabled, but a compromised renderer might still send |
| // events when it should not be able to. Simply ignores the events. |
| return; |
| } |
| |
| // Always insert `pa_event_type` to `received_pa_events_`, since |
| // `private_aggregation_event_map_` might grow with more entries when |
| // reportWin() completes. |
| received_pa_events_.emplace(pa_event_type); |
| |
| SendPrivateAggregationRequestsForEventInternal(pa_event_type); |
| } |
| |
| void FencedFrameReporter::SendPrivateAggregationRequestsForEventInternal( |
| const std::string& pa_event_type) { |
| DCHECK(private_aggregation_manager_); |
| DCHECK(winner_origin_.has_value() && |
| winner_origin_.value().scheme() == url::kHttpsScheme); |
| DCHECK(main_frame_origin_.scheme() == url::kHttpsScheme); |
| |
| auto it = private_aggregation_event_map_.find(pa_event_type); |
| if (it == private_aggregation_event_map_.end()) { |
| return; |
| } |
| |
| SplitContributionsIntoBatchesThenSendToHost( |
| /*requests=*/std::move(it->second), *private_aggregation_manager_, |
| /*reporting_origin=*/winner_origin_.value(), |
| /*aggregation_coordinator_origin=*/winner_aggregation_coordinator_origin_, |
| main_frame_origin_); |
| |
| // Remove the entry of key `pa_event_type` from |
| // `private_aggregation_event_map_` to avoid possibly sending the same |
| // requests more than once. As a result, receiving the same event type |
| // multiple times only triggers sending the event's requests once. |
| private_aggregation_event_map_.erase(it); |
| } |
| |
| const std::vector<blink::FencedFrame::ReportingDestination> |
| FencedFrameReporter::ReportingDestinations() { |
| std::vector<blink::FencedFrame::ReportingDestination> out; |
| for (const auto& reporting_metadata : reporting_metadata_) { |
| // Only add the reporting destination if the URL map has at least 1 entry. |
| // If the reporting URL map is null, it hasn't been received yet, so add |
| // the destination in case the URL map ends up populated with at least 1 |
| // entry. |
| if (!reporting_metadata.second.reporting_url_map || |
| !reporting_metadata.second.reporting_url_map->empty()) { |
| out.emplace_back(reporting_metadata.first); |
| } |
| } |
| return out; |
| } |
| |
| base::flat_map<blink::FencedFrame::ReportingDestination, url::Origin> |
| FencedFrameReporter::GetReportingUrlDeclarerOriginsForTesting() { |
| base::flat_map<blink::FencedFrame::ReportingDestination, url::Origin> out; |
| for (const auto& reporting_metadata : reporting_metadata_) { |
| if (reporting_metadata.second.reporting_url_declarer_origin) { |
| out.emplace(reporting_metadata.first, |
| *reporting_metadata.second.reporting_url_declarer_origin); |
| } |
| } |
| return out; |
| } |
| |
| base::flat_map<blink::FencedFrame::ReportingDestination, |
| FencedFrameReporter::ReportingUrlMap> |
| FencedFrameReporter::GetAdBeaconMapForTesting() { |
| base::flat_map<blink::FencedFrame::ReportingDestination, ReportingUrlMap> out; |
| for (const auto& reporting_metadata : reporting_metadata_) { |
| if (reporting_metadata.second.reporting_url_map) { |
| out.emplace(reporting_metadata.first, |
| *reporting_metadata.second.reporting_url_map); |
| } |
| } |
| return out; |
| } |
| |
| base::flat_map<blink::FencedFrame::ReportingDestination, |
| FencedFrameReporter::ReportingMacros> |
| FencedFrameReporter::GetAdMacrosForTesting() { |
| base::flat_map<blink::FencedFrame::ReportingDestination, ReportingMacros> out; |
| for (const auto& reporting_metadata : reporting_metadata_) { |
| if (reporting_metadata.second.reporting_ad_macros) { |
| out.emplace(reporting_metadata.first, |
| *reporting_metadata.second.reporting_ad_macros); |
| } |
| } |
| return out; |
| } |
| |
| std::set<std::string> FencedFrameReporter::GetReceivedPaEventsForTesting() |
| const { |
| return received_pa_events_; |
| } |
| |
| std::map<std::string, FencedFrameReporter::FinalizedPrivateAggregationRequests> |
| FencedFrameReporter::GetPrivateAggregationEventMapForTesting() { |
| std::map<std::string, FinalizedPrivateAggregationRequests> out; |
| for (auto& [event_type, requests] : private_aggregation_event_map_) { |
| for (auction_worklet::mojom::FinalizedPrivateAggregationRequestPtr& |
| request : requests) { |
| out[event_type].emplace_back(request.Clone()); |
| } |
| } |
| return out; |
| } |
| |
| void FencedFrameReporter::NotifyFencedFrameReportingBeaconFailed( |
| const std::optional<AttributionReportingData>& attribution_reporting_data) { |
| if (!attribution_reporting_data.has_value()) { |
| return; |
| } |
| |
| AttributionDataHostManager* attribution_data_host_manager = |
| attribution_manager_ ? attribution_manager_->GetDataHostManager() |
| : nullptr; |
| if (!attribution_data_host_manager) { |
| return; |
| } |
| |
| attribution_data_host_manager->NotifyFencedFrameReportingBeaconData( |
| attribution_reporting_data->beacon_id, |
| /*reporting_url=*/GURL(), /*headers=*/nullptr, |
| /*is_final_response=*/true); |
| } |
| |
| void FencedFrameReporter::NotifyIsBeaconQueued( |
| const DestinationVariant& event_variant, |
| bool is_queued) { |
| for (ObserverForTesting& observer : observers_) { |
| observer.OnBeaconQueued(event_variant, is_queued); |
| } |
| } |
| |
| } // namespace content |