| // 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 "third_party/blink/public/common/storage_key/storage_key.h" |
| |
| #include <optional> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/feature_list.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/unguessable_token.h" |
| #include "net/base/features.h" |
| #include "net/base/isolation_info.h" |
| #include "net/base/schemeful_site.h" |
| #include "net/cookies/cookie_partition_key.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/mojom/storage_key/ancestor_chain_bit.mojom.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace blink { |
| namespace { |
| |
| // Opaqueness here is used as a way of checking for "correctly constructed" in |
| // most tests. |
| // |
| // Why not call it IsValid()? Because some tests actually want to check for |
| // opaque origins. |
| bool IsOpaque(const StorageKey& key) { |
| return key.origin().opaque() && key.top_level_site().opaque(); |
| } |
| |
| } // namespace |
| |
| class StorageKeyTest : public ::testing::Test { |
| protected: |
| const net::SchemefulSite GetOpaqueSite(uint64_t high, |
| uint64_t low, |
| std::string_view url_string) { |
| return net::SchemefulSite(url::Origin( |
| url::Origin::Nonce(base::UnguessableToken::CreateForTesting(high, low)), |
| url::SchemeHostPort(GURL(url_string)))); |
| } |
| |
| std::vector<StorageKey> StorageKeysForCookiePartitionKeyTest( |
| const base::UnguessableToken& nonce) { |
| return std::vector<StorageKey>{ |
| /*Check storage key from string*/ |
| {StorageKey::CreateFromStringForTesting("https://www.example.com")}, |
| /*kCrossSite*/ |
| {StorageKey::Create(url::Origin::Create(GURL("https://www.foo.com")), |
| net::SchemefulSite(GURL("https://www.bar.com")), |
| mojom::AncestorChainBit::kCrossSite)}, |
| /*kSameSite keys check*/ |
| {StorageKey::Create(url::Origin::Create(GURL("https://www.foo.com")), |
| net::SchemefulSite(GURL("https://www.foo.com")), |
| mojom::AncestorChainBit::kSameSite)}, |
| /*First party check*/ |
| {StorageKey::CreateFirstParty( |
| url::Origin::Create(GURL("https://www.foo.com")))}, |
| /*Nonced*/ |
| {StorageKey::CreateWithNonce( |
| url::Origin::Create(GURL("https://www.example.com")), nonce)}}; |
| } |
| }; |
| |
| // Test when a constructed StorageKey object should be considered valid/opaque. |
| TEST_F(StorageKeyTest, ConstructionValidity) { |
| StorageKey empty = StorageKey(); |
| EXPECT_TRUE(IsOpaque(empty)); |
| // These cases will have the same origin for both `origin` and |
| // `top_level_site`. |
| url::Origin valid_origin = url::Origin::Create(GURL("https://example.com")); |
| const StorageKey valid = StorageKey::CreateFirstParty(valid_origin); |
| EXPECT_FALSE(IsOpaque(valid)); |
| // Since the same origin is used for both `origin` and `top_level_site`, it is |
| // by definition same-site. |
| EXPECT_EQ(valid.ancestor_chain_bit(), mojom::AncestorChainBit::kSameSite); |
| |
| url::Origin invalid_origin = |
| url::Origin::Create(GURL("I'm not a valid URL.")); |
| const StorageKey invalid = StorageKey::CreateFirstParty(invalid_origin); |
| EXPECT_TRUE(IsOpaque(invalid)); |
| } |
| |
| // Test that StorageKeys are/aren't equivalent as expected when storage |
| // partitioning is disabled. |
| TEST_F(StorageKeyTest, EquivalenceUnpartitioned) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndDisableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| url::Origin origin1 = url::Origin::Create(GURL("https://example.com")); |
| url::Origin origin2 = url::Origin::Create(GURL("https://test.example")); |
| url::Origin origin3 = url::Origin(); |
| // Create another opaque origin different from origin3. |
| url::Origin origin4 = url::Origin(); |
| base::UnguessableToken nonce1 = base::UnguessableToken::Create(); |
| base::UnguessableToken nonce2 = base::UnguessableToken::Create(); |
| |
| // Ensure that the opaque origins produce opaque StorageKeys |
| EXPECT_TRUE(IsOpaque(StorageKey::CreateFirstParty(origin3))); |
| EXPECT_TRUE(IsOpaque(StorageKey::CreateFirstParty(origin4))); |
| |
| const struct { |
| StorageKey storage_key1; |
| StorageKey storage_key2; |
| bool expected_equivalent; |
| } kTestCases[] = { |
| // StorageKeys made from the same origin are equivalent. |
| {StorageKey::CreateFirstParty(origin1), |
| StorageKey::CreateFirstParty(origin1), true}, |
| {StorageKey::CreateFirstParty(origin2), |
| StorageKey::CreateFirstParty(origin2), true}, |
| {StorageKey::CreateFirstParty(origin3), |
| StorageKey::CreateFirstParty(origin3), true}, |
| {StorageKey::CreateFirstParty(origin4), |
| StorageKey::CreateFirstParty(origin4), true}, |
| // StorageKeys made from the same origin and nonce are equivalent. |
| {StorageKey::CreateWithNonce(origin1, nonce1), |
| StorageKey::CreateWithNonce(origin1, nonce1), true}, |
| {StorageKey::CreateWithNonce(origin1, nonce2), |
| StorageKey::CreateWithNonce(origin1, nonce2), true}, |
| {StorageKey::CreateWithNonce(origin2, nonce1), |
| StorageKey::CreateWithNonce(origin2, nonce1), true}, |
| // StorageKeys made from different origins are not equivalent. |
| {StorageKey::CreateFirstParty(origin1), |
| StorageKey::CreateFirstParty(origin2), false}, |
| {StorageKey::CreateFirstParty(origin3), |
| StorageKey::CreateFirstParty(origin4), false}, |
| {StorageKey::CreateWithNonce(origin1, nonce1), |
| StorageKey::CreateWithNonce(origin2, nonce1), false}, |
| // StorageKeys made from different nonces are not equivalent. |
| {StorageKey::CreateWithNonce(origin1, nonce1), |
| StorageKey::CreateWithNonce(origin1, nonce2), false}, |
| // StorageKeys made from different origins and nonce are not equivalent. |
| {StorageKey::CreateWithNonce(origin1, nonce1), |
| StorageKey::CreateWithNonce(origin2, nonce2), false}, |
| // When storage partitioning is disabled, the top-level site isn't taken |
| // into account for equivalence. |
| {StorageKey::Create(origin1, net::SchemefulSite(origin2), |
| blink::mojom::AncestorChainBit::kCrossSite), |
| StorageKey::CreateFirstParty(origin1), true}, |
| {StorageKey::Create(origin2, net::SchemefulSite(origin1), |
| blink::mojom::AncestorChainBit::kCrossSite), |
| StorageKey::CreateFirstParty(origin2), true}, |
| }; |
| for (const auto& test_case : kTestCases) { |
| ASSERT_EQ(test_case.storage_key1 == test_case.storage_key2, |
| test_case.expected_equivalent); |
| } |
| } |
| |
| // Test that StorageKeys are/aren't equivalent as expected when storage |
| // partitioning is enabled. |
| TEST_F(StorageKeyTest, EquivalencePartitioned) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| url::Origin origin1 = url::Origin::Create(GURL("https://example.com")); |
| url::Origin origin2 = url::Origin::Create(GURL("https://test.example")); |
| |
| net::SchemefulSite site1(origin1); |
| net::SchemefulSite site2(origin2); |
| |
| // Keys should only match when both the origin and top-level site are the |
| // same. Such as keys made from the single argument constructor and keys |
| // created by the two argument constructor (when both arguments are the same |
| // origin). |
| |
| const StorageKey OneArgKey_origin1 = StorageKey::CreateFirstParty(origin1); |
| const StorageKey OneArgKey_origin2 = StorageKey::CreateFirstParty(origin2); |
| |
| StorageKey TwoArgKey_origin1_origin1 = StorageKey::Create( |
| origin1, site1, blink::mojom::AncestorChainBit::kSameSite); |
| StorageKey TwoArgKey_origin2_origin2 = StorageKey::Create( |
| origin2, site2, blink::mojom::AncestorChainBit::kSameSite); |
| |
| EXPECT_EQ(OneArgKey_origin1, TwoArgKey_origin1_origin1); |
| EXPECT_EQ(OneArgKey_origin2, TwoArgKey_origin2_origin2); |
| |
| // And when the two argument constructor gets different values |
| StorageKey TwoArgKey1_origin1_origin2 = StorageKey::Create( |
| origin1, site2, blink::mojom::AncestorChainBit::kCrossSite); |
| StorageKey TwoArgKey2_origin1_origin2 = StorageKey::Create( |
| origin1, site2, blink::mojom::AncestorChainBit::kCrossSite); |
| StorageKey TwoArgKey_origin2_origin1 = StorageKey::Create( |
| origin2, site1, blink::mojom::AncestorChainBit::kCrossSite); |
| |
| EXPECT_EQ(TwoArgKey1_origin1_origin2, TwoArgKey2_origin1_origin2); |
| |
| // Otherwise they're not equivalent. |
| EXPECT_NE(TwoArgKey1_origin1_origin2, TwoArgKey_origin1_origin1); |
| EXPECT_NE(TwoArgKey_origin2_origin1, TwoArgKey_origin2_origin2); |
| EXPECT_NE(TwoArgKey1_origin1_origin2, TwoArgKey_origin2_origin1); |
| } |
| |
| // Test that StorageKeys Serialize to the expected value with partitioning |
| // enabled and disabled. |
| TEST_F(StorageKeyTest, SerializeFirstParty) { |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| |
| struct { |
| const char* origin_str; |
| const char* expected_serialization; |
| } kTestCases[] = { |
| {"https://example.com/", "https://example.com/"}, |
| // Trailing slash is added. |
| {"https://example.com", "https://example.com/"}, |
| // Subdomains are preserved. |
| {"http://sub.test.example/", "http://sub.test.example/"}, |
| // file: origins all serialize to "file:///" |
| {"file:///", "file:///"}, |
| {"file:///foo/bar", "file:///"}, |
| {"file://example.fileshare.com/foo/bar", "file:///"}, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(test.origin_str); |
| const StorageKey key = |
| StorageKey::CreateFromStringForTesting(test.origin_str); |
| EXPECT_EQ(test.expected_serialization, key.Serialize()); |
| } |
| } |
| } |
| |
| TEST_F(StorageKeyTest, SerializeFirstPartyForLocalStorage) { |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| |
| struct { |
| const char* origin_str; |
| const char* expected_serialization; |
| } kTestCases[] = { |
| // Trailing slash is removed. |
| {"https://example.com/", "https://example.com"}, |
| {"https://example.com", "https://example.com"}, |
| // Subdomains are preserved. |
| {"http://sub.test.example/", "http://sub.test.example"}, |
| // file: origins all serialize to "file://" |
| {"file://", "file://"}, |
| {"file:///foo/bar", "file://"}, |
| {"file://example.fileshare.com/foo/bar", "file://"}, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(test.origin_str); |
| const StorageKey key = |
| StorageKey::CreateFromStringForTesting(test.origin_str); |
| EXPECT_EQ(test.expected_serialization, key.SerializeForLocalStorage()); |
| } |
| } |
| } |
| |
| // Tests that the top-level site is correctly serialized for service workers |
| // when kThirdPartyStoragePartitioning is enabled. This is expected to be the |
| // same for localStorage. |
| TEST_F(StorageKeyTest, SerializePartitioned) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| net::SchemefulSite SiteExample(GURL("https://example.com")); |
| net::SchemefulSite SiteTest(GURL("https://test.example")); |
| |
| const struct { |
| const char* origin; |
| net::SchemefulSite top_level_site; |
| mojom::AncestorChainBit ancestor_chain_bit; |
| const char* expected_serialization; |
| } kTestCases[] = { |
| // 3p context cases |
| {"https://example.com/", SiteTest, mojom::AncestorChainBit::kCrossSite, |
| "https://example.com/^0https://test.example"}, |
| {"https://sub.test.example/", SiteExample, |
| mojom::AncestorChainBit::kCrossSite, |
| "https://sub.test.example/^0https://example.com"}, |
| {"https://example.com/", SiteExample, mojom::AncestorChainBit::kCrossSite, |
| "https://example.com/^31"}, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(test.origin); |
| const url::Origin origin = url::Origin::Create(GURL(test.origin)); |
| StorageKey key = StorageKey::Create(origin, test.top_level_site, |
| test.ancestor_chain_bit); |
| EXPECT_EQ(test.expected_serialization, key.Serialize()); |
| EXPECT_EQ(test.expected_serialization, key.SerializeForLocalStorage()); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, SerializeNonce) { |
| struct { |
| const std::pair<const char*, const base::UnguessableToken> origin_and_nonce; |
| const char* expected_serialization; |
| } kTestCases[] = { |
| {{"https://example.com/", |
| base::UnguessableToken::CreateForTesting(12345ULL, 67890ULL)}, |
| "https://example.com/^112345^267890"}, |
| {{"https://test.example", |
| base::UnguessableToken::CreateForTesting(22222ULL, 99999ULL)}, |
| "https://test.example/^122222^299999"}, |
| {{"https://sub.test.example/", |
| base::UnguessableToken::CreateForTesting(9876ULL, 54321ULL)}, |
| "https://sub.test.example/^19876^254321"}, |
| {{"https://other.example/", |
| base::UnguessableToken::CreateForTesting(3735928559ULL, 110521ULL)}, |
| "https://other.example/^13735928559^2110521"}, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(test.origin_and_nonce.first); |
| const url::Origin origin = |
| url::Origin::Create(GURL(test.origin_and_nonce.first)); |
| const base::UnguessableToken& nonce = test.origin_and_nonce.second; |
| StorageKey key = StorageKey::CreateWithNonce(origin, nonce); |
| EXPECT_EQ(test.expected_serialization, key.Serialize()); |
| } |
| } |
| |
| // Test that deserialized StorageKeys are valid/opaque as expected. |
| TEST_F(StorageKeyTest, Deserialize) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| const struct { |
| std::string serialized_string; |
| bool expected_has_value; |
| bool expected_opaque = false; |
| } kTestCases[] = { |
| // Correct usage of origin. |
| {"https://example.com/", true, false}, |
| // Correct usage of test.example origin. |
| {"https://test.example/", true, false}, |
| // Correct usage of origin with port. |
| {"https://example.com:90/", true, false}, |
| // Invalid origin URL. |
| {"I'm not a valid URL.", false}, |
| // Empty string origin URL. |
| {std::string(), false}, |
| // Correct usage of origin and top-level site. |
| {"https://example.com/^0https://test.example", true, false}, |
| // Incorrect separator value used for top-level site. |
| {"https://example.com/^1https://test.example", false}, |
| // Correct usage of origin and top-level site with test.example. |
| {"https://test.example/^0https://example.com", true, false}, |
| // Invalid top-level site. |
| {"https://example.com/^0I'm not a valid URL.", false}, |
| // Invalid origin with top-level site scheme. |
| {"I'm not a valid URL.^0https://example.com", false}, |
| // Correct usage of origin and nonce. |
| {"https://example.com/^112345^267890", true, false}, |
| // Nonce high not followed by nonce low. |
| {"https://example.com/^112345^167890", false}, |
| // Nonce high not followed by nonce low; invalid separator value. |
| {"https://example.com/^112345^967890", false}, |
| // Values encoded with nonce separator not a valid nonce. |
| {"https://example.com/^1nota^2nonce", false}, |
| // Invalid origin with nonce scheme. |
| {"I'm not a valid URL.^112345^267890", false}, |
| // Nonce low was incorrectly encoded before nonce high. |
| {"https://example.com/^212345^167890", false}, |
| // Malformed usage of three separator carets. |
| {"https://example.com/^112345^267890^", false}, |
| // Incorrect: Separator not followed by data. |
| {"https://example.com/^1^267890", false}, |
| // Malformed first party serialization. |
| {"https://www.example.com/^0https://example.com", false}, |
| // Malformed ancestor chain bit value - outside range. |
| {"https://example.com/^35", false}, |
| // Correct ancestor chain bit value. |
| {"https://example.com/^31", true}, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| SCOPED_TRACE(test_case.serialized_string); |
| std::optional<StorageKey> key = |
| StorageKey::Deserialize(test_case.serialized_string); |
| ASSERT_EQ(key.has_value(), test_case.expected_has_value); |
| if (key.has_value()) |
| EXPECT_EQ(IsOpaque(*key), test_case.expected_opaque); |
| } |
| } |
| |
| // Test that string -> StorageKey test function performs as expected. |
| TEST_F(StorageKeyTest, CreateFromStringForTesting) { |
| std::string example = "https://example.com/"; |
| std::string wrong = "I'm not a valid URL."; |
| |
| StorageKey key1 = StorageKey::CreateFromStringForTesting(example); |
| StorageKey key2 = StorageKey::CreateFromStringForTesting(wrong); |
| StorageKey key3 = StorageKey::CreateFromStringForTesting(std::string()); |
| |
| EXPECT_FALSE(IsOpaque(key1)); |
| EXPECT_EQ(key1, StorageKey::CreateFromStringForTesting(example)); |
| EXPECT_TRUE(IsOpaque(key2)); |
| EXPECT_TRUE(IsOpaque(key3)); |
| } |
| |
| // Test that a StorageKey, constructed by deserializing another serialized |
| // StorageKey, is equivalent to the original. |
| TEST_F(StorageKeyTest, SerializeDeserialize) { |
| const char* kTestCases[] = {"https://example.com", "https://sub.test.example", |
| "https://example.com:90", "file://", |
| "file://example.fileshare.com"}; |
| |
| for (const char* test : kTestCases) { |
| SCOPED_TRACE(test); |
| url::Origin origin = url::Origin::Create(GURL(test)); |
| const StorageKey key = StorageKey::CreateFirstParty(origin); |
| std::string key_string = key.Serialize(); |
| std::string key_string_for_local_storage = key.SerializeForLocalStorage(); |
| std::optional<StorageKey> key_deserialized = |
| StorageKey::Deserialize(key_string); |
| std::optional<StorageKey> key_deserialized_from_local_storage = |
| StorageKey::DeserializeForLocalStorage(key_string_for_local_storage); |
| |
| ASSERT_TRUE(key_deserialized.has_value()); |
| ASSERT_TRUE(key_deserialized_from_local_storage.has_value()); |
| if (origin.scheme() != "file") { |
| EXPECT_EQ(key, *key_deserialized); |
| EXPECT_EQ(key, *key_deserialized_from_local_storage); |
| } else { |
| // file origins are all collapsed to file:// by serialization. |
| EXPECT_EQ(StorageKey::CreateFromStringForTesting("file://"), |
| *key_deserialized); |
| EXPECT_EQ(StorageKey::CreateFromStringForTesting("file://"), |
| *key_deserialized_from_local_storage); |
| } |
| } |
| } |
| |
| // Same as SerializeDeserialize but for partitioned StorageKeys when |
| // kThirdPartyStoragePartitioning is enabled. |
| TEST_F(StorageKeyTest, SerializeDeserializePartitioned) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| net::SchemefulSite SiteExample(GURL("https://example.com")); |
| net::SchemefulSite SiteTest(GURL("https://test.example")); |
| net::SchemefulSite SitePort(GURL("https://example.com:90")); |
| net::SchemefulSite SiteFile(GURL("file:///")); |
| |
| const struct { |
| const char* origin; |
| net::SchemefulSite site; |
| } kTestCases[] = { |
| // 1p context case. |
| {"https://example.com/", SiteExample}, |
| {"https://test.example", SiteTest}, |
| {"https://sub.test.example/", SiteTest}, |
| {"https://example.com:90", SitePort}, |
| // 3p context case |
| {"https://example.com/", SiteTest}, |
| {"https://sub.test.example/", SiteExample}, |
| {"https://sub.test.example/", SitePort}, |
| // File case. |
| {"file:///foo/bar", SiteFile}, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(test.origin); |
| url::Origin origin = url::Origin::Create(GURL(test.origin)); |
| const net::SchemefulSite& site = test.site; |
| |
| StorageKey key = |
| StorageKey::Create(origin, site, |
| net::SchemefulSite(origin) == site |
| ? mojom::AncestorChainBit::kSameSite |
| : mojom::AncestorChainBit::kCrossSite); |
| std::string key_string = key.Serialize(); |
| std::string key_string_for_local_storage = key.SerializeForLocalStorage(); |
| std::optional<StorageKey> key_deserialized = |
| StorageKey::Deserialize(key_string); |
| std::optional<StorageKey> key_deserialized_from_local_storage = |
| StorageKey::DeserializeForLocalStorage(key_string_for_local_storage); |
| |
| ASSERT_TRUE(key_deserialized.has_value()); |
| ASSERT_TRUE(key_deserialized_from_local_storage.has_value()); |
| if (origin.scheme() != "file") { |
| EXPECT_EQ(key, *key_deserialized); |
| EXPECT_EQ(key, *key_deserialized_from_local_storage); |
| } else { |
| // file origins are all collapsed to file:// by serialization. |
| EXPECT_EQ(StorageKey::Create(url::Origin::Create(GURL("file://")), |
| net::SchemefulSite(GURL("file://")), |
| mojom::AncestorChainBit::kSameSite), |
| *key_deserialized); |
| EXPECT_EQ(StorageKey::Create(url::Origin::Create(GURL("file://")), |
| net::SchemefulSite(GURL("file://")), |
| mojom::AncestorChainBit::kSameSite), |
| *key_deserialized_from_local_storage); |
| } |
| } |
| } |
| |
| TEST_F(StorageKeyTest, SerializeDeserializeNonce) { |
| struct { |
| const char* origin; |
| const base::UnguessableToken nonce; |
| } kTestCases[] = { |
| {"https://example.com/", |
| base::UnguessableToken::CreateForTesting(12345ULL, 67890ULL)}, |
| {"https://test.example", |
| base::UnguessableToken::CreateForTesting(22222ULL, 99999ULL)}, |
| {"https://sub.test.example/", |
| base::UnguessableToken::CreateForTesting(9876ULL, 54321ULL)}, |
| {"https://other.example/", |
| base::UnguessableToken::CreateForTesting(3735928559ULL, 110521ULL)}, |
| {"https://other2.example/", base::UnguessableToken::Create()}, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(test.origin); |
| url::Origin origin = url::Origin::Create(GURL(test.origin)); |
| const base::UnguessableToken& nonce = test.nonce; |
| |
| StorageKey key = StorageKey::CreateWithNonce(origin, nonce); |
| std::string key_string = key.Serialize(); |
| std::string key_string_for_local_storage = key.SerializeForLocalStorage(); |
| std::optional<StorageKey> key_deserialized = |
| StorageKey::Deserialize(key_string); |
| std::optional<StorageKey> key_deserialized_from_local_storage = |
| StorageKey::DeserializeForLocalStorage(key_string_for_local_storage); |
| |
| ASSERT_TRUE(key_deserialized.has_value()); |
| ASSERT_TRUE(key_deserialized_from_local_storage.has_value()); |
| |
| EXPECT_EQ(key, *key_deserialized); |
| EXPECT_EQ(key, *key_deserialized_from_local_storage); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, SerializeDeserializeOpaqueTopLevelSite) { |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| const struct { |
| url::Origin origin; |
| net::SchemefulSite top_level_site; |
| const char* expected_serialization_without_partitioning; |
| const char* expected_serialization_with_partitioning; |
| } kTestCases[] = { |
| { |
| url::Origin::Create(GURL("https://example.com/")), |
| GetOpaqueSite(12345ULL, 67890ULL, ""), |
| "https://example.com/", |
| "https://example.com/^412345^567890^6", |
| }, |
| { |
| url::Origin::Create(GURL("https://sub.example.com/")), |
| GetOpaqueSite(22222ULL, 99999ULL, ""), |
| "https://sub.example.com/", |
| "https://sub.example.com/^422222^599999^6", |
| }, |
| { |
| url::Origin::Create(GURL("https://example.com/")), |
| GetOpaqueSite(9876ULL, 54321ULL, "https://notexample.com/"), |
| "https://example.com/", |
| "https://example.com/^49876^554321^6https://notexample.com", |
| }, |
| { |
| url::Origin::Create(GURL("https://sub.example.com/")), |
| GetOpaqueSite(3735928559ULL, 110521ULL, |
| "https://sub.notexample.com/"), |
| "https://sub.example.com/", |
| "https://sub.example.com/^43735928559^5110521^6https://" |
| "sub.notexample.com", |
| }, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| if (toggle) { |
| SCOPED_TRACE(test.expected_serialization_with_partitioning); |
| } else { |
| SCOPED_TRACE(test.expected_serialization_without_partitioning); |
| } |
| EXPECT_TRUE(test.top_level_site.opaque()); |
| StorageKey key = |
| StorageKey::Create(test.origin, test.top_level_site, |
| blink::mojom::AncestorChainBit::kCrossSite); |
| if (toggle) { |
| EXPECT_EQ(test.expected_serialization_with_partitioning, |
| key.Serialize()); |
| EXPECT_EQ(key, StorageKey::Deserialize( |
| test.expected_serialization_with_partitioning)); |
| } else { |
| EXPECT_EQ(test.expected_serialization_without_partitioning, |
| key.Serialize()); |
| EXPECT_EQ(key, StorageKey::Deserialize( |
| test.expected_serialization_without_partitioning)); |
| } |
| } |
| } |
| } |
| |
| TEST_F(StorageKeyTest, DeserializeNonces) { |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| struct { |
| const char* serialization; |
| std::optional<blink::StorageKey> expected_key; |
| const bool has_value_if_partitioning_is_disabled; |
| } kTestCases[] = { |
| { |
| "https://example.com/^40^50^6", |
| std::nullopt, |
| false, |
| }, |
| { |
| "https://example.com/^41^50^6", |
| blink::StorageKey::Create( |
| url::Origin::Create(GURL("https://example.com/")), |
| GetOpaqueSite(1ULL, 0ULL, ""), |
| mojom::AncestorChainBit::kCrossSite), |
| false, |
| }, |
| { |
| "https://example.com/^401^50^6", |
| std::nullopt, |
| false, |
| }, |
| { |
| "https://example.com/^40^51^6", |
| blink::StorageKey::Create( |
| url::Origin::Create(GURL("https://example.com/")), |
| GetOpaqueSite(0ULL, 1ULL, ""), |
| mojom::AncestorChainBit::kCrossSite), |
| false, |
| }, |
| { |
| "https://example.com/^400^51^6", |
| std::nullopt, |
| false, |
| }, |
| { |
| "https://example.com/^41^51^6", |
| blink::StorageKey::Create( |
| url::Origin::Create(GURL("https://example.com/")), |
| GetOpaqueSite(1ULL, 1ULL, ""), |
| mojom::AncestorChainBit::kCrossSite), |
| false, |
| }, |
| { |
| "https://example.com/^41^501^6", |
| std::nullopt, |
| false, |
| }, |
| { |
| "https://example.com/^10^20", |
| std::nullopt, |
| false, |
| }, |
| { |
| "https://example.com/^11^20", |
| blink::StorageKey::CreateWithNonce( |
| url::Origin::Create(GURL("https://example.com/")), |
| base::UnguessableToken::CreateForTesting(1ULL, 0ULL)), |
| true, |
| }, |
| { |
| "https://example.com/^101^20", |
| std::nullopt, |
| true, |
| }, |
| { |
| "https://example.com/^10^21", |
| blink::StorageKey::CreateWithNonce( |
| url::Origin::Create(GURL("https://example.com/")), |
| base::UnguessableToken::CreateForTesting(0ULL, 1ULL)), |
| true, |
| }, |
| { |
| "https://example.com/^100^21", |
| std::nullopt, |
| true, |
| }, |
| { |
| "https://example.com/^11^21", |
| blink::StorageKey::CreateWithNonce( |
| url::Origin::Create(GURL("https://example.com/")), |
| base::UnguessableToken::CreateForTesting(1ULL, 1ULL)), |
| true, |
| }, |
| { |
| "https://example.com/^11^201", |
| std::nullopt, |
| true, |
| }, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(test.serialization); |
| std::optional<blink::StorageKey> maybe_storage_key = |
| StorageKey::Deserialize(test.serialization); |
| EXPECT_EQ((test.has_value_if_partitioning_is_disabled || toggle) && |
| test.expected_key, |
| (bool)maybe_storage_key); |
| if (maybe_storage_key) { |
| EXPECT_EQ(test.expected_key, |
| StorageKey::Deserialize(test.serialization)); |
| } |
| } |
| } |
| } |
| |
| TEST_F(StorageKeyTest, DeserializeAncestorChainBits) { |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| struct { |
| const char* serialization; |
| std::optional<blink::StorageKey> expected_key; |
| } kTestCases[] = { |
| // An origin cannot be serialized with a SameSite bit. |
| { |
| "https://example.com/^30", |
| std::nullopt, |
| }, |
| // An origin cannot be serialized with a malformed CrossSite bit. |
| { |
| "https://example.com/^301", |
| std::nullopt, |
| }, |
| // An origin can be serialized with a CrossSite bit. |
| { |
| "https://example.com/^31", |
| StorageKey::Create(url::Origin::Create(GURL("https://example.com")), |
| net::SchemefulSite(GURL("https://example.com")), |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| // A mismatched origin and top_level_site cannot have a SameSite bit. |
| { |
| "https://example.com/^0https://notexample.com^30", |
| std::nullopt, |
| }, |
| // A mismatched origin and top_level_site cannot have a CrossSite bit. |
| { |
| "https://example.com/^0https://notexample.com^31", |
| std::nullopt, |
| }, |
| // A mismatched origin and top_level_site can have no bit. |
| { |
| "https://example.com/^0https://notexample.com", |
| StorageKey::Create( |
| url::Origin::Create(GURL("https://example.com")), |
| net::SchemefulSite(GURL("https://notexample.com")), |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| }; |
| |
| for (const auto& test : kTestCases) { |
| SCOPED_TRACE(test.serialization); |
| if (toggle) { |
| EXPECT_EQ(test.expected_key, |
| StorageKey::Deserialize(test.serialization)); |
| } else { |
| EXPECT_FALSE(StorageKey::Deserialize(test.serialization)); |
| } |
| } |
| } |
| } |
| |
| TEST_F(StorageKeyTest, IsThirdPartyStoragePartitioningEnabled) { |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| EXPECT_EQ(StorageKey::IsThirdPartyStoragePartitioningEnabled(), toggle); |
| } |
| } |
| |
| // Test that StorageKey's top_level_site getter returns origin's site when |
| // storage partitioning is disabled. |
| TEST_F(StorageKeyTest, TopLevelSiteGetterWithPartitioningDisabled) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndDisableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| url::Origin origin1 = url::Origin::Create(GURL("https://example.com")); |
| url::Origin origin2 = url::Origin::Create(GURL("https://test.example")); |
| net::SchemefulSite site1(origin1); |
| net::SchemefulSite site2(origin2); |
| |
| const StorageKey key_origin1 = StorageKey::CreateFirstParty(origin1); |
| StorageKey key_origin1_site1 = |
| StorageKey::Create(origin1, site1, mojom::AncestorChainBit::kSameSite); |
| StorageKey key_origin1_site2 = |
| StorageKey::Create(origin1, site2, mojom::AncestorChainBit::kCrossSite); |
| |
| EXPECT_EQ(site1, key_origin1.top_level_site()); |
| EXPECT_EQ(site1, key_origin1_site1.top_level_site()); |
| EXPECT_EQ(site1, key_origin1_site2.top_level_site()); |
| } |
| |
| // Test that StorageKey's top_level_site getter returns the top level site when |
| // storage partitioning is enabled. |
| TEST_F(StorageKeyTest, TopLevelSiteGetterWithPartitioningEnabled) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| url::Origin origin1 = url::Origin::Create(GURL("https://example.com")); |
| url::Origin origin2 = url::Origin::Create(GURL("https://test.example")); |
| |
| const StorageKey key_origin1 = StorageKey::CreateFirstParty(origin1); |
| StorageKey key_origin1_site1 = StorageKey::Create( |
| origin1, net::SchemefulSite(origin1), mojom::AncestorChainBit::kSameSite); |
| StorageKey key_origin1_site2 = |
| StorageKey::Create(origin1, net::SchemefulSite(origin2), |
| mojom::AncestorChainBit::kCrossSite); |
| |
| EXPECT_EQ(net::SchemefulSite(origin1), key_origin1.top_level_site()); |
| EXPECT_EQ(net::SchemefulSite(origin1), key_origin1_site1.top_level_site()); |
| EXPECT_EQ(net::SchemefulSite(origin2), key_origin1_site2.top_level_site()); |
| } |
| |
| // Test that cross-origin keys cannot be deserialized when partitioning is |
| // disabled. |
| TEST_F(StorageKeyTest, AncestorChainBitGetterWithPartitioningDisabled) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndDisableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| std::string cross_site_string = "https://example.com/^0https://test.example"; |
| std::optional<StorageKey> key_cross_site = |
| StorageKey::Deserialize(cross_site_string); |
| EXPECT_FALSE(key_cross_site.has_value()); |
| } |
| |
| // Test that the AncestorChainBit enum class is not reordered and returns the |
| // correct value when storage partitioning is enabled. |
| TEST_F(StorageKeyTest, AncestorChainBitGetterWithPartitioningEnabled) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| std::string cross_site_string = "https://example.com/^0https://test.example"; |
| std::optional<StorageKey> key_cross_site = |
| StorageKey::Deserialize(cross_site_string); |
| EXPECT_TRUE(key_cross_site.has_value()); |
| EXPECT_EQ(mojom::AncestorChainBit::kCrossSite, |
| key_cross_site->ancestor_chain_bit()); |
| } |
| |
| TEST_F(StorageKeyTest, IsThirdPartyContext) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| const url::Origin kOrigin = url::Origin::Create(GURL("https://www.foo.com")); |
| const url::Origin kInsecureOrigin = |
| url::Origin::Create(GURL("http://www.foo.com")); |
| const url::Origin kSubdomainOrigin = |
| url::Origin::Create(GURL("https://bar.foo.com")); |
| const url::Origin kDifferentSite = |
| url::Origin::Create(GURL("https://www.bar.com")); |
| |
| struct TestCase { |
| const url::Origin origin; |
| const url::Origin top_level_origin; |
| const bool expected; |
| const bool has_nonce = false; |
| } test_cases[] = {{kOrigin, kOrigin, false}, |
| {kOrigin, kInsecureOrigin, true}, |
| {kOrigin, kSubdomainOrigin, false}, |
| {kOrigin, kDifferentSite, true}, |
| {kOrigin, kOrigin, true, true}}; |
| for (const auto& test_case : test_cases) { |
| if (test_case.has_nonce) { |
| StorageKey key = StorageKey::CreateWithNonce( |
| test_case.origin, base::UnguessableToken::Create()); |
| EXPECT_EQ(test_case.expected, key.IsThirdPartyContext()); |
| EXPECT_NE(key.IsThirdPartyContext(), key.IsFirstPartyContext()); |
| continue; |
| } |
| mojom::AncestorChainBit ancestor_chain_bit = |
| net::SchemefulSite(test_case.origin) == |
| net::SchemefulSite(test_case.top_level_origin) |
| ? mojom::AncestorChainBit::kSameSite |
| : mojom::AncestorChainBit::kCrossSite; |
| StorageKey key = StorageKey::Create( |
| test_case.origin, net::SchemefulSite(test_case.top_level_origin), |
| ancestor_chain_bit); |
| EXPECT_EQ(test_case.expected, key.IsThirdPartyContext()); |
| EXPECT_NE(key.IsThirdPartyContext(), key.IsFirstPartyContext()); |
| // IsThirdPartyContext should not depend on the order of the arguments. |
| key = StorageKey::Create(test_case.top_level_origin, |
| net::SchemefulSite(test_case.origin), |
| ancestor_chain_bit); |
| EXPECT_EQ(test_case.expected, key.IsThirdPartyContext()); |
| EXPECT_NE(key.IsThirdPartyContext(), key.IsFirstPartyContext()); |
| } |
| // Explicitly testing the A->B->A case AncestorChainBit is preventing: |
| // Same origin and top-level site but cross-site ancestor |
| StorageKey cross_key = |
| StorageKey::Create(kOrigin, net::SchemefulSite(kOrigin), |
| mojom::AncestorChainBit::kCrossSite); |
| EXPECT_EQ(true, cross_key.IsThirdPartyContext()); |
| } |
| |
| TEST_F(StorageKeyTest, ToNetSiteForCookies) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| const url::Origin kOrigin = url::Origin::Create(GURL("https://www.foo.com")); |
| const url::Origin kInsecureOrigin = |
| url::Origin::Create(GURL("http://www.foo.com")); |
| const url::Origin kSubdomainOrigin = |
| url::Origin::Create(GURL("https://bar.foo.com")); |
| const url::Origin kDifferentSite = |
| url::Origin::Create(GURL("https://www.bar.com")); |
| |
| struct TestCase { |
| const url::Origin origin; |
| const url::Origin top_level_origin; |
| const net::SchemefulSite expected; |
| const bool expected_opaque = false; |
| const bool has_nonce = false; |
| } test_cases[] = { |
| {kOrigin, kOrigin, net::SchemefulSite(kOrigin)}, |
| {kOrigin, kInsecureOrigin, net::SchemefulSite(), true}, |
| {kInsecureOrigin, kOrigin, net::SchemefulSite(), true}, |
| {kOrigin, kSubdomainOrigin, net::SchemefulSite(kOrigin)}, |
| {kSubdomainOrigin, kOrigin, net::SchemefulSite(kOrigin)}, |
| {kOrigin, kDifferentSite, net::SchemefulSite(), true}, |
| {kOrigin, kOrigin, net::SchemefulSite(), true, true}, |
| }; |
| for (const auto& test_case : test_cases) { |
| net::SchemefulSite got_site; |
| if (test_case.has_nonce) { |
| got_site = StorageKey::CreateWithNonce(test_case.origin, |
| base::UnguessableToken::Create()) |
| .ToNetSiteForCookies() |
| .site(); |
| } else { |
| net::SchemefulSite top_level_site = |
| net::SchemefulSite(test_case.top_level_origin); |
| |
| mojom::AncestorChainBit ancestor_chain_bit = |
| top_level_site == net::SchemefulSite(test_case.origin) |
| ? mojom::AncestorChainBit::kSameSite |
| : mojom::AncestorChainBit::kCrossSite; |
| |
| got_site = StorageKey::Create(test_case.origin, top_level_site, |
| ancestor_chain_bit) |
| .ToNetSiteForCookies() |
| .site(); |
| } |
| if (test_case.expected_opaque) { |
| EXPECT_TRUE(got_site.opaque()); |
| continue; |
| } |
| EXPECT_EQ(test_case.expected, got_site); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, ToPartialNetIsolationInfo) { |
| const auto kOrigin = url::Origin::Create(GURL("https://subdomain.foo.com")); |
| const auto kOtherOrigin = |
| url::Origin::Create(GURL("https://subdomain.bar.com")); |
| const auto nonce = base::UnguessableToken::Create(); |
| |
| { // Same-site storage key |
| const auto storage_key = |
| StorageKey::Create(kOrigin, net::SchemefulSite(kOrigin), |
| mojom::AncestorChainBit::kSameSite); |
| |
| storage_key.ToPartialNetIsolationInfo().IsEqualForTesting( |
| net::IsolationInfo::Create(net::IsolationInfo::RequestType::kOther, |
| kOrigin, kOrigin, |
| net::SiteForCookies::FromOrigin(kOrigin))); |
| } |
| |
| { // Cross-site storage key |
| const auto storage_key = |
| StorageKey::Create(kOrigin, net::SchemefulSite(kOtherOrigin), |
| mojom::AncestorChainBit::kCrossSite); |
| |
| storage_key.ToPartialNetIsolationInfo().IsEqualForTesting( |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kOther, |
| net::SchemefulSite(kOrigin).GetInternalOriginForTesting(), |
| kOtherOrigin, net::SiteForCookies())); |
| } |
| |
| { // Nonced key |
| const auto storage_key = StorageKey::CreateWithNonce(kOrigin, nonce); |
| |
| storage_key.ToPartialNetIsolationInfo().IsEqualForTesting( |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kOther, |
| net::SchemefulSite(kOrigin).GetInternalOriginForTesting(), kOrigin, |
| net::SiteForCookies(), nonce)); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, CopyWithForceEnabledThirdPartyStoragePartitioning) { |
| const url::Origin kOrigin = url::Origin::Create(GURL("https://foo.com")); |
| const url::Origin kOtherOrigin = url::Origin::Create(GURL("https://bar.com")); |
| |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| |
| const StorageKey storage_key = |
| StorageKey::Create(kOrigin, net::SchemefulSite(kOtherOrigin), |
| mojom::AncestorChainBit::kCrossSite); |
| EXPECT_EQ(storage_key.IsThirdPartyContext(), toggle); |
| EXPECT_EQ(storage_key.top_level_site(), |
| net::SchemefulSite(toggle ? kOtherOrigin : kOrigin)); |
| EXPECT_EQ(storage_key.ancestor_chain_bit(), |
| toggle ? mojom::AncestorChainBit::kCrossSite |
| : mojom::AncestorChainBit::kSameSite); |
| |
| const StorageKey storage_key_with_3psp = |
| storage_key.CopyWithForceEnabledThirdPartyStoragePartitioning(); |
| EXPECT_TRUE(storage_key_with_3psp.IsThirdPartyContext()); |
| EXPECT_EQ(storage_key_with_3psp.top_level_site(), |
| net::SchemefulSite(kOtherOrigin)); |
| EXPECT_EQ(storage_key_with_3psp.ancestor_chain_bit(), |
| mojom::AncestorChainBit::kCrossSite); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, ToCookiePartitionKeyAncestorChainEnabled) { |
| auto nonce = base::UnguessableToken::Create(); |
| |
| std::vector<StorageKey> storage_keys = |
| StorageKeysForCookiePartitionKeyTest(nonce); |
| |
| // The ScopedFeatureList is used here to ensure that the |
| // CookiePartitionKeys created from the storage_keys vector have the expected |
| // result. |
| |
| { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatures( |
| {net::features::kThirdPartyStoragePartitioning}, |
| {}); |
| |
| std::vector<std::optional<net::CookiePartitionKey>> expected_cpk{ |
| {net::CookiePartitionKey::FromURLForTesting( |
| GURL("https://www.example.com"), |
| net::CookiePartitionKey::AncestorChainBit::kSameSite)}, |
| {net::CookiePartitionKey::FromURLForTesting( |
| GURL("https://subdomain.bar.com"))}, |
| {net::CookiePartitionKey::FromURLForTesting( |
| GURL("https://www.foo.com"), |
| net::CookiePartitionKey::AncestorChainBit::kSameSite)}, |
| {net::CookiePartitionKey::FromURLForTesting( |
| GURL("https://www.foo.com"), |
| net::CookiePartitionKey::AncestorChainBit::kSameSite)}, |
| {net::CookiePartitionKey::FromURLForTesting( |
| GURL("https://www.example.com"), |
| net::CookiePartitionKey::AncestorChainBit::kCrossSite, nonce)}, |
| }; |
| std::vector<std::optional<net::CookiePartitionKey>> got; |
| std::ranges::transform( |
| storage_keys, std::back_inserter(got), |
| [](const StorageKey& key) -> std::optional<net::CookiePartitionKey> { |
| return key.ToCookiePartitionKey(); |
| }); |
| EXPECT_EQ(expected_cpk, got); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, NonceRequiresMatchingOriginSiteAndCrossSite) { |
| const url::Origin origin = url::Origin::Create(GURL("https://foo.com")); |
| const url::Origin opaque_origin; |
| const net::SchemefulSite site(origin); |
| const net::SchemefulSite opaque_site(opaque_origin); |
| base::UnguessableToken nonce = base::UnguessableToken::Create(); |
| |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| |
| // Test non-opaque origin. |
| StorageKey key = StorageKey::CreateWithNonce(origin, nonce); |
| EXPECT_EQ(key.ancestor_chain_bit(), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| EXPECT_EQ(key.top_level_site(), site); |
| |
| // Test opaque origin. |
| key = StorageKey::CreateWithNonce(opaque_origin, nonce); |
| EXPECT_EQ(key.ancestor_chain_bit(), |
| blink::mojom::AncestorChainBit::kCrossSite); |
| EXPECT_EQ(key.top_level_site(), opaque_site); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, NoncedKeyForbidsUnpartitionedAccess) { |
| const url::Origin origin = url::Origin::Create(GURL("https://foo.com")); |
| const net::SchemefulSite site(origin); |
| base::UnguessableToken nonce = base::UnguessableToken::Create(); |
| |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| |
| StorageKey key = |
| StorageKey::Create(origin, site, mojom::AncestorChainBit::kSameSite); |
| EXPECT_FALSE(key.ForbidsUnpartitionedStorageAccess()); |
| |
| key = StorageKey::CreateWithNonce(origin, nonce); |
| EXPECT_TRUE(key.ForbidsUnpartitionedStorageAccess()); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, OpaqueTopLevelSiteRequiresCrossSite) { |
| const url::Origin origin = url::Origin::Create(GURL("https://foo.com")); |
| const net::SchemefulSite site(origin); |
| const net::SchemefulSite opaque_site; |
| |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| |
| // A non-opaque site with SameSite and CrossSite works. |
| std::ignore = |
| StorageKey::Create(origin, site, mojom::AncestorChainBit::kSameSite); |
| std::ignore = |
| StorageKey::Create(origin, site, mojom::AncestorChainBit::kCrossSite); |
| |
| // An opaque site with CrossSite works. |
| std::ignore = StorageKey::Create(origin, opaque_site, |
| mojom::AncestorChainBit::kCrossSite); |
| |
| // An opaque site with SameSite fails. |
| EXPECT_DCHECK_DEATH(StorageKey::Create(origin, opaque_site, |
| mojom::AncestorChainBit::kSameSite)); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, ShouldSkipKeyDueToPartitioning) { |
| const struct { |
| std::string serialized_key; |
| bool expected_skip; |
| } kTestCases[] = { |
| // First party keys should not be skipped. |
| { |
| "https://example.com/", |
| false, |
| }, |
| // Nonce keys should not be skipped. |
| { |
| "https://example.com/^19876^25432", |
| false, |
| }, |
| // Third-party keys should be skipped. |
| { |
| "https://example.com/^0https://test.example", |
| true, |
| }, |
| // Third-party keys should be skipped. |
| { |
| "https://example.com/^31", |
| true, |
| }, |
| // Third-party keys with opaque sites should be skipped. |
| { |
| "https://example.com/^412345^567890^6", |
| true, |
| }, |
| // Corrupted keys should be not skipped. |
| { |
| "https://example.com/^7", |
| false, |
| }, |
| }; |
| |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| for (const auto& test_case : kTestCases) { |
| if (toggle) { |
| EXPECT_FALSE(StorageKey::ShouldSkipKeyDueToPartitioning( |
| test_case.serialized_key)); |
| } else { |
| EXPECT_EQ(StorageKey::ShouldSkipKeyDueToPartitioning( |
| test_case.serialized_key), |
| test_case.expected_skip); |
| } |
| } |
| } |
| } |
| |
| TEST_F(StorageKeyTest, OriginAndSiteMismatchRequiresCrossSite) { |
| const url::Origin origin = url::Origin::Create(GURL("https://foo.com")); |
| const url::Origin opaque_origin; |
| const net::SchemefulSite site(origin); |
| const net::SchemefulSite other_site(GURL("https://notfoo.com")); |
| |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| |
| // A matching origin and site can be SameSite or CrossSite. |
| std::ignore = |
| StorageKey::Create(origin, site, mojom::AncestorChainBit::kSameSite); |
| std::ignore = |
| StorageKey::Create(origin, site, mojom::AncestorChainBit::kCrossSite); |
| |
| // A mismatched origin and site cannot be SameSite. |
| EXPECT_DCHECK_DEATH(StorageKey::Create(origin, other_site, |
| mojom::AncestorChainBit::kSameSite)); |
| EXPECT_DCHECK_DEATH(StorageKey::Create(opaque_origin, other_site, |
| mojom::AncestorChainBit::kSameSite)); |
| |
| // A mismatched origin and site must be CrossSite. |
| std::ignore = StorageKey::Create(origin, other_site, |
| mojom::AncestorChainBit::kCrossSite); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, WithOrigin) { |
| const url::Origin origin = url::Origin::Create(GURL("https://foo.com")); |
| const url::Origin other_origin = |
| url::Origin::Create(GURL("https://notfoo.com")); |
| const url::Origin opaque_origin; |
| const net::SchemefulSite site(origin); |
| const net::SchemefulSite other_site(other_origin); |
| const net::SchemefulSite opaque_site(opaque_origin); |
| const base::UnguessableToken nonce = base::UnguessableToken::Create(); |
| |
| base::test::ScopedFeatureList scoped_feature_list; |
| // WithOrigin's operation doesn't depend on the state of |
| // kThirdPartyStoragePartitioning and toggling the feature's state makes the |
| // test more difficult since Create()'s behavior *will* |
| // change. So we only run with it on. |
| scoped_feature_list.InitAndEnableFeature( |
| net::features::kThirdPartyStoragePartitioning); |
| |
| const struct { |
| blink::StorageKey original_key; |
| url::Origin new_origin; |
| std::optional<blink::StorageKey> expected_key; |
| } kTestCases[] = { |
| // No change in first-party key updated with same origin. |
| { |
| blink::StorageKey::Create(origin, site, |
| mojom::AncestorChainBit::kSameSite), |
| origin, |
| std::nullopt, |
| }, |
| // Change in first-party key updated with new origin. |
| { |
| blink::StorageKey::Create(origin, site, |
| mojom::AncestorChainBit::kSameSite), |
| other_origin, |
| blink::StorageKey::Create(other_origin, site, |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| // No change in third-party same-site key updated with same origin. |
| { |
| blink::StorageKey::Create(origin, site, |
| mojom::AncestorChainBit::kCrossSite), |
| origin, |
| std::nullopt, |
| }, |
| // Change in third-party same-site key updated with same origin. |
| { |
| blink::StorageKey::Create(origin, site, |
| mojom::AncestorChainBit::kCrossSite), |
| other_origin, |
| blink::StorageKey::Create(other_origin, site, |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| // No change in third-party key updated with same origin. |
| { |
| blink::StorageKey::Create(origin, other_site, |
| mojom::AncestorChainBit::kCrossSite), |
| origin, |
| std::nullopt, |
| }, |
| // Change in third-party key updated with new origin. |
| { |
| blink::StorageKey::Create(origin, other_site, |
| mojom::AncestorChainBit::kCrossSite), |
| other_origin, |
| blink::StorageKey::Create(other_origin, other_site, |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| // No change in opaque tls key updated with same origin. |
| { |
| blink::StorageKey::Create(origin, opaque_site, |
| mojom::AncestorChainBit::kCrossSite), |
| origin, |
| std::nullopt, |
| }, |
| // Change in opaque tls key updated with new origin. |
| { |
| blink::StorageKey::Create(origin, opaque_site, |
| mojom::AncestorChainBit::kCrossSite), |
| other_origin, |
| blink::StorageKey::Create(other_origin, opaque_site, |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| // No change in nonce key updated with same origin. |
| { |
| blink::StorageKey::CreateWithNonce(origin, nonce), |
| origin, |
| std::nullopt, |
| }, |
| // Change in nonce key updated with new origin. |
| { |
| blink::StorageKey::CreateWithNonce(origin, nonce), |
| other_origin, |
| blink::StorageKey::CreateWithNonce(other_origin, nonce), |
| }, |
| // Change in opaque top_level_site key updated with opaque origin. |
| { |
| blink::StorageKey::Create(origin, opaque_site, |
| mojom::AncestorChainBit::kCrossSite), |
| opaque_origin, |
| blink::StorageKey::Create(opaque_origin, opaque_site, |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| if (test_case.expected_key == std::nullopt) { |
| EXPECT_EQ(test_case.original_key, |
| test_case.original_key.WithOrigin(test_case.new_origin)); |
| } else { |
| ASSERT_NE(test_case.expected_key, test_case.original_key); |
| EXPECT_EQ(test_case.expected_key, |
| test_case.original_key.WithOrigin(test_case.new_origin)); |
| } |
| } |
| } |
| |
| // Tests that FromWire() returns true/false correctly. |
| // If you make a change here, you should probably make it in BlinkStorageKeyTest |
| // too. |
| TEST_F(StorageKeyTest, FromWireReturnValue) { |
| using AncestorChainBit = blink::mojom::AncestorChainBit; |
| const url::Origin o1 = url::Origin::Create(GURL("https://a.com")); |
| const url::Origin o2 = url::Origin::Create(GURL("https://b.com")); |
| const url::Origin o3 = url::Origin::Create(GURL("https://c.com")); |
| const url::Origin opaque = url::Origin(); |
| const net::SchemefulSite site1 = net::SchemefulSite(o1); |
| const net::SchemefulSite site2 = net::SchemefulSite(o2); |
| const net::SchemefulSite site3 = net::SchemefulSite(o3); |
| const net::SchemefulSite opaque_site = net::SchemefulSite(opaque); |
| base::UnguessableToken nonce1 = base::UnguessableToken::Create(); |
| |
| const struct TestCase { |
| const url::Origin origin; |
| const net::SchemefulSite top_level_site; |
| const net::SchemefulSite top_level_site_if_third_party_enabled; |
| const std::optional<base::UnguessableToken> nonce; |
| AncestorChainBit ancestor_chain_bit; |
| AncestorChainBit ancestor_chain_bit_if_third_party_enabled; |
| bool result; |
| } test_cases[] = { |
| // Passing cases: |
| {o1, site1, site1, std::nullopt, AncestorChainBit::kSameSite, |
| AncestorChainBit::kSameSite, true}, |
| {o1, site1, site1, nonce1, AncestorChainBit::kCrossSite, |
| AncestorChainBit::kCrossSite, true}, |
| {o1, site1, site2, std::nullopt, AncestorChainBit::kSameSite, |
| AncestorChainBit::kCrossSite, true}, |
| {o1, site1, site1, std::nullopt, AncestorChainBit::kSameSite, |
| AncestorChainBit::kCrossSite, true}, |
| {o1, site1, site1, nonce1, AncestorChainBit::kCrossSite, |
| AncestorChainBit::kCrossSite, true}, |
| {opaque, site1, site1, std::nullopt, AncestorChainBit::kCrossSite, |
| AncestorChainBit::kCrossSite, true}, |
| {o1, site1, opaque_site, std::nullopt, AncestorChainBit::kSameSite, |
| AncestorChainBit::kCrossSite, true}, |
| {o1, opaque_site, opaque_site, std::nullopt, AncestorChainBit::kCrossSite, |
| AncestorChainBit::kCrossSite, true}, |
| {opaque, opaque_site, opaque_site, std::nullopt, |
| AncestorChainBit::kCrossSite, AncestorChainBit::kCrossSite, true}, |
| // Failing cases: |
| // If a 3p key is indicated, the *if_third_party_enabled pieces should |
| // match their counterparts. |
| {o1, site2, site3, std::nullopt, AncestorChainBit::kSameSite, |
| AncestorChainBit::kSameSite, false}, |
| {o1, site1, site1, std::nullopt, AncestorChainBit::kCrossSite, |
| AncestorChainBit::kSameSite, false}, |
| // If the top_level_site* is cross-site to the origin, the |
| // ancestor_chain_bit* must indicate cross-site. |
| {o1, site2, site2, std::nullopt, AncestorChainBit::kSameSite, |
| AncestorChainBit::kCrossSite, false}, |
| {o1, site1, site2, std::nullopt, AncestorChainBit::kSameSite, |
| AncestorChainBit::kSameSite, false}, |
| {o1, site2, site2, std::nullopt, AncestorChainBit::kSameSite, |
| AncestorChainBit::kSameSite, false}, |
| // If there is a nonce, all other values must indicate same-site to |
| // origin. |
| {o1, site2, site2, nonce1, AncestorChainBit::kSameSite, |
| AncestorChainBit::kSameSite, false}, |
| {o1, site1, site1, nonce1, AncestorChainBit::kSameSite, |
| AncestorChainBit::kSameSite, false}, |
| {o1, site1, site1, nonce1, AncestorChainBit::kSameSite, |
| AncestorChainBit::kCrossSite, false}, |
| // If the top_level_site* is opaque, the ancestor_chain_bit* must be |
| // cross-site. |
| {o1, site1, opaque_site, std::nullopt, AncestorChainBit::kCrossSite, |
| AncestorChainBit::kSameSite, false}, |
| {o1, opaque_site, opaque_site, std::nullopt, AncestorChainBit::kSameSite, |
| AncestorChainBit::kSameSite, false}, |
| // If the origin is opaque, the ancestor_chain_bit* must be cross-site. |
| {opaque, opaque_site, opaque_site, std::nullopt, |
| AncestorChainBit::kSameSite, AncestorChainBit::kSameSite, false}, |
| {opaque, opaque_site, opaque_site, std::nullopt, |
| AncestorChainBit::kCrossSite, AncestorChainBit::kSameSite, false}, |
| {opaque, opaque_site, opaque_site, std::nullopt, |
| AncestorChainBit::kSameSite, AncestorChainBit::kCrossSite, false}, |
| }; |
| |
| const StorageKey starting_key; |
| |
| for (const auto& test_case : test_cases) { |
| StorageKey result_key = starting_key; |
| EXPECT_EQ( |
| test_case.result, |
| StorageKey::FromWire( |
| test_case.origin, test_case.top_level_site, |
| test_case.top_level_site_if_third_party_enabled, test_case.nonce, |
| test_case.ancestor_chain_bit, |
| test_case.ancestor_chain_bit_if_third_party_enabled, result_key)); |
| if (!test_case.result) { |
| // The key should not be modified for a return value of false. |
| EXPECT_TRUE(starting_key.ExactMatchForTesting(result_key)); |
| } |
| } |
| } |
| |
| TEST_F(StorageKeyTest, CreateFromOriginAndIsolationInfo) { |
| const url::Origin origin = url::Origin::Create(GURL("https://foo.com")); |
| const url::Origin other_origin = |
| url::Origin::Create(GURL("https://notfoo.com")); |
| const url::Origin opaque_origin; |
| const net::SchemefulSite site(origin); |
| const net::SchemefulSite other_site(other_origin); |
| const net::SchemefulSite opaque_site(opaque_origin); |
| const base::UnguessableToken nonce = base::UnguessableToken::Create(); |
| |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| |
| const struct { |
| url::Origin new_origin; |
| const net::IsolationInfo isolation_info; |
| std::optional<blink::StorageKey> expected_key; |
| } kTestCases[] = { |
| // First party context. |
| { |
| origin, |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, origin, origin, |
| net::SiteForCookies::FromOrigin(origin), |
| /*nonce=*/std::nullopt), |
| blink::StorageKey::Create(origin, site, |
| mojom::AncestorChainBit::kSameSite), |
| }, |
| // Third party same-site context. |
| { |
| other_origin, |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, other_origin, |
| origin, net::SiteForCookies(), |
| /*nonce=*/std::nullopt), |
| blink::StorageKey::Create(other_origin, other_site, |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| // Third party context. |
| { |
| other_origin, |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, origin, origin, |
| net::SiteForCookies::FromOrigin(origin), |
| /*nonce=*/std::nullopt), |
| blink::StorageKey::Create(other_origin, site, |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| // Opaque TLS context. |
| { |
| origin, |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, opaque_origin, |
| opaque_origin, net::SiteForCookies::FromOrigin(opaque_origin), |
| /*nonce=*/std::nullopt), |
| blink::StorageKey::Create(origin, opaque_site, |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| // Nonce context. |
| { |
| origin, |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, other_origin, |
| other_origin, net::SiteForCookies::FromOrigin(other_origin), |
| nonce), |
| blink::StorageKey::CreateWithNonce(origin, nonce), |
| }, |
| // Opaque context. |
| { |
| opaque_origin, |
| net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kMainFrame, origin, origin, |
| net::SiteForCookies::FromOrigin(origin), |
| /*nonce=*/std::nullopt), |
| blink::StorageKey::Create(opaque_origin, site, |
| mojom::AncestorChainBit::kCrossSite), |
| }, |
| }; |
| |
| for (const auto& test_case : kTestCases) { |
| if (test_case.expected_key == std::nullopt) { |
| EXPECT_DCHECK_DEATH(StorageKey::CreateFromOriginAndIsolationInfo( |
| test_case.new_origin, test_case.isolation_info)); |
| } else { |
| EXPECT_EQ(test_case.expected_key, |
| StorageKey::CreateFromOriginAndIsolationInfo( |
| test_case.new_origin, test_case.isolation_info)); |
| } |
| } |
| } |
| } |
| |
| TEST_F(StorageKeyTest, MalformedOriginsAndSchemefulSites) { |
| std::string kTestCases[] = { |
| // We cannot omit the slash in a first party key. |
| "https://example.com", |
| // We cannot add a path in a first party key. |
| "https://example.com/a", |
| // We cannot omit the slash in a same-site third party key. |
| "https://example.com^31", |
| // We cannot add a path in a same-site third party key. |
| "https://example.com/a^31", |
| // We cannot omit the slash in a nonce key. |
| "https://example.com^11^21", |
| // We cannot add a path in a nonce key. |
| "https://example.com/a^11^21", |
| // We cannot omit the first slash in a third-party key. |
| "https://example.com^0https://example.com", |
| // We cannot add a final slash in a third-party key. |
| "https://example.com/^0https://example.com/", |
| // We cannot add a first path in a third-party key. |
| "https://example.com/a^0https://example.com/", |
| // We cannot add a final path in a third-party key. |
| "https://example.com/^0https://example.com/a", |
| // We cannot omit the slash in an opaque top level site key. |
| "https://example.com^44^55^6", |
| // We cannot add a path in an opaque top level site key. |
| "https://example.com/a^44^55^6", |
| // We cannot omit the first slash in an opaque precursor key. |
| "https://example.com^44^55^6https://example.com", |
| // We cannot add a final slash in an opaque precursor key. |
| "https://example.com/^44^55^6https://example.com/", |
| // We cannot add a first path in an opaque precursor key. |
| "https://example.com/a^44^55^6https://example.com", |
| // We cannot add a final path in an opaque precursor key. |
| "https://example.com/^44^55^6https://example.com/a", |
| // We cannot omit the slash in a first party file key. |
| "file://", |
| // We cannot add a path in a first party file key. |
| "file:///a", |
| // We cannot add a slash in a third party file key. |
| "https://example.com/^0file:///", |
| // We cannot add a path in a third party file key. |
| "https://example.com/^0file:///a", |
| }; |
| |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| for (const auto& test_case : kTestCases) { |
| SCOPED_TRACE(test_case); |
| EXPECT_FALSE(StorageKey::Deserialize(test_case)); |
| } |
| } |
| } |
| |
| TEST_F(StorageKeyTest, DeserializeForLocalStorageFirstParty) { |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| |
| // This should deserialize as it lacks a trailing slash. |
| EXPECT_TRUE(StorageKey::DeserializeForLocalStorage("https://example.com") |
| .has_value()); |
| |
| // This should deserialize as it lacks a trailing slash. |
| EXPECT_FALSE(StorageKey::DeserializeForLocalStorage("https://example.com/") |
| .has_value()); |
| } |
| } |
| |
| TEST_F(StorageKeyTest, |
| SerializeDeserializeWithAndWithoutThirdPartyStoragePartitioning) { |
| struct { |
| const std::string serialized_key; |
| const bool has_value_if_partitioning_is_disabled; |
| } kTestCases[] = { |
| // This is a valid first-party file key. |
| { |
| "file:///", |
| true, |
| }, |
| // This is a valid third-party file key. |
| { |
| "file:///^31", |
| false, |
| }, |
| // This is a valid first-party origin key. |
| { |
| "https://example.com/", |
| true, |
| }, |
| // This is a valid third-party origin key. |
| { |
| "https://example.com/^31", |
| false, |
| }, |
| // This is a valid third-party cross-origin key. |
| { |
| "https://example.com/^0https://notexample.com", |
| false, |
| }, |
| // This is a valid nonce key. |
| { |
| "https://example.com/^11^21", |
| true, |
| }, |
| // This is a valid opaque top_level_site key. |
| { |
| "https://example.com/^41^51^6", |
| false, |
| }, |
| }; |
| |
| for (const bool toggle : {false, true}) { |
| base::test::ScopedFeatureList scope_feature_list; |
| scope_feature_list.InitWithFeatureState( |
| net::features::kThirdPartyStoragePartitioning, toggle); |
| for (const auto& test_case : kTestCases) { |
| const std::optional<blink::StorageKey> maybe_storage_key = |
| StorageKey::Deserialize(test_case.serialized_key); |
| EXPECT_EQ(test_case.has_value_if_partitioning_is_disabled || toggle, |
| (bool)maybe_storage_key); |
| if (maybe_storage_key) { |
| EXPECT_EQ(test_case.serialized_key, maybe_storage_key->Serialize()); |
| } |
| } |
| } |
| } |
| } // namespace blink |