diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/modules/ad_auction')
10 files changed, 642 insertions, 39 deletions
diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/BUILD.gn b/chromium/third_party/blink/renderer/modules/ad_auction/BUILD.gn index 820b2f7e2c9..22f7b5cbfa4 100644 --- a/chromium/third_party/blink/renderer/modules/ad_auction/BUILD.gn +++ b/chromium/third_party/blink/renderer/modules/ad_auction/BUILD.gn @@ -8,5 +8,21 @@ blink_modules_sources("ad_auction") { sources = [ "navigator_auction.cc", "navigator_auction.h", + "validate_blink_interest_group.cc", + "validate_blink_interest_group.h", + ] +} + +source_set("unit_tests") { + testonly = true + sources = [ "validate_blink_interest_group_test.cc" ] + + deps = [ + "//base", + "//testing/gtest:gtest", + "//third_party/blink/public:test_headers", + "//third_party/blink/public/common:headers", + "//third_party/blink/renderer/modules:modules", + "//url", ] } diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/DEPS b/chromium/third_party/blink/renderer/modules/ad_auction/DEPS new file mode 100644 index 00000000000..f6bde18f96f --- /dev/null +++ b/chromium/third_party/blink/renderer/modules/ad_auction/DEPS @@ -0,0 +1,11 @@ +include_rules = [ + "+url/url_constants.h", +] + +specific_include_rules = { + "validate_blink_interest_group_test.cc": [ + "+base", + "+url/gurl.h", + "+url/origin.h", + ], +} diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/DIR_METADATA b/chromium/third_party/blink/renderer/modules/ad_auction/DIR_METADATA new file mode 100644 index 00000000000..5313345b4ca --- /dev/null +++ b/chromium/third_party/blink/renderer/modules/ad_auction/DIR_METADATA @@ -0,0 +1,3 @@ +monorail { + component: "Blink>InterestGroups" +} diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/idls.gni b/chromium/third_party/blink/renderer/modules/ad_auction/idls.gni deleted file mode 100644 index 3ed27eec0c2..00000000000 --- a/chromium/third_party/blink/renderer/modules/ad_auction/idls.gni +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright 2021 The Chromium Authors. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -modules_dependency_idl_files = [ "navigator_auction.idl" ] - -modules_dictionary_idl_files = [ - "auction_ad_config.idl", - "auction_ad_interest_group.idl", - "auction_ad.idl", -] diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc b/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc index 0fba4da4ea8..956a70bb352 100644 --- a/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc +++ b/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.cc @@ -4,11 +4,16 @@ #include "third_party/blink/renderer/modules/ad_auction/navigator_auction.h" +#include <utility> + #include "third_party/blink/public/common/browser_interface_broker_proxy.h" +#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom-blink.h" #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" +#include "third_party/blink/renderer/bindings/core/v8/v8_union_usvstring_usvstringsequence.h" #include "third_party/blink/renderer/bindings/modules/v8/v8_auction_ad.h" #include "third_party/blink/renderer/bindings/modules/v8/v8_auction_ad_config.h" #include "third_party/blink/renderer/bindings/modules/v8/v8_auction_ad_interest_group.h" +#include "third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/weborigin/security_origin_hash.h" @@ -90,7 +95,8 @@ scoped_refptr<const SecurityOrigin> ParseOrigin(const String& origin_string) { // joinAdInterestGroup() copy functions. -bool CopyOwnerFromIdlToMojo(ExceptionState& exception_state, +bool CopyOwnerFromIdlToMojo(const ExecutionContext& execution_context, + ExceptionState& exception_state, const AuctionAdInterestGroup& input, mojom::blink::InterestGroup& output) { scoped_refptr<const SecurityOrigin> owner = ParseOrigin(input.owner()); @@ -101,6 +107,16 @@ bool CopyOwnerFromIdlToMojo(ExceptionState& exception_state, input.owner().Utf8().c_str(), input.name().Utf8().c_str())); return false; } + + if (!execution_context.GetSecurityOrigin()->IsSameOriginWith(owner.get())) { + exception_state.ThrowTypeError(String::Format( + "owner '%s' for AuctionAdInterestGroup with name '%s' match frame " + "origin '%s'.", + input.owner().Utf8().c_str(), input.name().Utf8().c_str(), + owner->ToString().Utf8().c_str())); + return false; + } + output.owner = std::move(owner); return true; } @@ -256,31 +272,37 @@ bool CopyInterestGroupBuyersFromIdlToMojo( if (!input.hasInterestGroupBuyers()) return true; output.interest_group_buyers = mojom::blink::InterestGroupBuyers::New(); - if (input.interestGroupBuyers().IsUSVString()) { - String maybe_wildcard = input.interestGroupBuyers().GetAsUSVString(); - if (maybe_wildcard != "*") { - exception_state.ThrowTypeError(ErrorInvalidAuctionConfig( - input, "interestGroupBuyers", maybe_wildcard, - "must be \"*\" (wildcard) or a list of buyer https origin strings.")); - return false; - } - output.interest_group_buyers->set_all_buyers( - mojom::blink::AllBuyers::New()); - } else { - DCHECK(input.interestGroupBuyers().IsUSVStringSequence()); - Vector<scoped_refptr<const SecurityOrigin>> buyers; - for (const auto& buyer_str : - input.interestGroupBuyers().GetAsUSVStringSequence()) { - scoped_refptr<const SecurityOrigin> buyer = ParseOrigin(buyer_str); - if (!buyer) { + switch (input.interestGroupBuyers()->GetContentType()) { + case V8UnionUSVStringOrUSVStringSequence::ContentType::kUSVString: { + const String& maybe_wildcard = + input.interestGroupBuyers()->GetAsUSVString(); + if (maybe_wildcard != "*") { exception_state.ThrowTypeError(ErrorInvalidAuctionConfig( - input, "interestGroupBuyers buyer", buyer_str, - "must be a valid https origin.")); + input, "interestGroupBuyers", maybe_wildcard, + "must be \"*\" (wildcard) or a list of buyer https origin " + "strings.")); return false; } - buyers.push_back(buyer); + output.interest_group_buyers->set_all_buyers( + mojom::blink::AllBuyers::New()); + break; + } + case V8UnionUSVStringOrUSVStringSequence::ContentType::kUSVStringSequence: { + Vector<scoped_refptr<const SecurityOrigin>> buyers; + for (const auto& buyer_str : + input.interestGroupBuyers()->GetAsUSVStringSequence()) { + scoped_refptr<const SecurityOrigin> buyer = ParseOrigin(buyer_str); + if (!buyer) { + exception_state.ThrowTypeError(ErrorInvalidAuctionConfig( + input, "interestGroupBuyers buyer", buyer_str, + "must be a valid https origin.")); + return false; + } + buyers.push_back(buyer); + } + output.interest_group_buyers->set_buyers(std::move(buyers)); + break; } - output.interest_group_buyers->set_buyers(std::move(buyers)); } return true; @@ -384,26 +406,42 @@ void NavigatorAuction::joinAdInterestGroup(ScriptState* script_state, auto mojo_group = mojom::blink::InterestGroup::New(); mojo_group->expiry = base::Time::Now() + base::TimeDelta::FromSecondsD(duration_seconds); - if (!CopyOwnerFromIdlToMojo(exception_state, *group, *mojo_group)) + if (!CopyOwnerFromIdlToMojo(*context, exception_state, *group, *mojo_group)) return; mojo_group->name = group->name(); if (!CopyBiddingLogicUrlFromIdlToMojo(*context, exception_state, *group, - *mojo_group)) + *mojo_group)) { return; + } if (!CopyDailyUpdateUrlFromIdlToMojo(*context, exception_state, *group, - *mojo_group)) + *mojo_group)) { return; + } if (!CopyTrustedBiddingSignalsUrlFromIdlToMojo(*context, exception_state, - *group, *mojo_group)) + *group, *mojo_group)) { return; + } if (!CopyTrustedBiddingSignalsKeysFromIdlToMojo(*group, *mojo_group)) return; if (!CopyUserBiddingSignalsFromIdlToMojo(*script_state, exception_state, - *group, *mojo_group)) + *group, *mojo_group)) { return; + } if (!CopyAdsFromIdlToMojo(*context, *script_state, exception_state, *group, - *mojo_group)) + *mojo_group)) { return; + } + + String error_field_name; + String error_field_value; + String error; + if (!ValidateBlinkInterestGroup( + *mojo_group, error_field_name, error_field_value, error)) { + exception_state.ThrowTypeError(ErrorInvalidInterestGroup( + *group, error_field_name, error_field_value, error)); + return; + } + interest_group_store_->JoinInterestGroup(std::move(mojo_group)); } @@ -441,6 +479,17 @@ void NavigatorAuction::leaveAdInterestGroup(ScriptState* script_state, .leaveAdInterestGroup(script_state, group, exception_state); } +void NavigatorAuction::updateAdInterestGroups() { + interest_group_store_->UpdateAdInterestGroups(); +} + +/* static */ +void NavigatorAuction::updateAdInterestGroups(ScriptState* script_state, + Navigator& navigator) { + return From(ExecutionContext::From(script_state), navigator) + .updateAdInterestGroups(); +} + ScriptPromise NavigatorAuction::runAdAuction(ScriptState* script_state, const AuctionAdConfig* config, ExceptionState& exception_state) { diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.h b/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.h index f58a2070d3f..ee8d74ddb6e 100644 --- a/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.h +++ b/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.h @@ -47,6 +47,8 @@ class MODULES_EXPORT NavigatorAuction final Navigator&, const AuctionAdInterestGroup*, ExceptionState&); + void updateAdInterestGroups(); + static void updateAdInterestGroups(ScriptState*, Navigator&); ScriptPromise runAdAuction(ScriptState*, const AuctionAdConfig*, ExceptionState&); diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.idl b/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.idl index 44f95cbb45e..4d7941c6242 100644 --- a/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.idl +++ b/chromium/third_party/blink/renderer/modules/ad_auction/navigator_auction.idl @@ -16,6 +16,9 @@ [CallWith=ScriptState, Measure, RaisesException] void leaveAdInterestGroup(AuctionAdInterestGroup group); + [CallWith=ScriptState, Measure] + void updateAdInterestGroups(); + [CallWith=ScriptState, Measure, RaisesException] Promise<USVString?> runAdAuction(AuctionAdConfig config); }; diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.cc b/chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.cc new file mode 100644 index 00000000000..6ff2dbe16d3 --- /dev/null +++ b/chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.cc @@ -0,0 +1,104 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.h" + +#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom-blink.h" +#include "third_party/blink/renderer/platform/weborigin/kurl.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" +#include "third_party/blink/renderer/platform/wtf/vector.h" +#include "url/url_constants.h" + +namespace blink { + +namespace { + +// Check if `url` can be used as an interest group's ad render URL. Ad URLs can +// be cross origin, unlike other interest group URLs, but are still restricted +// to HTTPS with no embedded credentials. +bool IsUrlAllowedForRenderUrls(const KURL& url) { + if (!url.IsValid() || !url.ProtocolIs(url::kHttpsScheme)) + return false; + + return url.User().IsEmpty() && url.Pass().IsEmpty(); +} + +// Check if `url` can be used with the specified interest group for any of +// script URL, update URL, or realtime data URL. Ad render URLs should be +// checked with IsUrlAllowedForRenderUrls(), which doesn't have the same-origin +// check, and allows references. +bool IsUrlAllowed(const KURL& url, const mojom::blink::InterestGroup& group) { + if (!group.owner->IsSameOriginWith(SecurityOrigin::Create(url).get())) + return false; + + return IsUrlAllowedForRenderUrls(url) && !url.HasFragmentIdentifier(); +} + +} // namespace + +// The logic in this method must be kept in sync with InterestGroup::IsValid() +// in blink/common/interest_group/. +bool ValidateBlinkInterestGroup(const mojom::blink::InterestGroup& group, + String& error_field_name, + String& error_field_value, + String& error) { + if (group.owner->Protocol() != url::kHttpsScheme) { + error_field_name = String::FromUTF8("owner"); + error_field_value = group.owner->ToString(); + error = String::FromUTF8("owner origin must be HTTPS."); + return false; + } + + if (group.bidding_url && !IsUrlAllowed(*group.bidding_url, group)) { + error_field_name = String::FromUTF8("biddingUrl"); + error_field_value = group.bidding_url->GetString(); + error = String::FromUTF8( + "biddingUrl must have the same origin as the InterestGroup owner " + "and have no fragment identifier or embedded credentials."); + return false; + } + + if (group.update_url && !IsUrlAllowed(*group.update_url, group)) { + error_field_name = String::FromUTF8("updateUrl"); + error_field_value = group.update_url->GetString(); + error = String::FromUTF8( + "updateUrl must have the same origin as the InterestGroup owner " + "and have no fragment identifier or embedded credentials."); + return false; + } + + if (group.trusted_bidding_signals_url) { + // In addition to passing the same checks used on the other URLs, + // `trusted_bidding_signals_url` must not have a query string, since the + // query parameter needs to be set as part of running an auction. + if (!IsUrlAllowed(*group.trusted_bidding_signals_url, group) || + !group.trusted_bidding_signals_url->Query().IsEmpty()) { + error_field_name = String::FromUTF8("trustedBiddingSignalsUrl"); + error_field_value = group.trusted_bidding_signals_url->GetString(); + error = String::FromUTF8( + "trustedBiddingSignalsUrl must have the same origin as the " + "InterestGroup owner and have no query string, fragment identifier " + "or embedded credentials."); + return false; + } + } + + if (group.ads) { + for (WTF::wtf_size_t i = 0; i < group.ads.value().size(); ++i) { + const KURL& render_url = group.ads.value()[i]->render_url; + if (!IsUrlAllowedForRenderUrls(render_url)) { + error_field_name = String::Format("ad[%u].renderUrl", i); + error_field_value = render_url.GetString(); + error = String::FromUTF8( + "renderUrls must be HTTPS and have no embedded credentials."); + return false; + } + } + } + + return true; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.h b/chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.h new file mode 100644 index 00000000000..932c18715d8 --- /dev/null +++ b/chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.h @@ -0,0 +1,34 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_AD_AUCTION_VALIDATE_BLINK_INTEREST_GROUP_H_ +#define THIRD_PARTY_BLINK_RENDERER_MODULES_AD_AUCTION_VALIDATE_BLINK_INTEREST_GROUP_H_ + +#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom-blink-forward.h" +#include "third_party/blink/renderer/modules/modules_export.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" + +namespace blink { + +// Checks that the specified mojom::blink::InterestGroup is valid. Invalid +// interest groups contain auction or update URLs cross-origin to the owner, or +// URLs that contain disallowed components (e.g., user/pass). When it returns +// false, writes information about the error to `error_field_name`, +// `error_field_value`, and `error`. +// +// Checks all provided URLs. Does no validation of expiration time. Does no +// validation of values expected to be in JSON, since ValidateInterestGroup() +// does not validate JSON. Must be kept in sync with ValidateInterestGroup(), +// which performs the exact same logic, except on mojom::InterestGroups, and is +// used to validate InterestGroups received from a less trusted renderer +// process. +MODULES_EXPORT bool ValidateBlinkInterestGroup( + const mojom::blink::InterestGroup& group, + String& error_field_name, + String& error_field_value, + String& error); + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_AD_AUCTION_VALIDATE_BLINK_INTEREST_GROUP_H_ diff --git a/chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group_test.cc b/chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group_test.cc new file mode 100644 index 00000000000..ce14a446729 --- /dev/null +++ b/chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group_test.cc @@ -0,0 +1,392 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group.h" + +#include "base/memory/scoped_refptr.h" +#include "base/strings/string_piece.h" +#include "mojo/public/cpp/bindings/array_traits_wtf_vector.h" +#include "mojo/public/cpp/bindings/message.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/public/common/interest_group/interest_group.h" +#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom-blink.h" +#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h" +#include "third_party/blink/renderer/platform/bindings/exception_state.h" +#include "third_party/blink/renderer/platform/weborigin/kurl.h" +#include "third_party/blink/renderer/platform/weborigin/security_origin.h" +#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace blink { + +namespace { + +constexpr char kOriginString[] = "https://origin.test/"; +constexpr char kNameString[] = "name"; + +} // namespace + +// Test fixture for testing both ValidateBlinkInterestGroup() and +// ValidateInterestGroup(), and making sure they behave the same. +class ValidateBlinkInterestGroupTest : public testing::Test { + public: + // Check that `blink_interest_group` is valid, if added from its owner origin. + void ExpectInterestGroupIsValid( + const mojom::blink::InterestGroupPtr& blink_interest_group) { + String error_field_name; + String error_field_value; + String error; + EXPECT_TRUE(ValidateBlinkInterestGroup( + *blink_interest_group, error_field_name, error_field_value, error)); + EXPECT_TRUE(error_field_name.IsNull()); + EXPECT_TRUE(error_field_value.IsNull()); + EXPECT_TRUE(error.IsNull()); + + EXPECT_TRUE(CanSerializeAndDeserialize(blink_interest_group)); + } + + // Check that `blink_interest_group` is valid, if added from `blink_origin`, + // and returns the provided error values. + void ExpectInterestGroupIsNotValid( + const mojom::blink::InterestGroupPtr& blink_interest_group, + const std::string& expected_error_field_name, + const std::string& expected_error_field_value, + const std::string& expected_error) { + String error_field_name; + String error_field_value; + String error; + EXPECT_FALSE(ValidateBlinkInterestGroup( + *blink_interest_group, error_field_name, error_field_value, error)); + EXPECT_EQ(String::FromUTF8(expected_error_field_name), error_field_name); + EXPECT_EQ(String::FromUTF8(expected_error_field_value), error_field_value); + EXPECT_EQ(String::FromUTF8(expected_error), error); + + EXPECT_FALSE(CanSerializeAndDeserialize(blink_interest_group)); + } + + // Tries to Converts a mojom::blink::InterestGroupPtr to a + // blink::InterestGroup by using Mojo to serialize and deserialize it. Returns + // true on success, false on failure. Failure indicates the traits conversion + // logic refused to serialize the InterestGroup, since it was invalid. Based + // off of mojo::test::SerializeAndDeserialize(), which can't convert between + // blink and non-blink types. + bool CanSerializeAndDeserialize( + const mojom::blink::InterestGroupPtr& blink_interest_group) { + mojo::Message message = + mojom::blink::InterestGroup::SerializeAsMessage(&blink_interest_group); + mojo::ScopedMessageHandle handle = message.TakeMojoMessage(); + message = mojo::Message::CreateFromMessageHandle(&handle); + DCHECK(!message.IsNull()); + + auto interest_group = std::make_unique<blink::InterestGroup>(); + return mojom::InterestGroup::DeserializeFromMessage(std::move(message), + interest_group.get()); + } + + // Creates and returns a minimally populated mojom::blink::InterestGroup. + mojom::blink::InterestGroupPtr CreateMinimalInterestGroup() { + mojom::blink::InterestGroupPtr blink_interest_group = + mojom::blink::InterestGroup::New(); + blink_interest_group->owner = kOrigin; + blink_interest_group->name = kName; + return blink_interest_group; + } + + // Creates an interest group with all fields populated with valid values. + mojom::blink::InterestGroupPtr CreateFullyPopulatedInterestGroup() { + mojom::blink::InterestGroupPtr blink_interest_group = + CreateMinimalInterestGroup(); + + // Url that's allowed in every field. Populate all portions of the URL that + // are allowed in most places. + const KURL kAllowedUrl = + KURL(String::FromUTF8("https://origin.test/foo?bar")); + blink_interest_group->bidding_url = kAllowedUrl; + blink_interest_group->update_url = kAllowedUrl; + + // `trusted_bidding_signals_url` doesn't allow query strings, unlike the + // above ones. + blink_interest_group->trusted_bidding_signals_url = + KURL(String::FromUTF8("https://origin.test/foo")); + + blink_interest_group->trusted_bidding_signals_keys.emplace(); + blink_interest_group->trusted_bidding_signals_keys->push_back( + String::FromUTF8("1")); + blink_interest_group->trusted_bidding_signals_keys->push_back( + String::FromUTF8("2")); + blink_interest_group->user_bidding_signals = + String::FromUTF8("\"This field isn't actually validated\""); + + // Add two ads. Use different URLs, with references. + blink_interest_group->ads.emplace(); + auto mojo_ad1 = mojom::blink::InterestGroupAd::New(); + mojo_ad1->render_url = + KURL(String::FromUTF8("https://origin.test/foo?bar#baz")); + mojo_ad1->metadata = + String::FromUTF8("\"This field isn't actually validated\""); + blink_interest_group->ads->push_back(std::move(mojo_ad1)); + auto mojo_ad2 = mojom::blink::InterestGroupAd::New(); + mojo_ad2->render_url = + KURL(String::FromUTF8("https://origin.test/foo?bar#baz2")); + blink_interest_group->ads->push_back(std::move(mojo_ad2)); + + return blink_interest_group; + } + + protected: + // SecurityOrigin used as the owner in most tests. + const scoped_refptr<const SecurityOrigin> kOrigin = + SecurityOrigin::CreateFromString(String::FromUTF8(kOriginString)); + + const String kName = String::FromUTF8(kNameString); +}; + +// Test behavior with an InterestGroup with as few fields populated as allowed. +TEST_F(ValidateBlinkInterestGroupTest, MinimallyPopulated) { + mojom::blink::InterestGroupPtr blink_interest_group = + CreateMinimalInterestGroup(); + ExpectInterestGroupIsValid(blink_interest_group); +} + +// Test behavior with an InterestGroup with all fields populated with valid +// values. +TEST_F(ValidateBlinkInterestGroupTest, FullyPopulated) { + mojom::blink::InterestGroupPtr blink_interest_group = + CreateFullyPopulatedInterestGroup(); + ExpectInterestGroupIsValid(blink_interest_group); +} + +// Make sure that non-HTTPS origins are rejected, both as the frame origin, and +// as the owner. HTTPS frame origins with non-HTTPS owners are currently +// rejected due to origin mismatch, but once sites can add users to 3P interest +// groups, they should still be rejected for being non-HTTPS. +TEST_F(ValidateBlinkInterestGroupTest, NonHttpsOriginRejected) { + mojom::blink::InterestGroupPtr blink_interest_group = + CreateMinimalInterestGroup(); + blink_interest_group->owner = + SecurityOrigin::CreateFromString(String::FromUTF8("http://origin.test/")); + ExpectInterestGroupIsNotValid( + blink_interest_group, "owner" /* expected_error_field_name */, + "http://origin.test" /* expected_error_field_value */, + "owner origin must be HTTPS." /* expected_error */); + + blink_interest_group->owner = + SecurityOrigin::CreateFromString(String::FromUTF8("data:,foo")); + // Data URLs have opaque origins, which are mapped to the string "null". + ExpectInterestGroupIsNotValid( + blink_interest_group, "owner" /* expected_error_field_name */, + "null" /* expected_error_field_value */, + "owner origin must be HTTPS." /* expected_error */); +} + +// Check that `bidding_url`, `update_url`, and `trusted_bidding_signals_url` +// must be same-origin and HTTPS. +// +// Ad URLs do not have to be same origin, so they're checked in a different +// test. +TEST_F(ValidateBlinkInterestGroupTest, RejectedUrls) { + // Strings when each field has a bad URL, copied from cc file. + const char kBadBiddingUrlError[] = + "biddingUrl must have the same origin as the InterestGroup owner " + "and have no fragment identifier or embedded credentials."; + const char kBadUpdateUrlError[] = + "updateUrl must have the same origin as the InterestGroup owner " + "and have no fragment identifier or embedded credentials."; + const char kBadTrustedBiddingSignalsUrlError[] = + "trustedBiddingSignalsUrl must have the same origin as the " + "InterestGroup owner and have no query string, fragment identifier " + "or embedded credentials."; + + // Nested URL schemes, like filesystem URLs, are the only cases where a URL + // being same origin with an HTTPS origin does not imply the URL itself is + // also HTTPS. + const KURL kFileSystemUrl = + KURL(String::FromUTF8("filesystem:https://origin.test/foo")); + EXPECT_TRUE( + kOrigin->IsSameOriginWith(SecurityOrigin::Create(kFileSystemUrl).get())); + + const KURL kRejectedUrls[] = { + // HTTP URLs is rejected: it's both the wrong scheme, and cross-origin. + KURL(String::FromUTF8("filesystem:http://origin.test/foo")), + // Cross origin HTTPS URLs are rejected. + KURL(String::FromUTF8("https://origin2.test/foo")), + // URL with different ports are cross-origin. + KURL(String::FromUTF8("https://origin.test:1234/")), + // URLs with opaque origins are cross-origin. + KURL(String::FromUTF8("data://text/html,payload")), + // Unknown scheme. + KURL(String::FromUTF8("unknown-scheme://foo/")), + + // filesystem URLs are rejected, even if they're same-origin with the page + // origin. + kFileSystemUrl, + + // URLs with user/ports are rejected. + KURL(String::FromUTF8("https://user:pass@origin.test/")), + // References also aren't allowed, as they aren't sent over HTTP. + KURL(String::FromUTF8("https://origin.test/#foopy")), + + // Invalid URLs. + KURL(String::FromUTF8("")), + KURL(String::FromUTF8("invalid url")), + KURL(String::FromUTF8("https://!@#$%^&*()/")), + KURL(String::FromUTF8("https://[1::::::2]/")), + KURL(String::FromUTF8("https://origin.test/%00")), + }; + + for (const KURL& rejected_url : kRejectedUrls) { + SCOPED_TRACE(rejected_url.GetString()); + + // Test `bidding_url`. + mojom::blink::InterestGroupPtr blink_interest_group = + CreateMinimalInterestGroup(); + blink_interest_group->bidding_url = rejected_url; + ExpectInterestGroupIsNotValid( + blink_interest_group, "biddingUrl" /* expected_error_field_name */, + rejected_url.GetString().Utf8() /* expected_error_field_value */, + kBadBiddingUrlError /* expected_error */); + + // Test `update_url`. + blink_interest_group = CreateMinimalInterestGroup(); + blink_interest_group->update_url = rejected_url; + ExpectInterestGroupIsNotValid( + blink_interest_group, "updateUrl" /* expected_error_field_name */, + rejected_url.GetString().Utf8() /* expected_error_field_value */, + // expected_error + kBadUpdateUrlError /* expected_error */); + + // Test `trusted_bidding_signals_url`. + blink_interest_group = CreateMinimalInterestGroup(); + blink_interest_group->trusted_bidding_signals_url = rejected_url; + ExpectInterestGroupIsNotValid( + blink_interest_group, + "trustedBiddingSignalsUrl" /* expected_error_field_name */, + rejected_url.GetString().Utf8() /* expected_error_field_value */, + kBadTrustedBiddingSignalsUrlError /* expected_error */); + } + + // `trusted_bidding_signals_url` also can't include query strings. + mojom::blink::InterestGroupPtr blink_interest_group = + CreateMinimalInterestGroup(); + KURL rejected_url = KURL(String::FromUTF8("https://origin.test/?query")); + blink_interest_group->trusted_bidding_signals_url = rejected_url; + ExpectInterestGroupIsNotValid( + blink_interest_group, + "trustedBiddingSignalsUrl" /* expected_error_field_name */, + rejected_url.GetString().Utf8() /* expected_error_field_value */, + kBadTrustedBiddingSignalsUrlError /* expected_error */); +} + +// Tests valid and invalid ad render URLs. +TEST_F(ValidateBlinkInterestGroupTest, AdRenderUrlValidation) { + const char kBadAdUrlError[] = + "renderUrls must be HTTPS and have no embedded credentials."; + + const struct { + bool expect_allowed; + const char* url; + } kTestCases[] = { + // Same origin URLs are allowed. + {true, "https://origin.test/foo?bar"}, + + // Cross origin URLs are allowed, as long as they're HTTPS. + {true, "https://b.test/"}, + {true, "https://a.test:1234/"}, + + // URLs with the wrong scheme are rejected. + {false, "http://a.test/"}, + {false, "data://text/html,payload"}, + {false, "filesystem:https://a.test/foo"}, + + // URLs with user/ports are rejected. + {false, "https://user:pass@a.test/"}, + + // References are allowed for ads, though not other requests, since they + // only have an effect when loading a page in a renderer. + {true, "https://a.test/#foopy"}, + }; + + for (const auto& test_case : kTestCases) { + SCOPED_TRACE(test_case.url); + + KURL test_case_url = KURL(String::FromUTF8(test_case.url)); + + // Add an InterestGroup with the test cases's URL as the only ad's URL. + mojom::blink::InterestGroupPtr blink_interest_group = + CreateMinimalInterestGroup(); + blink_interest_group->ads.emplace(); + blink_interest_group->ads->emplace_back(mojom::blink::InterestGroupAd::New( + test_case_url, String() /* metadata */)); + if (test_case.expect_allowed) { + ExpectInterestGroupIsValid(blink_interest_group); + } else { + ExpectInterestGroupIsNotValid( + blink_interest_group, + "ad[0].renderUrl" /* expected_error_field_name */, + test_case_url.GetString().Utf8() /* expected_error_field_value */, + kBadAdUrlError /* expected_error */); + } + + // Add an InterestGroup with the test cases's URL as the second ad's URL. + blink_interest_group = CreateMinimalInterestGroup(); + blink_interest_group->ads.emplace(); + blink_interest_group->ads->emplace_back(mojom::blink::InterestGroupAd::New( + KURL(String::FromUTF8("https://origin.test/")), + String() /* metadata */)); + blink_interest_group->ads->emplace_back(mojom::blink::InterestGroupAd::New( + test_case_url, String() /* metadata */)); + if (test_case.expect_allowed) { + ExpectInterestGroupIsValid(blink_interest_group); + } else { + ExpectInterestGroupIsNotValid( + blink_interest_group, + "ad[1].renderUrl" /* expected_error_field_name */, + test_case_url.GetString().Utf8() /* expected_error_field_value */, + kBadAdUrlError /* expected_error */); + } + } +} + +// Mojo rejects malformed URLs when converting mojom::blink::InterestGroup to +// blink::InterestGroup. Since the rejection happens internally in Mojo, +// typemapping code that invokes blink::InterestGroup::IsValid() isn't run, so +// adding a AdRenderUrlValidation testcase to verify malformed URLs wouldn't +// exercise blink::InterestGroup::IsValid(). Since blink::InterestGroup users +// can call IsValid() directly (i.e when not using Mojo), we need a test that +// also calls IsValid() directly. +TEST_F(ValidateBlinkInterestGroupTest, MalformedUrl) { + constexpr char kMalformedUrl[] = "https://invalid^"; + + // First, check against mojom::blink::InterestGroup. + constexpr char kBadAdUrlError[] = + "renderUrls must be HTTPS and have no embedded credentials."; + mojom::blink::InterestGroupPtr blink_interest_group = + mojom::blink::InterestGroup::New(); + blink_interest_group->owner = kOrigin; + blink_interest_group->name = kName; + blink_interest_group->ads.emplace(); + blink_interest_group->ads->emplace_back(mojom::blink::InterestGroupAd::New( + KURL(kMalformedUrl), String() /* metadata */)); + String error_field_name; + String error_field_value; + String error; + EXPECT_FALSE(ValidateBlinkInterestGroup( + *blink_interest_group, error_field_name, error_field_value, error)); + EXPECT_EQ(error_field_name, String::FromUTF8("ad[0].renderUrl")); + // The invalid ^ gets escaped. + EXPECT_EQ(error_field_value, String::FromUTF8("https://invalid%5E/")); + EXPECT_EQ(error, String::FromUTF8(kBadAdUrlError)); + + // Now, test against blink::InterestGroup. + blink::InterestGroup interest_group; + interest_group.owner = url::Origin::Create(GURL(kOriginString)); + interest_group.name = kNameString; + interest_group.ads.emplace(); + interest_group.ads->emplace_back( + blink::InterestGroup::Ad(GURL(kMalformedUrl), /*metadata=*/"")); + EXPECT_FALSE(interest_group.IsValid()); +} + +} // namespace blink |