| // 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 "net/cookies/cookie_partition_key.h" |
| |
| #include <optional> |
| #include <string> |
| #include <tuple> |
| |
| #include "base/test/scoped_feature_list.h" |
| #include "net/base/features.h" |
| #include "net/base/network_isolation_partition.h" |
| #include "net/cookies/cookie_constants.h" |
| #include "net/cookies/site_for_cookies.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| namespace net { |
| |
| using enum CookiePartitionKey::AncestorChainBit; |
| |
| class CookiePartitionKeyTest : public ::testing::Test {}; |
| |
| |
| TEST(CookiePartitionKeyTest, TestFromStorage) { |
| struct { |
| std::string top_level_site; |
| bool third_party; |
| base::expected<std::optional<CookiePartitionKey>, std::string> |
| expected_output; |
| } cases[] = { |
| {/*empty site*/ |
| "", true, base::ok(std::nullopt)}, |
| /*invalid site*/ |
| {"Invalid", true, |
| base::unexpected( |
| "Cannot deserialize opaque origin to CookiePartitionKey")}, |
| /*malformed site*/ |
| {"https://toplevelsite.com/", true, |
| base::unexpected("Cannot deserialize malformed top_level_site to " |
| "CookiePartitionKey")}, |
| /*valid site: cross site*/ |
| {"https://toplevelsite.com", true, |
| base::ok(CookiePartitionKey::FromURLForTesting( |
| GURL("https://toplevelsite.com")))}, |
| /*valid site: same site*/ |
| {"https://toplevelsite.com", false, |
| base::ok(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(tc.expected_output, got) << got.ToString(); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| { |
| base::AutoReset<bool> reset = |
| CookiePartitionKey::DisablePartitioningInScopeForTesting(); |
| EXPECT_FALSE( |
| CookiePartitionKey::FromStorage("https://toplevelsite.com", |
| /*has_cross_site_ancestor=*/true) |
| .has_value()); |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| TEST(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); |
| } |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| { |
| base::AutoReset<bool> reset = |
| CookiePartitionKey::DisablePartitioningInScopeForTesting(); |
| EXPECT_FALSE( |
| CookiePartitionKey::FromUntrustedInput("https://toplevelsite.com", |
| /*has_cross_site_ancestor=*/true) |
| .has_value()); |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| } |
| |
| TEST(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), |
| Output{"https://toplevelsite.com", false}}, |
| // AncestorChain::kCrossSite |
| {CookiePartitionKey::FromURLForTesting(GURL("https://toplevelsite.com"), |
| kCrossSite), |
| Output{"https://toplevelsite.com", true}}, |
| // With nonce |
| {CookiePartitionKey::FromURLForTesting(GURL("https://cookiesite.com"), |
| kCrossSite, nonce), |
| std::nullopt}, |
| // Same site w/ nonce |
| {CookiePartitionKey::FromURLForTesting(GURL("https://toplevelsite.com"), |
| kSameSite, nonce), |
| 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()) { |
| EXPECT_EQ(tc.expected_output->top_level_site, got->TopLevelSite()); |
| EXPECT_EQ(tc.expected_output->cross_site, got->has_cross_site_ancestor()); |
| } |
| } |
| } |
| |
| TEST(CookiePartitionKeyTest, FromNetworkIsolationKey) { |
| const SchemefulSite kTopLevelSite(GURL("https://toplevelsite.com")); |
| const SchemefulSite kCookieSite(GURL("https://cookiesite.com")); |
| const SchemefulSite kLocalhost(GURL("https://localhost:8000")); |
| 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; |
| const bool main_frame_navigation; |
| } test_cases[] = { |
| {"Empty", NetworkIsolationKey(), std::nullopt, SiteForCookies(), |
| SchemefulSite(), |
| /*main_frame_navigation=*/false}, |
| {"WithTopLevelSite", NetworkIsolationKey(kTopLevelSite, kCookieSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL()), |
| SiteForCookies(), kTopLevelSite, |
| /*main_frame_navigation=*/false}, |
| {"WithNonce", NetworkIsolationKey(kTopLevelSite, kCookieSite, kNonce), |
| CookiePartitionKey::FromURLForTesting(kCookieSite.GetURL(), kCrossSite, |
| kNonce), |
| SiteForCookies(), kTopLevelSite, |
| /*main_frame_navigation=*/false}, |
| {"WithNetworkIsolationPartition", |
| NetworkIsolationKey( |
| kTopLevelSite, kCookieSite, /*nonce=*/std::nullopt, |
| NetworkIsolationPartition::kProtectedAudienceSellerWorklet), |
| std::nullopt, SiteForCookies(), kTopLevelSite, |
| /*main_frame_navigation=*/false}, |
| {"WithCrossSiteAncestorSameSite", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kSameSite, |
| std::nullopt), |
| SiteForCookies::FromUrl(kTopLevelSite.GetURL()), kTopLevelSite, |
| /*main_frame_navigation=*/false}, |
| {"Nonced first party NIK results in kCrossSite partition key", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite, kNonce), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kCrossSite, |
| kNonce), |
| SiteForCookies::FromUrl(kTopLevelSite.GetURL()), kTopLevelSite, |
| /*main_frame_navigation=*/false}, |
| {"WithCrossSiteAncestorNotSameSite", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kCrossSite, |
| std::nullopt), |
| SiteForCookies(), kCookieSite, |
| /*main_frame_navigation=*/false}, |
| {"TestMainFrameNavigationParam", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kSameSite, |
| std::nullopt), |
| SiteForCookies::FromUrl(kTopLevelSite.GetURL()), kCookieSite, |
| /*main_frame_navigation=*/true}, |
| {"PresenceOfNonceTakesPriorityOverMainFrameNavigation", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite, kNonce), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kCrossSite, |
| kNonce), |
| SiteForCookies::FromUrl(kTopLevelSite.GetURL()), kTopLevelSite, |
| /*main_frame_navigation=*/true}, |
| {"LocalhostABA", NetworkIsolationKey(kLocalhost, kLocalhost), |
| CookiePartitionKey::FromURLForTesting(kLocalhost.GetURL(), kCrossSite, |
| std::nullopt), |
| SiteForCookies(), kLocalhost, |
| /*main_frame_navigation=*/false}, |
| {"LocalhostCrossSite", NetworkIsolationKey(kLocalhost, kCookieSite), |
| CookiePartitionKey::FromURLForTesting(kLocalhost.GetURL(), kCrossSite, |
| std::nullopt), |
| SiteForCookies(), kLocalhost, |
| /*main_frame_navigation=*/false}, |
| // Different request_site results in cross site ancestor |
| {"DifferentRequestSite", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), |
| kCrossSite), |
| SiteForCookies::FromUrl(kTopLevelSite.GetURL()), kCookieSite, |
| /*main_frame_navigation=*/false}, |
| {"DifferentRequestSiteMainFrameNavigation", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kSameSite), |
| SiteForCookies::FromUrl(kTopLevelSite.GetURL()), kCookieSite, |
| /*main_frame_navigation=*/true}, |
| {"DifferentRequestSiteMainFrameNavigationNullSiteForCookies", |
| NetworkIsolationKey(kTopLevelSite, kTopLevelSite), |
| CookiePartitionKey::FromURLForTesting(kTopLevelSite.GetURL(), kSameSite), |
| SiteForCookies(), kCookieSite, |
| /*main_frame_navigation=*/true}, |
| }; |
| |
| 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, test_case.main_frame_navigation); |
| |
| EXPECT_EQ(test_case.expected, got); |
| if (got) { |
| EXPECT_EQ(test_case.network_isolation_key.GetNonce(), got->nonce()); |
| } |
| } |
| } |
| |
| TEST(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(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(CookiePartitionKeyTest, FromScript) { |
| auto key = CookiePartitionKey::FromScript(); |
| EXPECT_TRUE(key.from_script()); |
| EXPECT_TRUE(key.site().opaque()); |
| EXPECT_TRUE(key.IsThirdParty()); |
| |
| auto key2 = CookiePartitionKey::FromScript(); |
| EXPECT_TRUE(key2.from_script()); |
| EXPECT_TRUE(key2.site().opaque()); |
| EXPECT_TRUE(key2.IsThirdParty()); |
| |
| EXPECT_EQ(key, key2); |
| } |
| |
| TEST(CookiePartitionKeyTest, IsSerializeable) { |
| EXPECT_FALSE(CookiePartitionKey::FromURLForTesting(GURL()).IsSerializeable()); |
| EXPECT_TRUE( |
| CookiePartitionKey::FromURLForTesting(GURL("https://www.example.com")) |
| .IsSerializeable()); |
| } |
| |
| TEST(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(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_NE(key1 , key2); |
| EXPECT_EQ(key1, CookiePartitionKey::FromURLForTesting( |
| GURL("https://foo.com"), kSameSite, std::nullopt)); |
| } |
| |
| TEST(CookiePartitionKeyTest, Equality_WithNonce) { |
| GURL frame_url("https://cookiesite.com"); |
| base::UnguessableToken nonce1 = base::UnguessableToken::Create(); |
| base::UnguessableToken nonce2 = base::UnguessableToken::Create(); |
| ASSERT_NE(nonce1, nonce2); |
| |
| CookiePartitionKey key1 = |
| CookiePartitionKey::FromURLForTesting(frame_url, kCrossSite, nonce1); |
| CookiePartitionKey key2 = |
| CookiePartitionKey::FromURLForTesting(frame_url, kCrossSite, nonce2); |
| EXPECT_NE(key1, key2); |
| |
| CookiePartitionKey key3 = |
| CookiePartitionKey::FromURLForTesting(frame_url, kCrossSite, nonce1); |
| EXPECT_EQ(key1, key3); |
| CookiePartitionKey unnonced_key = |
| CookiePartitionKey::FromURLForTesting(frame_url, kCrossSite); |
| EXPECT_NE(key1, unnonced_key); |
| } |
| |
| TEST(CookiePartitionKeyTest, NoncedKeyForbidsUnpartitionedAccess) { |
| GURL frame_url("https://cookiesite.com"); |
| |
| EXPECT_FALSE( |
| CookiePartitionKey::FromURLForTesting(frame_url, kCrossSite, std::nullopt) |
| .ForbidsUnpartitionedCookieAccess()); |
| |
| EXPECT_TRUE(CookiePartitionKey::FromURLForTesting( |
| frame_url, kCrossSite, base::UnguessableToken::Create()) |
| .ForbidsUnpartitionedCookieAccess()); |
| } |
| |
| } // namespace net |