blob: 4aaebb6c1eda7a74dc60cb66995d8f144e7da89a [file]
// 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