diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group_test.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/modules/ad_auction/validate_blink_interest_group_test.cc | 392 |
1 files changed, 392 insertions, 0 deletions
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 |