blob: a695e4a749b38fdd316b83991f82b5b1285e3760 [file] [log] [blame]
// 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