| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <string> |
| #include <tuple> |
| |
| #include "base/test/scoped_feature_list.h" |
| #include "net/base/features.h" |
| #include "net/cookies/cookie_constants.h" |
| #include "net/cookies/cookie_partition_key.h" |
| #include "net/cookies/site_for_cookies.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace net { |
| |
| using enum CookiePartitionKey::AncestorChainBit; |
| |
| class CookiePartitionKeyTest : public testing::TestWithParam<bool> { |
| protected: |
| // testing::Test |
| void SetUp() override { |
| scoped_feature_list_.InitWithFeatureState( |
| features::kAncestorChainBitEnabledInPartitionedCookies, |
| AncestorChainBitEnabled()); |
| } |
| |
| bool AncestorChainBitEnabled() { return GetParam(); } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(/* no label */, |
| CookiePartitionKeyTest, |
| ::testing::Bool()); |
| |
| TEST_P(CookiePartitionKeyTest, TestFromStorage) { |
| struct { |
| const std::string top_level_site; |
| bool third_party; |
| const std::optional<CookiePartitionKey> expected_output; |
| } cases[] = { |
| {/*empty site*/ |
| "", true, CookiePartitionKey::FromURLForTesting(GURL(""))}, |
| /*invalid site*/ |
| {"Invalid", true, std::nullopt}, |
| /*malformed site*/ |
| {"https://toplevelsite.com/", true, std::nullopt}, |
| /*valid site: cross site*/ |
| {"https://toplevelsite.com", true, |
| CookiePartitionKey::FromURLForTesting(GURL("https://toplevelsite.com"))}, |
| /*valid site: same site*/ |
| {"https://toplevelsite.com", false, |
| CookiePartitionKey::FromURLForTesting(GURL("https://toplevelsite.com"), |
| kSameSite)}}; |
| for (const auto& tc : cases) { |
| base::expected<std::optional<CookiePartitionKey>, std::string> got = |
| CookiePartitionKey::FromStorage(tc.top_level_site, tc.third_party); |
| EXPECT_EQ(got.has_value(), tc.expected_output.has_value()); |
| if (!tc.top_level_site.empty() && tc.expected_output.has_value()) { |
| ASSERT_TRUE(got.has_value()) << "Expected result to have value."; |
| EXPECT_EQ(got.value()->IsThirdParty(), tc.third_party); |
| } |
| } |
| } |
| |
| TEST_P(CookiePartitionKeyTest, TestFromUntrustedInput) { |
| const std::string kFullURL = "https://subdomain.toplevelsite.com/index.html"; |
| const std::string kValidSite = "https://toplevelsite.com"; |
| struct Output { |
| bool third_party; |
| }; |
| struct { |
| std::string top_level_site; |
| CookiePartitionKey::AncestorChainBit has_cross_site_ancestor; |
| std::optional<Output> expected_output; |
| } cases[] = { |
| {/*empty site*/ |
| "", kCrossSite, std::nullopt}, |
| {/*empty site : same site ancestor*/ |
| "", kSameSite, std::nullopt}, |
| {/*valid site*/ |
| kValidSite, kCrossSite, Output{true}}, |
| {/*valid site: same site ancestor*/ |
| kValidSite, kSameSite, Output{false}}, |
| {/*valid site with extra slash: same site ancestor*/ |
| kValidSite + "/", kSameSite, Output{false}}, |
| {/*invalid site (missing scheme)*/ |
| "toplevelsite.com", kCrossSite, std::nullopt}, |
| {/*invalid site (missing scheme): same site ancestor*/ |
| "toplevelsite.com", kSameSite, std::nullopt}, |
| {/*invalid site*/ |
| "abc123foobar!!", kCrossSite, std::nullopt}, |
| {/*invalid site: same site ancestor*/ |
| "abc123foobar!!", kSameSite, std::nullopt}, |
| }; |
| |
| for (const auto& tc : cases) { |
| base::expected<CookiePartitionKey, std::string> got = |
| CookiePartitionKey::FromUntrustedInput( |
| tc.top_level_site, tc.has_cross_site_ancestor == kCrossSite); |
| EXPECT_EQ(got.has_value(), tc.expected_output.has_value()); |
| if (tc.expected_output.has_value()) { |
| EXPECT_EQ(got->site().Serialize(), kValidSite); |
| EXPECT_EQ(got->IsThirdParty(), tc.expected_output->third_party); |
| } |
| } |
| } |
| |
| TEST_P(CookiePartitionKeyTest, Serialization) { |
| base::UnguessableToken nonce = base::UnguessableToken::Create(); |
| struct Output { |
| std::string top_level_site; |
| bool cross_site; |
| }; |
| struct { |
| std::optional<CookiePartitionKey> input; |
| std::optional<Output> expected_output; |
| } cases[] = { |
| // No partition key |
| {std::nullopt, Output{kEmptyCookiePartitionKey, true}}, |
| // Partition key present |
| {CookiePartitionKey::FromURLForTesting(GURL("https://toplevelsite.com")), |
| Output{"https://toplevelsite.com", true}}, |
| // Local file URL |
| {CookiePartitionKey::FromURLForTesting(GURL("file:///path/to/file.txt")), |
| Output{"file://", true}}, |
| // File URL with host |
| {CookiePartitionKey::FromURLForTesting( |
| GURL("file://toplevelsite.com/path/to/file.pdf")), |
| Output{"file://toplevelsite.com", true}}, |
| // Opaque origin |
| {CookiePartitionKey::FromURLForTesting(GURL()), std::nullopt}, |
| // AncestorChain::kSameSite |
| {CookiePartitionKey::FromURLForTesting(GURL("https://toplevelsite.com"), |
| kSameSite, std::nullopt), |
| Output{"https://toplevelsite.com", false}}, |
| // AncestorChain::kCrossSite |
| {CookiePartitionKey::FromURLForTesting(GURL("https://toplevelsite.com"), |
| kCrossSite, std::nullopt), |
| Output{"https://toplevelsite.com", true}}, |
| // With nonce |
| {CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(SchemefulSite(GURL("https://toplevelsite.com")), |
| SchemefulSite(GURL("https://cookiesite.com")), |
| nonce), |
| SiteForCookies::FromUrl(GURL::EmptyGURL()), |
| SchemefulSite(GURL("https://toplevelsite.com"))), |
| std::nullopt}, |
| // Same site no nonce from NIK |
| {CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(SchemefulSite(GURL("https://toplevelsite.com")), |
| SchemefulSite(GURL("https://toplevelsite.com"))), |
| SiteForCookies::FromUrl(GURL("https://toplevelsite.com")), |
| SchemefulSite(GURL("https://toplevelsite.com"))), |
| Output{"https://toplevelsite.com", false}}, |
| // Different request_site results in cross site ancestor |
| {CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(SchemefulSite(GURL("https://toplevelsite.com")), |
| SchemefulSite(GURL("https://toplevelsite.com"))), |
| SiteForCookies::FromUrl(GURL("https://toplevelsite.com")), |
| SchemefulSite(GURL("https://differentOrigin.com"))), |
| Output{"https://toplevelsite.com", true}}, |
| // Same site with nonce from NIK |
| {CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(SchemefulSite(GURL("https://toplevelsite.com")), |
| SchemefulSite(GURL("https://toplevelsite.com")), |
| nonce), |
| SiteForCookies::FromUrl(GURL("https://toplevelsite.com")), |
| SchemefulSite(GURL("https://toplevelsite.com"))), |
| std::nullopt}, |
| // Invalid partition key |
| {std::make_optional( |
| CookiePartitionKey::FromURLForTesting(GURL("abc123foobar!!"))), |
| std::nullopt}, |
| }; |
| |
| for (const auto& tc : cases) { |
| base::expected<CookiePartitionKey::SerializedCookiePartitionKey, |
| std::string> |
| got = CookiePartitionKey::Serialize(tc.input); |
| |
| EXPECT_EQ(tc.expected_output.has_value(), got.has_value()); |
| if (got.has_value()) { |
| // TODO (crbug.com/41486025) once ancestor chain bit is implemented update |
| // test to check bit's value. |
| EXPECT_EQ(tc.expected_output->top_level_site, got->TopLevelSite()); |
| EXPECT_EQ(tc.expected_output->cross_site, got->has_cross_site_ancestor()); |
| } |
| } |
| } |
| |
| TEST_P(CookiePartitionKeyTest, FromNetworkIsolationKey) { |
| const SchemefulSite kTopLevelSite = |
| SchemefulSite(GURL("https://toplevelsite.com")); |
| const SchemefulSite kCookieSite = |
| SchemefulSite(GURL("https://cookiesite.com")); |
| const base::UnguessableToken kNonce = base::UnguessableToken::Create(); |
| |
| struct TestCase { |
| const std::string desc; |
| const NetworkIsolationKey network_isolation_key; |
| const std::optional<CookiePartitionKey> expected; |
| const SiteForCookies site_for_cookies; |
| const SchemefulSite request_site; |
| } test_cases[] = { |
| {"Empty", NetworkIsolationKey(), std::nullopt, |
| SiteForCookies::FromUrl(GURL::EmptyGURL()), SchemefulSite(GURL(""))}, |
| {"WithTopLevelSite", NetworkIsolationKey(kTopLevelSite, kCookieSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL()), |
| SiteForCookies::FromUrl(GURL::EmptyGURL()), |
| SchemefulSite(kTopLevelSite)}, |
| {"WithNonce", NetworkIsolationKey(kTopLevelSite, kCookieSite, kNonce), |
| CookiePartitionKey::FromURLForTesting(kCookieSite.GetURL(), kCrossSite, |
| kNonce), |
| SiteForCookies::FromUrl(GURL::EmptyGURL()), |
| SchemefulSite(kTopLevelSite)}, |
| {"WithCrossSiteAncestorSameSite", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kSameSite, |
| std::nullopt), |
| SiteForCookies::FromUrl(GURL(kTopLevelSite.GetURL())), |
| SchemefulSite(kTopLevelSite)}, |
| {"Nonced first party NIK results in kCrossSite partition key", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite, kNonce), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kCrossSite, |
| kNonce), |
| SiteForCookies::FromUrl(GURL(kTopLevelSite.GetURL())), |
| SchemefulSite(kTopLevelSite)}, |
| {"WithCrossSiteAncestorNotSameSite", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kCrossSite, |
| std::nullopt), |
| SiteForCookies::FromUrl(GURL::EmptyGURL()), kCookieSite}}; |
| |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitWithFeatureState( |
| features::kAncestorChainBitEnabledInPartitionedCookies, |
| AncestorChainBitEnabled()); |
| |
| for (const auto& test_case : test_cases) { |
| SCOPED_TRACE(test_case.desc); |
| |
| std::optional<CookiePartitionKey> got = |
| CookiePartitionKey::FromNetworkIsolationKey( |
| test_case.network_isolation_key, test_case.site_for_cookies, |
| test_case.request_site); |
| |
| EXPECT_EQ(test_case.expected, got); |
| if (got) { |
| EXPECT_EQ(test_case.network_isolation_key.GetNonce(), got->nonce()); |
| } |
| } |
| } |
| |
| TEST_P(CookiePartitionKeyTest, FromWire) { |
| struct TestCase { |
| const GURL url; |
| const std::optional<base::UnguessableToken> nonce; |
| const CookiePartitionKey::AncestorChainBit ancestor_chain_bit; |
| } test_cases[] = { |
| {GURL("https://foo.com"), std::nullopt, kCrossSite}, |
| {GURL("https://foo.com"), std::nullopt, kSameSite}, |
| {GURL(), std::nullopt, kCrossSite}, |
| {GURL("https://foo.com"), base::UnguessableToken::Create(), kCrossSite}}; |
| |
| for (const auto& test_case : test_cases) { |
| auto want = CookiePartitionKey::FromURLForTesting( |
| test_case.url, test_case.ancestor_chain_bit, test_case.nonce); |
| auto got = CookiePartitionKey::FromWire( |
| want.site(), want.IsThirdParty() ? kCrossSite : kSameSite, |
| want.nonce()); |
| EXPECT_EQ(want, got); |
| EXPECT_FALSE(got.from_script()); |
| } |
| } |
| |
| TEST_P(CookiePartitionKeyTest, FromStorageKeyComponents) { |
| struct TestCase { |
| const GURL url; |
| const std::optional<base::UnguessableToken> nonce = std::nullopt; |
| const CookiePartitionKey::AncestorChainBit ancestor_chain_bit; |
| } test_cases[] = { |
| {GURL("https://foo.com"), std::nullopt, kCrossSite}, |
| {GURL("https://foo.com"), std::nullopt, kSameSite}, |
| {GURL(), std::nullopt, kCrossSite}, |
| {GURL("https://foo.com"), base::UnguessableToken::Create(), kCrossSite}}; |
| |
| for (const auto& test_case : test_cases) { |
| auto want = CookiePartitionKey::FromURLForTesting( |
| test_case.url, test_case.ancestor_chain_bit, test_case.nonce); |
| std::optional<CookiePartitionKey> got = |
| CookiePartitionKey::FromStorageKeyComponents( |
| want.site(), want.IsThirdParty() ? kCrossSite : kSameSite, |
| want.nonce()); |
| EXPECT_EQ(got, want); |
| } |
| } |
| |
| TEST_P(CookiePartitionKeyTest, FromScript) { |
| auto key = CookiePartitionKey::FromScript(); |
| EXPECT_TRUE(key); |
| EXPECT_TRUE(key->from_script()); |
| EXPECT_TRUE(key->site().opaque()); |
| EXPECT_TRUE(key->IsThirdParty()); |
| |
| auto key2 = CookiePartitionKey::FromScript(); |
| EXPECT_TRUE(key2); |
| EXPECT_TRUE(key2->from_script()); |
| EXPECT_TRUE(key2->site().opaque()); |
| EXPECT_TRUE(key2->IsThirdParty()); |
| |
| // The keys should not be equal because they get created with different opaque |
| // sites. Test both the '==' and '!=' operators here. |
| EXPECT_FALSE(key == key2); |
| EXPECT_TRUE(key != key2); |
| } |
| |
| TEST_P(CookiePartitionKeyTest, IsSerializeable) { |
| EXPECT_FALSE(CookiePartitionKey::FromURLForTesting(GURL()).IsSerializeable()); |
| EXPECT_TRUE( |
| CookiePartitionKey::FromURLForTesting(GURL("https://www.example.com")) |
| .IsSerializeable()); |
| } |
| |
| TEST_P(CookiePartitionKeyTest, Equality) { |
| // Same eTLD+1 but different scheme are not equal. |
| EXPECT_NE(CookiePartitionKey::FromURLForTesting(GURL("https://foo.com")), |
| CookiePartitionKey::FromURLForTesting(GURL("http://foo.com"))); |
| |
| // Different subdomains of the same site are equal. |
| EXPECT_EQ(CookiePartitionKey::FromURLForTesting(GURL("https://a.foo.com")), |
| CookiePartitionKey::FromURLForTesting(GURL("https://b.foo.com"))); |
| } |
| |
| TEST_P(CookiePartitionKeyTest, Equality_WithAncestorChain) { |
| CookiePartitionKey key1 = CookiePartitionKey::FromURLForTesting( |
| GURL("https://foo.com"), kSameSite, std::nullopt); |
| CookiePartitionKey key2 = CookiePartitionKey::FromURLForTesting( |
| GURL("https://foo.com"), kCrossSite, std::nullopt); |
| |
| EXPECT_EQ((key1 == key2), !AncestorChainBitEnabled()); |
| EXPECT_EQ(key1, CookiePartitionKey::FromURLForTesting( |
| GURL("https://foo.com"), kSameSite, std::nullopt)); |
| } |
| |
| TEST_P(CookiePartitionKeyTest, Equality_WithNonce) { |
| SchemefulSite top_level_site = |
| SchemefulSite(GURL("https://toplevelsite.com")); |
| SchemefulSite frame_site = SchemefulSite(GURL("https://cookiesite.com")); |
| base::UnguessableToken nonce1 = base::UnguessableToken::Create(); |
| base::UnguessableToken nonce2 = base::UnguessableToken::Create(); |
| EXPECT_NE(nonce1, nonce2); |
| auto key1 = CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(top_level_site, frame_site, nonce1), SiteForCookies(), |
| top_level_site); |
| EXPECT_TRUE(key1.has_value()); |
| |
| auto key2 = CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(top_level_site, frame_site, nonce2), SiteForCookies(), |
| top_level_site); |
| EXPECT_TRUE(key1.has_value() && key2.has_value()); |
| EXPECT_NE(key1, key2); |
| |
| auto key3 = CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(top_level_site, frame_site, nonce1), SiteForCookies(), |
| top_level_site); |
| EXPECT_EQ(key1, key3); |
| |
| auto unnonced_key = CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(top_level_site, frame_site), SiteForCookies(), |
| frame_site); |
| EXPECT_NE(key1, unnonced_key); |
| } |
| |
| TEST_P(CookiePartitionKeyTest, Localhost) { |
| SchemefulSite top_level_site(GURL("https://localhost:8000")); |
| |
| auto key = CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(top_level_site, top_level_site), SiteForCookies(), |
| top_level_site); |
| EXPECT_TRUE(key.has_value()); |
| |
| SchemefulSite frame_site(GURL("https://cookiesite.com")); |
| key = CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(top_level_site, frame_site), SiteForCookies(), |
| top_level_site); |
| EXPECT_TRUE(key.has_value()); |
| } |
| |
| // Test that creating nonced partition keys works with both types of |
| // NetworkIsolationKey modes. See https://crbug.com/1442260. |
| TEST_P(CookiePartitionKeyTest, NetworkIsolationKeyMode) { |
| const net::SchemefulSite kTopFrameSite(GURL("https://a.com")); |
| const net::SchemefulSite kFrameSite(GURL("https://b.com")); |
| const auto kNonce = base::UnguessableToken::Create(); |
| |
| SiteForCookies site_for_cookies = |
| SiteForCookies::FromUrl(GURL("https://a.com")); |
| |
| { // Frame site mode. |
| base::test::ScopedFeatureList feature_list; |
| |
| feature_list.InitWithFeatureState( |
| features::kEnableCrossSiteFlagNetworkIsolationKey, false); |
| |
| const auto key = CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(kTopFrameSite, kFrameSite, kNonce), |
| site_for_cookies, kTopFrameSite); |
| EXPECT_TRUE(key); |
| EXPECT_EQ(key->site(), kFrameSite); |
| EXPECT_EQ(key->nonce().value(), kNonce); |
| EXPECT_TRUE(key->IsThirdParty()); |
| } |
| |
| { // Cross-site flag mode. |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitWithFeatureStates( |
| {{net::features::kEnableCrossSiteFlagNetworkIsolationKey, true}, |
| {features::kAncestorChainBitEnabledInPartitionedCookies, |
| AncestorChainBitEnabled()}}); |
| |
| const auto key = CookiePartitionKey::FromNetworkIsolationKey( |
| NetworkIsolationKey(kTopFrameSite, kFrameSite, kNonce), |
| site_for_cookies, kTopFrameSite); |
| EXPECT_TRUE(key); |
| EXPECT_EQ(key->site(), kFrameSite); |
| EXPECT_EQ(key->nonce().value(), kNonce); |
| EXPECT_TRUE(key->IsThirdParty()); |
| } |
| } |
| } // namespace net |