From 95148e48cb19f8cf67458a11d92fb366692f31a6 Mon Sep 17 00:00:00 2001 From: Garrett Tanzer Date: Wed, 16 Aug 2023 16:24:02 +0000 Subject: [PATCH] Fenced frames: Add reporting to custom destination URLs [4/N] Finish reportEvent to custom destination URLs: * If the destination origin isn't an allowed origin, disallow all future reports to custom destination URLs from this FencedFrameReporter. * Substitute macros into the destination URL. https://github.com/WICG/turtledove/issues/477 Change-Id: I222c94bf41d559b549a108834abb485ba0787e43 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4753243 Reviewed-by: Shivani Sharma Reviewed-by: Qingxin Wu Commit-Queue: Garrett Tanzer Cr-Commit-Position: refs/heads/main@{#1184195} --- .../fenced_frame/fenced_frame_browsertest.cc | 9 +- .../fenced_frame/fenced_frame_config.cc | 27 ++ .../fenced_frame/fenced_frame_config.h | 9 + .../fenced_frame/fenced_frame_reporter.cc | 88 +++++- .../fenced_frame/fenced_frame_reporter.h | 8 + .../fenced_frame_reporter_unittest.cc | 262 +++++++++++++++++- .../fenced_frame/fenced_frame_url_mapping.cc | 32 --- content/public/test/fenced_frame_test_util.cc | 2 +- 8 files changed, 389 insertions(+), 48 deletions(-) diff --git a/content/browser/fenced_frame/fenced_frame_browsertest.cc b/content/browser/fenced_frame/fenced_frame_browsertest.cc index d7ab339a327854..93860323b29db5 100644 --- a/content/browser/fenced_frame/fenced_frame_browsertest.cc +++ b/content/browser/fenced_frame/fenced_frame_browsertest.cc @@ -4746,7 +4746,11 @@ class FencedFrameReportEventBrowserTest *web_contents()->GetBrowserContext()), /*main_frame_origin=*/ web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin(), - /*winner_origin=*/url::Origin::Create(GURL("https://a.test"))); + /*winner_origin=*/url::Origin::Create(GURL("https://a.test")), + /*allowed_reporting_origins=*/ + {{url::Origin::Create(https_server()->GetURL("a.test", "/")), + url::Origin::Create(https_server()->GetURL("b.test", "/")), + url::Origin::Create(https_server()->GetURL("c.test", "/"))}}); } // A helper function for specifying reportEvent tests. Each step consists of a @@ -4829,7 +4833,8 @@ class FencedFrameReportEventBrowserTest blink::FencedFrame::ReportingDestination::kBuyer, { {"click", reporting_url}, - }); + }, + /*reporting_ad_macro_map=*/FencedFrameReporter::ReportingMacroMap()); // Set empty reporting url for seller. fenced_frame_reporter->OnUrlMappingReady( blink::FencedFrame::ReportingDestination::kSeller, {{"click", GURL()}}); diff --git a/content/browser/fenced_frame/fenced_frame_config.cc b/content/browser/fenced_frame/fenced_frame_config.cc index 7d66279cd44331..ccc7473a348c48 100644 --- a/content/browser/fenced_frame/fenced_frame_config.cc +++ b/content/browser/fenced_frame/fenced_frame_config.cc @@ -21,6 +21,33 @@ GURL GenerateUrnUuid() { base::Uuid::GenerateRandomV4().AsLowercaseString()); } +std::string SubstituteMappedStrings( + const std::string& input, + const std::vector>& substitutions) { + std::vector output_vec; + size_t input_idx = 0; + while (input_idx < input.size()) { + size_t replace_idx = input.size(); + size_t replace_end_idx = input.size(); + std::pair const* next_replacement = nullptr; + for (const auto& substitution : substitutions) { + size_t found_idx = input.find(substitution.first, input_idx); + if (found_idx < replace_idx) { + replace_idx = found_idx; + replace_end_idx = found_idx + substitution.first.size(); + next_replacement = &substitution; + } + } + output_vec.push_back(input.substr(input_idx, replace_idx - input_idx)); + if (replace_idx < input.size()) { + output_vec.push_back(next_replacement->second); + } + // move input index to after what we replaced (or end of string). + input_idx = replace_end_idx; + } + return base::StrCat(output_vec); +} + namespace { std::vector> diff --git a/content/browser/fenced_frame/fenced_frame_config.h b/content/browser/fenced_frame/fenced_frame_config.h index ccfcce95cb6bd7..c7bacbb5b9abb5 100644 --- a/content/browser/fenced_frame/fenced_frame_config.h +++ b/content/browser/fenced_frame/fenced_frame_config.h @@ -93,6 +93,15 @@ class FencedFrameURLMapping; extern const char kUrnUuidPrefix[]; GURL CONTENT_EXPORT GenerateUrnUuid(); +// Returns a new string based on input where the matching substrings have been +// replaced with the corresponding substitutions. This function avoids repeated +// string operations by building the output based on all substitutions, one +// substitution at a time. This effectively performs all substitutions +// simultaneously, with the earliest match in the input taking precedence. +std::string SubstituteMappedStrings( + const std::string& input, + const std::vector>& substitutions); + using AdAuctionData = blink::FencedFrame::AdAuctionData; using DeprecatedFencedFrameMode = blink::FencedFrame::DeprecatedFencedFrameMode; using SharedStorageBudgetMetadata = diff --git a/content/browser/fenced_frame/fenced_frame_reporter.cc b/content/browser/fenced_frame/fenced_frame_reporter.cc index 46bb9838142c6b..b748f1c7733adf 100644 --- a/content/browser/fenced_frame/fenced_frame_reporter.cc +++ b/content/browser/fenced_frame/fenced_frame_reporter.cc @@ -31,6 +31,7 @@ #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" @@ -307,7 +308,8 @@ bool FencedFrameReporter::SendReport( // 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() || - (it->second.reporting_url_map && it->second.reporting_url_map->empty())) { + (absl::holds_alternative(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), "'."}); @@ -389,7 +391,7 @@ bool FencedFrameReporter::SendReportInternal( return false; } - // Validate the reporting url. + // Validate the reporting URL. url = url_iter->second; if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) { error_message = base::StrCat( @@ -401,12 +403,84 @@ bool FencedFrameReporter::SendReportInternal( return false; } } else { - CHECK(absl::holds_alternative(event_variant)); // Since the event references a destination URL, use it directly. - // The URL should have been validated previously, to be a valid HTTPS url. - // TODO(gtanzer): Substitute macros from the reporting metadata as needed. - // TODO(gtanzer): Check whether the url is an allowlisted origin. - url = absl::get(event_variant).url; + // The URL should have been validated previously, to be a valid HTTPS URL. + CHECK(absl::holds_alternative(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_macro_map.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; + } + + // Substitute macros in the specified URL using the macro map. + // TODO(qingxinwu): Lift these changes up out of FencedFrameReporter into + // the code that constructs the reporting ad macro map. + std::vector> macro_map; + for (const auto& entry : + reporting_destination_info.reporting_ad_macro_map.value()) { + macro_map.emplace_back("${" + entry.first + "}", entry.second); + } + url = GURL(SubstituteMappedStrings( + absl::get(event_variant).url.spec(), macro_map)); + url::Origin destination_origin = url::Origin::Create(url); + + // Check whether the destination URL has an allowed origin. + 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() diff --git a/content/browser/fenced_frame/fenced_frame_reporter.h b/content/browser/fenced_frame/fenced_frame_reporter.h index a20ce9ab7c9e7c..6e820788e3ee34 100644 --- a/content/browser/fenced_frame/fenced_frame_reporter.h +++ b/content/browser/fenced_frame/fenced_frame_reporter.h @@ -377,6 +377,14 @@ class CONTENT_EXPORT FencedFrameReporter // Origins allowed to receive macro expanded reports. const absl::optional> allowed_reporting_origins_; + // Whether there has been an attempt to send a custom destination url with + // macro substitution report to a disallowed origin (according to + // `allowed_reporting_origins_`). Once this occurs, custom destination url + // reports will be disabled for the remainder of the FencedFrameReporter's + // lifetime. This prevents an interest group from encoding cross-site data + // about a user in binary with its choices of allowed/disallowed origins. + bool attempted_custom_url_report_to_disallowed_origin_ = false; + // Private aggregation requests for non-reserved event types registered in // bidder worklets, keyed by event type. // OnForEventPrivateAggregationRequestsReceived() builds this map up. diff --git a/content/browser/fenced_frame/fenced_frame_reporter_unittest.cc b/content/browser/fenced_frame/fenced_frame_reporter_unittest.cc index 35d3f5061ea5f7..846a4d1450635c 100644 --- a/content/browser/fenced_frame/fenced_frame_reporter_unittest.cc +++ b/content/browser/fenced_frame/fenced_frame_reporter_unittest.cc @@ -178,6 +178,10 @@ class FencedFrameReporterTest : public RenderViewHostTestHarness { url::Origin::Create(request_initiator_); const url::Origin report_destination_origin_ = url::Origin::Create(report_destination_); + const url::Origin report_destination2_origin_ = + url::Origin::Create(report_destination2_); + const url::Origin report_destination3_origin_ = + url::Origin::Create(report_destination3_); TestInterestGroupPrivateAggregationManager private_aggregation_manager_{ main_frame_origin_}; @@ -404,7 +408,8 @@ TEST_F(FencedFrameReporterTest, SendFledgeReportsAfterMapsReceived) { shared_url_loader_factory(), browser_context(), /*direct_seller_is_seller=*/false, &private_aggregation_manager_, main_frame_origin_, - /*winner_origin=*/report_destination_origin_); + /*winner_origin=*/report_destination_origin_, + /*allowed_reporting_origins=*/{{report_destination_origin_}}); // Receive all mappings. reporter->OnUrlMappingReady( @@ -415,7 +420,8 @@ TEST_F(FencedFrameReporterTest, SendFledgeReportsAfterMapsReceived) { /*reporting_url_map=*/{{"event_type", report_destination2_}}); reporter->OnUrlMappingReady( blink::FencedFrame::ReportingDestination::kBuyer, - /*reporting_url_map=*/{{"event_type", report_destination3_}}); + /*reporting_url_map=*/{{"event_type", report_destination3_}}, + /*reporting_ad_macro_map=*/FencedFrameReporter::ReportingMacroMap()); EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); // Make reports. Each should be sent immediately. @@ -477,7 +483,8 @@ TEST_F(FencedFrameReporterTest, SendReportsFledgeBeforeMapsReceived) { shared_url_loader_factory(), browser_context(), /*direct_seller_is_seller=*/true, &private_aggregation_manager_, main_frame_origin_, - /*winner_origin=*/report_destination_origin_); + /*winner_origin=*/report_destination_origin_, + /*allowed_reporting_origins=*/{{report_destination_origin_}}); // Make reports. They should be queued, since mappings haven't been received // yet. @@ -534,7 +541,8 @@ TEST_F(FencedFrameReporterTest, SendReportsFledgeBeforeMapsReceived) { reporter->OnUrlMappingReady( blink::FencedFrame::ReportingDestination::kBuyer, - /*reporting_url_map=*/{{"event_type", report_destination3_}}); + /*reporting_url_map=*/{{"event_type", report_destination3_}}, + /*reporting_ad_macro_map=*/FencedFrameReporter::ReportingMacroMap()); EXPECT_EQ(test_url_loader_factory_.NumPending(), 5); ValidateRequest((*test_url_loader_factory_.pending_requests())[3].request, report_destination3_, "event_data"); @@ -570,7 +578,8 @@ TEST_F(FencedFrameReporterTest, SendFledgeReportsBeforeMapsReceivedWithErrors) { shared_url_loader_factory(), browser_context(), /*direct_seller_is_seller=*/false, &private_aggregation_manager_, main_frame_origin_, - /*winner_origin=*/report_destination_origin_); + /*winner_origin=*/report_destination_origin_, + /*allowed_reporting_origins=*/{{report_destination_origin_}}); // SendReport() is called, and then a mapping is received that doesn't have // the report's event type. No request should be made. @@ -612,11 +621,252 @@ TEST_F(FencedFrameReporterTest, SendFledgeReportsBeforeMapsReceivedWithErrors) { blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), network::AttributionReportingRuntimeFeatures(), error_message, console_message_level)); + reporter->OnUrlMappingReady( + blink::FencedFrame::ReportingDestination::kBuyer, + /*reporting_url_map=*/{}, + /*reporting_ad_macro_map=*/FencedFrameReporter::ReportingMacroMap()); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); + ValidateRequest((*test_url_loader_factory_.pending_requests())[0].request, + report_destination_, absl::nullopt); +} + +// Test that absence of an allowlist disables custom destination URL reports. +TEST_F(FencedFrameReporterTest, CustomDestinationURLNoAllowlist) { + scoped_refptr reporter = + FencedFrameReporter::CreateForFledge( + shared_url_loader_factory(), browser_context(), + /*direct_seller_is_seller=*/false, &private_aggregation_manager_, + main_frame_origin_, + /*winner_origin=*/report_destination_origin_, + /*allowed_reporting_origins=*/absl::nullopt); + + // Receive buyer mapping. + reporter->OnUrlMappingReady( + blink::FencedFrame::ReportingDestination::kBuyer, + /*reporting_url_map=*/{{}}, + /*reporting_ad_macro_map=*/FencedFrameReporter::ReportingMacroMap()); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); + + std::string error_message; + blink::mojom::ConsoleMessageLevel console_message_level = + blink::mojom::ConsoleMessageLevel::kError; + EXPECT_FALSE(reporter->SendReport( + DestinationURLEvent(report_destination_), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); + EXPECT_EQ(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."); + EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kError); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); +} + +// Test that absence of an ad macro map disables custom destination URL reports. +TEST_F(FencedFrameReporterTest, CustomDestinationURLNoAdMacroMap) { + scoped_refptr reporter = + FencedFrameReporter::CreateForFledge( + shared_url_loader_factory(), browser_context(), + /*direct_seller_is_seller=*/false, &private_aggregation_manager_, + main_frame_origin_, + /*winner_origin=*/report_destination_origin_, + /*allowed_reporting_origins=*/{}); + + // Receive buyer mapping. reporter->OnUrlMappingReady(blink::FencedFrame::ReportingDestination::kBuyer, - /*reporting_url_map=*/{}); + /*reporting_url_map=*/{{}}, + /*reporting_ad_macro_map=*/absl::nullopt); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); + + std::string error_message; + blink::mojom::ConsoleMessageLevel console_message_level = + blink::mojom::ConsoleMessageLevel::kError; + EXPECT_FALSE(reporter->SendReport( + DestinationURLEvent(report_destination_), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); + EXPECT_EQ(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."); + EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kError); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); +} + +// Test macro substitution for reports to custom destination URLs, where all +// macros are defined. +TEST_F(FencedFrameReporterTest, CustomDestinationURLCompleteMacroSubstitution) { + scoped_refptr reporter = + FencedFrameReporter::CreateForFledge( + shared_url_loader_factory(), browser_context(), + /*direct_seller_is_seller=*/false, &private_aggregation_manager_, + main_frame_origin_, + /*winner_origin=*/report_destination_origin_, + /*allowed_reporting_origins=*/ + {{report_destination_origin_}}); + + // Receive buyer mapping. + reporter->OnUrlMappingReady(blink::FencedFrame::ReportingDestination::kBuyer, + /*reporting_url_map=*/{{}}, + /*reporting_ad_macro_map=*/ + FencedFrameReporter::ReportingMacroMap( + {{"FOO", "foosub"}, {"BAR", "barsub"}})); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); + + GURL report_destination_template{ + "https://report_destination.test?foo=${FOO}&bar=${BAR}&foo2=${FOO}"}; + GURL report_destination_substituted{ + "https://report_destination.test?foo=foosub&bar=barsub&foo2=foosub"}; + + // Send a request from an allowed origin. (It should succeed.) + std::string error_message; + blink::mojom::ConsoleMessageLevel console_message_level = + blink::mojom::ConsoleMessageLevel::kError; + EXPECT_TRUE(reporter->SendReport( + DestinationURLEvent(report_destination_template), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); + ValidateRequest((*test_url_loader_factory_.pending_requests())[0].request, + report_destination_substituted, absl::nullopt); +} + +// Test macro substitution for reports to custom destination URLs, where only +// some macros are defined. Also test that macros are not substituted +// recursively. +TEST_F(FencedFrameReporterTest, CustomDestinationURLPartialMacroSubstitution) { + scoped_refptr reporter = + FencedFrameReporter::CreateForFledge( + shared_url_loader_factory(), browser_context(), + /*direct_seller_is_seller=*/false, &private_aggregation_manager_, + main_frame_origin_, + /*winner_origin=*/report_destination_origin_, + /*allowed_reporting_origins=*/ + {{report_destination_origin_}}); + + // Receive buyer mapping. + reporter->OnUrlMappingReady( + blink::FencedFrame::ReportingDestination::kBuyer, + /*reporting_url_map=*/{{}}, + /*reporting_ad_macro_map=*/ + FencedFrameReporter::ReportingMacroMap({{"FOO", "${FOO}${FOO}"}})); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); + + GURL report_destination_template{ + "https://report_destination.test?foo=${FOO}&bar=${BAR}&foo2=${FOO}"}; + GURL report_destination_substituted{ + "https://" + "report_destination.test?foo=${FOO}${FOO}&bar=${BAR}&foo2=${FOO}${FOO}"}; + + // Send a request from an allowed origin. (It should succeed.) + std::string error_message; + blink::mojom::ConsoleMessageLevel console_message_level = + blink::mojom::ConsoleMessageLevel::kError; + EXPECT_TRUE(reporter->SendReport( + DestinationURLEvent(report_destination_template), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); + ValidateRequest((*test_url_loader_factory_.pending_requests())[0].request, + report_destination_substituted, absl::nullopt); +} + +// Test allowlist for reports to custom destination URLs. +TEST_F(FencedFrameReporterTest, CustomDestinationURLAllowlist) { + scoped_refptr reporter = + FencedFrameReporter::CreateForFledge( + shared_url_loader_factory(), browser_context(), + /*direct_seller_is_seller=*/false, &private_aggregation_manager_, + main_frame_origin_, + /*winner_origin=*/report_destination_origin_, + /*allowed_reporting_origins=*/ + {{report_destination_origin_, report_destination2_origin_}}); + + // Receive buyer mapping. + reporter->OnUrlMappingReady( + blink::FencedFrame::ReportingDestination::kBuyer, + /*reporting_url_map=*/{{"event_type", report_destination_}}, + /*reporting_ad_macro_map=*/FencedFrameReporter::ReportingMacroMap()); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 0); + + // Send a request to an allowed origin. (It should succeed.) + std::string error_message; + blink::mojom::ConsoleMessageLevel console_message_level = + blink::mojom::ConsoleMessageLevel::kError; + EXPECT_TRUE(reporter->SendReport( + DestinationURLEvent(report_destination_), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); EXPECT_EQ(test_url_loader_factory_.NumPending(), 1); ValidateRequest((*test_url_loader_factory_.pending_requests())[0].request, report_destination_, absl::nullopt); + + // Send a request to a different allowed origin. (It should succeed.) + EXPECT_TRUE(reporter->SendReport( + DestinationURLEvent(report_destination2_), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 2); + ValidateRequest((*test_url_loader_factory_.pending_requests())[1].request, + report_destination2_, absl::nullopt); + + // Send a request to a disallowed origin. (It should fail, and disable + // future custom destination URL requests..) + EXPECT_FALSE(reporter->SendReport( + DestinationURLEvent(report_destination3_), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); + EXPECT_EQ( + 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."); + EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kError); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 2); + + // Send a custom URL request to an allowed origin again. (It should still + // fail.) + EXPECT_FALSE(reporter->SendReport( + DestinationURLEvent(report_destination_), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); + EXPECT_EQ( + 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."); + EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kError); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 2); + + // Send a regular report to an allowed origin. (It should succeed, because + // the allowlist is only for custom URL reports.) + EXPECT_TRUE(reporter->SendReport( + DestinationEnumEvent("event_type", "event_data"), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 3); + ValidateRequest((*test_url_loader_factory_.pending_requests())[2].request, + report_destination_, "event_data"); + + // Send a regular report to a disallowed origin. (It should succeed, because + // the allowlist is only for custom URL reports.) + NavigateAndCommit(report_destination2_); + EXPECT_TRUE(reporter->SendReport( + DestinationEnumEvent("event_type", "event_data"), + blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(), + network::AttributionReportingRuntimeFeatures(), error_message, + console_message_level)); + EXPECT_EQ(test_url_loader_factory_.NumPending(), 4); } // Test reports in the FLEDGE case, where reporting URL map is never received. diff --git a/content/browser/fenced_frame/fenced_frame_url_mapping.cc b/content/browser/fenced_frame/fenced_frame_url_mapping.cc index edaab2795e795f..2fd9d2d67a28ab 100644 --- a/content/browser/fenced_frame/fenced_frame_url_mapping.cc +++ b/content/browser/fenced_frame/fenced_frame_url_mapping.cc @@ -26,38 +26,6 @@ namespace content { namespace { -// Returns a new string based on input where the matching substrings have been -// replaced with the corresponding substitutions. This function avoids repeated -// string operations by building the output based on all substitutions, one -// substitution at a time. This effectively performs all substitutions -// simultaneously, with the earliest match in the input taking precedence. -std::string SubstituteMappedStrings( - const std::string& input, - const std::vector>& substitutions) { - std::vector output_vec; - size_t input_idx = 0; - while (input_idx < input.size()) { - size_t replace_idx = input.size(); - size_t replace_end_idx = input.size(); - std::pair const* next_replacement = nullptr; - for (const auto& substitution : substitutions) { - size_t found_idx = input.find(substitution.first, input_idx); - if (found_idx < replace_idx) { - replace_idx = found_idx; - replace_end_idx = found_idx + substitution.first.size(); - next_replacement = &substitution; - } - } - output_vec.push_back(input.substr(input_idx, replace_idx - input_idx)); - if (replace_idx < input.size()) { - output_vec.push_back(next_replacement->second); - } - // move input index to after what we replaced (or end of string). - input_idx = replace_end_idx; - } - return base::StrCat(output_vec); -} - int AdSizeToPixels(double size, blink::AdSize::LengthUnit unit) { switch (unit) { case blink::AdSize::LengthUnit::kPixels: diff --git a/content/public/test/fenced_frame_test_util.cc b/content/public/test/fenced_frame_test_util.cc index fad28f6833f278..c521f580215ae2 100644 --- a/content/public/test/fenced_frame_test_util.cc +++ b/content/public/test/fenced_frame_test_util.cc @@ -145,7 +145,7 @@ void FencedFrameTestHelper::NavigateFencedFrameUsingFledge( name: 'testAd1', owner: page_origin, biddingLogicUrl: bidding_url, - ads: [{renderURL: $1, bid: 1}], + ads: [{renderURL: $1, bid: 1, allowedReportingOrigins: [$1]}], }; // Pick an arbitrarily high duration to guarantee that we never leave the