| // 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/interest_group/ad_auction_headers_util.h" |
| |
| #include <cstddef> |
| #include <functional> |
| #include <map> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base64url.h" |
| #include "base/feature_list.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/types/expected.h" |
| #include "content/browser/devtools/devtools_instrumentation.h" |
| #include "content/browser/interest_group/ad_auction_page_data.h" |
| #include "content/browser/interest_group/interest_group_features.h" |
| #include "content/browser/renderer_host/frame_tree.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/browser/renderer_host/render_frame_host_impl.h" |
| #include "content/public/browser/page.h" |
| #include "content/public/browser/page_user_data.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "net/http/http_response_headers.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "services/network/public/cpp/permissions_policy/permissions_policy.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom-shared.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/mojom/devtools/console_message.mojom.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| // |
| // LINT.IfChange(AdditionalBidHeaderType) |
| enum class AdditionalBidHeaderType { |
| kAuctionNonce = 0, |
| kAuctionNonceAndSellerNonce = 1, |
| kMalformedHeaderWrongNumberOfColons = 2, |
| kMalformedHeaderWrongAuctionNonceSize = 3, |
| kMalformedHeaderWrongSellerNonceSize = 4, |
| |
| kMaxValue = kMalformedHeaderWrongSellerNonceSize, |
| }; |
| // LINT.ThenChange(//tools/metrics/histograms/enums.xml:AdditionalBidHeaderType) |
| |
| // Common conditions checked for eligibility in both |
| //`IsAdAuctionHeadersEligible` and `IsAdAuctionHeadersEligibleForNavigation`. |
| bool IsAdAuctionHeadersEligibleInternal(Page& page, |
| RenderFrameHost* render_frame_host, |
| const url::Origin& top_frame_origin, |
| const url::Origin& request_origin) { |
| if (!page.IsPrimary()) { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.NetHeaderResponse.StartRequestOutcome", |
| AdAuctionHeadersIsEligibleOutcomeForMetrics::kNotPrimaryPage); |
| return false; |
| } |
| |
| if (request_origin.opaque()) { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.NetHeaderResponse.StartRequestOutcome", |
| AdAuctionHeadersIsEligibleOutcomeForMetrics::kOpaqueRequestOrigin); |
| return false; |
| } |
| |
| if (!network::IsOriginPotentiallyTrustworthy(request_origin)) { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.NetHeaderResponse.StartRequestOutcome", |
| AdAuctionHeadersIsEligibleOutcomeForMetrics:: |
| kNotPotentiallyTrustworthy); |
| return false; |
| } |
| |
| if (!GetContentClient()->browser()->IsInterestGroupAPIAllowed( |
| render_frame_host->GetBrowserContext(), render_frame_host, |
| ContentBrowserClient::InterestGroupApiOperation::kSell, |
| top_frame_origin, request_origin)) { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.NetHeaderResponse.StartRequestOutcome", |
| AdAuctionHeadersIsEligibleOutcomeForMetrics::kApiNotAllowed); |
| return false; |
| } |
| |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.NetHeaderResponse.StartRequestOutcome", |
| AdAuctionHeadersIsEligibleOutcomeForMetrics::kSuccess); |
| return true; |
| } |
| |
| } // namespace |
| |
| bool IsAdAuctionHeadersEligible( |
| RenderFrameHostImpl& initiator_rfh, |
| const network::ResourceRequest& resource_request) { |
| DCHECK(resource_request.ad_auction_headers); |
| |
| // Fenced frames disallow most permissions policies which would let this |
| // function return false regardless, but adding this check to be more |
| // explicit. |
| if (initiator_rfh.IsNestedWithinFencedFrame()) { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.NetHeaderResponse.StartRequestOutcome", |
| AdAuctionHeadersIsEligibleOutcomeForMetrics::kInFencedFrame); |
| return false; |
| } |
| |
| const network::PermissionsPolicy* permissions_policy = |
| initiator_rfh.GetPermissionsPolicy(); |
| if (!permissions_policy->IsFeatureEnabledForOrigin( |
| network::mojom::PermissionsPolicyFeature::kRunAdAuction, |
| url::Origin::Create(resource_request.url), |
| /*override_default_policy_to_all=*/true)) { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.NetHeaderResponse.StartRequestOutcome", |
| AdAuctionHeadersIsEligibleOutcomeForMetrics:: |
| kDisabledByPermissionsPolicy); |
| return false; |
| } |
| |
| return IsAdAuctionHeadersEligibleInternal( |
| /*page=*/initiator_rfh.GetPage(), |
| /*render_frame_host=*/&initiator_rfh, |
| /*top_frame_origin=*/ |
| initiator_rfh.GetMainFrame()->GetLastCommittedOrigin(), |
| /*request_origin=*/url::Origin::Create(resource_request.url)); |
| } |
| |
| bool IsAdAuctionHeadersEligibleForNavigation( |
| const FrameTreeNode& frame, |
| const url::Origin& navigation_request_origin) { |
| // Fenced frames disallow most permissions policies which would let this |
| // function return false regardless, but adding this check to be more |
| // explicit. |
| if (frame.IsInFencedFrameTree()) { |
| return false; |
| } |
| |
| // The provided frame represents the iframe, so it must have a parent frame. |
| if (!frame.GetParentOrOuterDocument()) { |
| return false; |
| } |
| |
| const network::PermissionsPolicy* parent_policy = |
| frame.GetParentOrOuterDocument()->GetPermissionsPolicy(); |
| DCHECK(parent_policy); |
| if (!parent_policy->IsFeatureEnabledForOrigin( |
| network::mojom::PermissionsPolicyFeature::kRunAdAuction, |
| navigation_request_origin)) { |
| return false; |
| } |
| |
| return IsAdAuctionHeadersEligibleInternal( |
| /*page=*/frame.current_frame_host()->GetPage(), |
| /*render_frame_host=*/frame.GetParentOrOuterDocument(), |
| /*top_frame_origin=*/frame.frame_tree().root()->current_origin(), |
| /*request_origin=*/navigation_request_origin); |
| } |
| |
| // Please note: before modifying this function, please acknowledge this is |
| // processing untrusted content from a non sandboxed process. So please keep |
| // this function simple and avoid adding custom logic. |
| // |
| // Fuzzer: ad_auction_headers_util_fuzzer |
| std::vector<std::string> ParseAdAuctionResultResponseHeader( |
| const std::string& ad_auction_results) { |
| std::vector<std::string> parsed_results; |
| for (const auto& result : |
| base::SplitString(ad_auction_results, ",", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| std::string result_bytes; |
| if (!base::Base64UrlDecode(result, |
| base::Base64UrlDecodePolicy::IGNORE_PADDING, |
| &result_bytes)) { |
| continue; |
| } |
| if (result_bytes.size() != 32) { |
| continue; |
| } |
| parsed_results.emplace_back(std::move(result_bytes)); |
| } |
| return parsed_results; |
| } |
| |
| // Please note: before modifying this function, please acknowledge this is |
| // processing untrusted content from a non sandboxed process. So please keep |
| // this function simple and avoid adding custom logic. |
| // |
| // Fuzzer: ad_auction_headers_util_fuzzer |
| std::vector<std::string> ParseAdAuctionResultNonceResponseHeader( |
| const std::string& ad_auction_result_nonces) { |
| std::vector<std::string> parsed_results; |
| for (const auto& result : |
| base::SplitString(ad_auction_result_nonces, ",", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| base::Uuid result_uuid = base::Uuid::ParseCaseInsensitive(result); |
| if (!result_uuid.is_valid()) { |
| continue; |
| } |
| parsed_results.emplace_back(result_uuid.AsLowercaseString()); |
| } |
| return parsed_results; |
| } |
| |
| // Please note: before modifying this function, please acknowledge this is |
| // processing untrusted content from a non sandboxed process. So please keep |
| // this function simple and avoid adding custom logic. |
| // |
| // Fuzzer: ad_auction_headers_util_fuzzer |
| base::expected<void, std::string> ParseAdAuctionAdditionalBidResponseHeader( |
| const std::string& header_line, |
| std::map<std::string, std::vector<SignedAdditionalBidWithMetadata>>& |
| nonce_additional_bids_map) { |
| // Skip if `header_line` doesn't match either the usual format: |
| // |
| // <36 characters auction nonce>:<36 characters seller nonce>:<base64-encoded |
| // signed additional bid> |
| // |
| // OR the legacy format: |
| // |
| // <36 characters auction nonce>:<base64-encoded signed additional bid> |
| std::vector<std::string> nonces_and_additional_bid = base::SplitString( |
| header_line, ":", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| constexpr size_t kNonceSize = 36u; |
| |
| if (base::FeatureList::IsEnabled(blink::features::kFledgeSellerNonce) && |
| nonces_and_additional_bid.size() == 3u) { |
| std::string auction_nonce = std::move(nonces_and_additional_bid[0]); |
| if (auction_nonce.size() != kNonceSize) { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.Auction.AdditionalBids.HeaderReceived", |
| AdditionalBidHeaderType::kMalformedHeaderWrongAuctionNonceSize); |
| return base::unexpected(base::StringPrintf( |
| "Malformed %s: The first colon-delimited part (the auction nonce) is " |
| "expected to be %zu characters in size (representing the canonical " |
| "representation of a UUIDv4), but was instead %zu characters. " |
| "Header received: %s", |
| kAdAuctionAdditionalBidResponseHeaderKey, kNonceSize, |
| auction_nonce.size(), header_line)); |
| } |
| std::string seller_nonce = std::move(nonces_and_additional_bid[1]); |
| if (seller_nonce.size() != kNonceSize) { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.Auction.AdditionalBids.HeaderReceived", |
| AdditionalBidHeaderType::kMalformedHeaderWrongSellerNonceSize); |
| return base::unexpected(base::StringPrintf( |
| "Malformed %s: The second colon-delimited part (the seller nonce) is " |
| "expected to be %zu characters in size (representing the canonical " |
| "representation of a UUIDv4), but was instead %zu characters. " |
| "Header received: %s", |
| kAdAuctionAdditionalBidResponseHeaderKey, kNonceSize, |
| seller_nonce.size(), header_line)); |
| } |
| std::string additional_bid = std::move(nonces_and_additional_bid[2]); |
| |
| nonce_additional_bids_map[std::move(auction_nonce)].emplace_back( |
| /*signed_additional_bid=*/std::move(additional_bid), |
| /*seller_nonce=*/std::move(seller_nonce)); |
| |
| UMA_HISTOGRAM_ENUMERATION( |
| "Ads.InterestGroup.Auction.AdditionalBids.HeaderReceived", |
| AdditionalBidHeaderType::kAuctionNonceAndSellerNonce); |
| } else if (nonces_and_additional_bid.size() == 2u) { |
| std::string auction_nonce = std::move(nonces_and_additional_bid[0]); |
| if (auction_nonce.size() != kNonceSize) { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.Auction.AdditionalBids.HeaderReceived", |
| AdditionalBidHeaderType::kMalformedHeaderWrongAuctionNonceSize); |
| return base::unexpected(base::StringPrintf( |
| "Malformed %s: The first colon-delimited part (the auction nonce) is " |
| "expected to be %zu characters in size (representing the canonical " |
| "representation of a UUIDv4), but was instead %zu characters. " |
| "Header received: %s", |
| kAdAuctionAdditionalBidResponseHeaderKey, kNonceSize, |
| auction_nonce.size(), header_line)); |
| } |
| std::string additional_bid = std::move(nonces_and_additional_bid[1]); |
| |
| nonce_additional_bids_map[std::move(auction_nonce)].emplace_back( |
| /*signed_additional_bid=*/std::move(additional_bid), |
| /*seller_nonce=*/std::nullopt); |
| |
| UMA_HISTOGRAM_ENUMERATION( |
| "Ads.InterestGroup.Auction.AdditionalBids.HeaderReceived", |
| AdditionalBidHeaderType::kAuctionNonce); |
| } else { |
| base::UmaHistogramEnumeration( |
| "Ads.InterestGroup.Auction.AdditionalBids.HeaderReceived", |
| AdditionalBidHeaderType::kMalformedHeaderWrongNumberOfColons); |
| return base::unexpected(base::StringPrintf( |
| "Malformed %s: Expected two or three colon-delimited parts, but " |
| "instead received %zu. Header received: %s", |
| kAdAuctionAdditionalBidResponseHeaderKey, |
| nonces_and_additional_bid.size(), header_line)); |
| } |
| return base::ok(); |
| } |
| |
| // NOTE: This function processes untrusted content, in an unsafe language, from |
| // an unsandboxed process. As such, minimize the amount of parsing done in this |
| // function, and instead separate it into distinct functions that are covered |
| // by fuzz tests. |
| void ProcessAdAuctionResponseHeaders( |
| const url::Origin& request_origin, |
| RenderFrameHostImpl& rfh, |
| scoped_refptr<net::HttpResponseHeaders> headers) { |
| if (!headers) { |
| return; |
| } |
| AdAuctionPageData* ad_auction_page_data = |
| PageUserData<AdAuctionPageData>::GetOrCreateForPage(rfh.GetPage()); |
| |
| if (base::FeatureList::IsEnabled( |
| blink::features::kFledgeBiddingAndAuctionServer)) { |
| if (std::optional<std::string> ad_auction_results = |
| headers->GetNormalizedHeader(kAdAuctionResultResponseHeaderKey)) { |
| for (const std::string& parsed_result : |
| ParseAdAuctionResultResponseHeader(*ad_auction_results)) { |
| ad_auction_page_data->AddAuctionResultWitnessForOrigin(request_origin, |
| parsed_result); |
| } |
| } |
| } |
| // We intentionally leave the `Ad-Auction-Result` response header in place. |
| |
| if (base::FeatureList::IsEnabled( |
| features::kFledgeBiddingAndAuctionNonceSupport)) { |
| if (std::optional<std::string> ad_auction_results = |
| headers->GetNormalizedHeader( |
| kAdAuctionResultNonceResponseHeaderKey)) { |
| for (const std::string& nonce : |
| ParseAdAuctionResultNonceResponseHeader(*ad_auction_results)) { |
| ad_auction_page_data->AddAuctionResultNonceWitnessForOrigin( |
| request_origin, nonce); |
| } |
| } |
| } |
| headers->RemoveHeader(kAdAuctionResultNonceResponseHeaderKey); |
| |
| if (base::FeatureList::IsEnabled(blink::features::kAdAuctionSignals)) { |
| if (std::optional<std::string> ad_auction_signals = |
| headers->GetNormalizedHeader(kAdAuctionSignalsResponseHeaderKey)) { |
| if (ad_auction_signals->size() <= |
| static_cast<size_t>( |
| blink::features::kAdAuctionSignalsMaxSizeBytes.Get())) { |
| ad_auction_page_data->AddAuctionSignalsWitnessForOrigin( |
| request_origin, *ad_auction_signals); |
| } |
| } |
| } |
| headers->RemoveHeader(kAdAuctionSignalsResponseHeaderKey); |
| |
| std::map<std::string, std::vector<SignedAdditionalBidWithMetadata>> |
| nonce_additional_bids_map; |
| size_t iter = 0; |
| std::string header_line; |
| while (headers->EnumerateHeader( |
| &iter, kAdAuctionAdditionalBidResponseHeaderKey, &header_line)) { |
| base::expected<void, std::string> result = |
| ParseAdAuctionAdditionalBidResponseHeader(header_line, |
| nonce_additional_bids_map); |
| if (!result.has_value()) { |
| devtools_instrumentation::LogWorkletMessage( |
| rfh, blink::mojom::ConsoleMessageLevel::kError, |
| std::move(result).error()); |
| } |
| } |
| if (!nonce_additional_bids_map.empty()) { |
| ad_auction_page_data->AddAuctionAdditionalBidsWitnessForOrigin( |
| request_origin, std::move(nonce_additional_bids_map)); |
| } |
| headers->RemoveHeader(kAdAuctionAdditionalBidResponseHeaderKey); |
| } |
| |
| void RemoveAdAuctionResponseHeaders( |
| scoped_refptr<net::HttpResponseHeaders> headers) { |
| if (!headers) { |
| return; |
| } |
| // We intentionally leave the `Ad-Auction-Result` response header in place. |
| headers->RemoveHeader(kAdAuctionResultNonceResponseHeaderKey); |
| headers->RemoveHeader(kAdAuctionSignalsResponseHeaderKey); |
| headers->RemoveHeader(kAdAuctionAdditionalBidResponseHeaderKey); |
| } |
| |
| } // namespace content |