CVE-2026-79260
Overview
Background
- Nameless cookie
- A cookie serialized with an empty name, so it is sent on the wire as just its value (e.g.
Cookie: foo=barcan be ambiguously reparsed as namefoo, valuebarby a naive backend). - `__Host-` / `__Secure-` prefixes
- Restricted cookie name prefixes that enforce security constraints; RFC 6265bis §11.17 requires nameless cookies not to carry these prefixes in their values, to prevent subdomain injection into backends that reparse the value as a name.
- `EXCLUDE_AMBIGUOUS_SERIALIZATION`
- A
CookieInclusionStatusexclusion reason that rejects a nameless cookie whose value contains=, because such a cookie serializes ambiguously; it is gated on thekCookieParseRejectEmptyNameAmbiguousfeature. - Creation surfaces
- Cookies can be created via standard parsing (
CanonicalCookie::Create), the Mojo-facingCanonicalCookie::CreateSanitizedCookiepath, and are re-validated on load byIsCanonicalForFromStorage.
Root Cause Analysis
Standard cookie parsing in CanonicalCookie::Create already rejected nameless cookies containing = via the EXCLUDE_AMBIGUOUS_SERIALIZATION check (under kCookieParseRejectEmptyNameAmbiguous), but two other creation surfaces in canonical_cookie.cc lacked the same validation: the Mojo-facing CreateSanitizedCookie and the storage validator IsCanonicalForFromStorage.
As a result, a nameless cookie whose value contained = (for example =__Host-session=evil or foo=bar) could be created through the sanitized/Mojo path or accepted from storage even though it would be blocked on the normal parse path—violating the invariant that ambiguously-serializable nameless cookies are rejected uniformly across all entry points. Such a cookie, when sent, can be reparsed by a naive backend as a differently-named (possibly prefix-bearing) cookie, enabling subdomain cookie injection.
The fix replicates the check in both places: CreateSanitizedCookie now adds EXCLUDE_AMBIGUOUS_SERIALIZATION when name.empty(), the feature is enabled, and value.contains('='); IsCanonicalForFromStorage returns Fail(CanonicalizationFailure::kEmptyNameWithAmbiguousValue) under the same conditions. This tightens validation, closing the alternate paths. Complementarily, sqlite_persistent_cookie_store.cc now surgically deletes only the specific non-canonical rows (via DeleteNonCanonicalCookies) rather than aborting the whole load, so already-stored ambiguous cookies are purged while valid sibling cookies are preserved.
= ambiguity check on only the standard parse path while leaving CreateSanitizedCookie and IsCanonicalForFromStorage unguarded; the fix replicates the EXCLUDE_AMBIGUOUS_SERIALIZATION / kEmptyNameWithAmbiguousValue rejection on those surfaces so the rule holds everywhere.Attack Path
- Craft an ambiguous nameless cookie
An attacker constructs a nameless cookie whose value contains
=and embeds a restricted prefix, e.g. value=__Host-session=evil. - Use an unvalidated creation surface
The cookie is set through the Mojo-facing
CreateSanitizedCookiepath (or loaded from storage and validated byIsCanonicalForFromStorage), which pre-patch did not apply theEXCLUDE_AMBIGUOUS_SERIALIZATIONcheck and thus accepted it. - Cookie is stored/sent ambiguously Because the cookie serializes with no name, it is transmitted as just its value.
- Naive backend reparses value as a prefixed cookie (inferred)
A backend parser that splits on
=could interpret the value as a__Host-/__Secure-named cookie, enabling subdomain cookie injection per RFC 6265bis §11.17; this downstream reparse is(inferred)and depends on backend behavior outside Chrome.
Impact Assessment
= and a restricted prefix, which a naive backend may reparse as a differently-named, prefix-bearing cookie, enabling subdomain cookie injection. The advisory classifies this as InputValidation / “Improper input validation in Cookies”; the impact depends on a cooperating naive backend parser and is (inferred) beyond Chrome’s acceptance of the malformed cookie. The advisory does not state exploitation in the wild.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/extras/sqlite/sqlite_persistent_cookie_store.cc |
modified | |
fornet/extras/sqlite/sqlite_persistent_cookie_store.cc |
modified | |
TESTnet/cookies/canonical_cookie_unittest.cc |
modified | |
fornet/cookies/canonical_cookie_unittest.cc |
modified | |
fornet/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc |
modified |
Files Changed
net/extras/sqlite/sqlite_persistent_cookie_store_unittest.ccnet/cookies/canonical_cookie_unittest.ccservices/network/restricted_cookie_manager_unittest.ccnet/extras/sqlite/sqlite_persistent_cookie_store.ccnet/cookies/canonical_cookie.ccnet/cookies/canonical_cookie.h
Audit Directions
- Validation only on the primary parse pathLook for cookie/header/URL validation implemented in one creation function (e.g.
CanonicalCookie::Create) but missing from sibling surfaces such as sanitized/Mojo creators and from-storage validators. - Serialization ambiguity of empty-name/empty-field valuesAudit code that serializes name/value pairs where an empty name plus a value containing the delimiter (
=) can be reparsed differently by downstream consumers. - Storage-load rejection semanticsReview persistent-store load paths for whether non-canonical entries are silently kept, dropped wholesale, or surgically purged, ensuring invalid rows cannot survive to be re-served.
Patch
From bbf5240096c5e533b0c62ce3cc6979caf48f7bdb Mon Sep 17 00:00:00 2001
From: Etienne Bergeron <etienneb@google.com>
Date: Thu, 23 Jul 2026 08:20:41 -0700
Subject: [PATCH] [net/cookies] Reject ambiguous nameless cookies during sanitized creation
RFC 6265bis §11.17 mandates that nameless cookies should not contain
restricted prefixes (like __Host- or __Secure-) in their values, to
prevent subdomain cookie injection to naive backend parsers.
While standard parsing in CanonicalCookie::Create() blocks nameless
cookies containing '=' via the EXCLUDE_AMBIGUOUS_SERIALIZATION check
(gated on the kCookieParseRejectEmptyNameAmbiguous feature), the
Mojo-facing creation path in CanonicalCookie::CreateSanitizedCookie()
and the validator CanonicalCookie::IsCanonicalForFromStorage() lacked
this validation step.
This CL replicates the EXCLUDE_AMBIGUOUS_SERIALIZATION check inside
CreateSanitizedCookie() and IsCanonicalForFromStorage() to ensure
nameless cookies containing '=' in their values are correctly blocked
across all creation surfaces.
Bug: 533511967
Test: net_unittests --gtest_filter=CanonicalCookieTest.*
Test: services_unittests --gtest_filter=*RestrictedCookieManagerTest.*
Change-Id: I9545da06ab68422e1d65226e4d2794ee98a98458
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8088135
Reviewed-by: Adam Rice <ricea@chromium.org>
Commit-Queue: Etienne Bergeron <etienneb@chromium.org>
Reviewed-by: Nidhi Jaju <nidhijaju@chromium.org>
Reviewed-by: Maks Orlovich <morlovich@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1667115}
---
diff --git a/net/cookies/canonical_cookie.cc b/net/cookies/canonical_cookie.cc
index d8bc1573..6d4f43c 100644
--- a/net/cookies/canonical_cookie.cc
+++ b/net/cookies/canonical_cookie.cc
@@ -703,6 +703,14 @@
net::CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX);
}
+ if (name.empty() &&
+ base::FeatureList::IsEnabled(
+ features::kCookieParseRejectEmptyNameAmbiguous) &&
+ value.contains('=')) {
+ status->AddExclusionReason(CookieInclusionStatus::ExclusionReason::
+ EXCLUDE_AMBIGUOUS_SERIALIZATION);
+ }
+
if (!cookie_util::IsCookiePartitionedValid(url, secure, partition_key)) {
status->AddExclusionReason(net::CookieInclusionStatus::ExclusionReason::
EXCLUDE_INVALID_PARTITIONED);
@@ -1076,6 +1084,13 @@
return Fail(CanonicalizationFailure::kEmptyNameWithHiddenPrefix);
}
+ if (Name().empty() &&
+ base::FeatureList::IsEnabled(
+ features::kCookieParseRejectEmptyNameAmbiguous) &&
+ Value().contains('=')) {
+ return Fail(CanonicalizationFailure::kEmptyNameWithAmbiguousValue);
+ }
+
if (IsPartitioned() && !CookiePartitionKey::HasNonce(PartitionKey()) &&
!SecureAttribute()) {
return Fail(CanonicalizationFailure::kPartitionedInsecure);
@@ -1253,6 +1268,9 @@
return "kEmptyNameWithHiddenPrefix";
case CanonicalCookie::CanonicalizationFailure::kPartitionedInsecure:
return "kPartitionedInsecure";
+ case CanonicalCookie::CanonicalizationFailure::
+ kEmptyNameWithAmbiguousValue:
+ return "kEmptyNameWithAmbiguousValue";
}
NOTREACHED();
}();
diff --git a/net/cookies/canonical_cookie.h b/net/cookies/canonical_cookie.h
index b7dff30..b61a2cfc 100644
--- a/net/cookies/canonical_cookie.h
+++ b/net/cookies/canonical_cookie.h
@@ -83,6 +83,7 @@
kInvalidHostHttpPrefix,
kEmptyNameWithHiddenPrefix,
kPartitionedInsecure,
+ kEmptyNameWithAmbiguousValue,
};
// Carries metadata related to the canonicalization results for a given
diff --git a/net/extras/sqlite/sqlite_persistent_cookie_store.cc b/net/extras/sqlite/sqlite_persistent_cookie_store.cc
index 42d939f..586059f8 100644
--- a/net/extras/sqlite/sqlite_persistent_cookie_store.cc
+++ b/net/extras/sqlite/sqlite_persistent_cookie_store.cc
@@ -375,13 +375,25 @@
void LoadCookiesForKey(base::optional_ref<const std::string> key,
LoadedCallback loaded_callback);
+ struct NonCanonicalCookieDeleteInfo {
+ std::string name;
+ std::string host_key;
+ std::string top_frame_site_key;
+ std::string path;
+ int source_scheme;
+ int source_port;
+ bool has_cross_site_ancestor;
+ };
+
// Steps through all results of |statement|, makes a cookie from each, and
// adds the cookie to |cookies|. Returns true if everything loaded
// successfully.
bool MakeCookiesFromSQLStatement(
std::vector<std::unique_ptr<CanonicalCookie>>& cookies,
sql::Statement& statement,
- absl::flat_hash_set<std::string>& top_frame_site_keys_to_delete);
+ absl::flat_hash_set<std::string>& top_frame_site_keys_to_delete,
+ std::vector<NonCanonicalCookieDeleteInfo>&
+ non_canonical_cookies_to_delete);
// Batch a cookie addition.
void AddCookie(const CanonicalCookie& cc);
@@ -466,6 +478,9 @@
void DeleteTopFrameSiteKeys(
const absl::flat_hash_set<std::string>& top_frame_site_keys);
+ void DeleteNonCanonicalCookies(
+ const std::vector<NonCanonicalCookieDeleteInfo>& cookies_to_delete);
+
// Batch a cookie operation (add or delete)
void BatchOperation(PendingOperation::OperationType op,
const CanonicalCookie& cc);
@@ -902,16 +917,19 @@
std::vector<std::unique_ptr<CanonicalCookie>> cookies;
absl::flat_hash_set<std::string> top_frame_site_keys_to_delete;
+ std::vector<NonCanonicalCookieDeleteInfo> non_canonical_cookies_to_delete;
auto it = domains.begin();
bool ok = true;
for (; it != domains.end() && ok; ++it) {
smt.BindString(0, *it);
- ok = MakeCookiesFromSQLStatement(cookies, smt,
- top_frame_site_keys_to_delete);
+ ok =
+ MakeCookiesFromSQLStatement(cookies, smt, top_frame_site_keys_to_delete,
+ non_canonical_cookies_to_delete);
smt.Reset(true);
}
DeleteTopFrameSiteKeys(std::move(top_frame_site_keys_to_delete));
+ DeleteNonCanonicalCookies(std::move(non_canonical_cookies_to_delete));
if (ok) {
base::AutoLock locked(lock_);
@@ -955,10 +973,42 @@
}
}
+void SQLitePersistentCookieStore::Backend::DeleteNonCanonicalCookies(
+ const std::vector<NonCanonicalCookieDeleteInfo>& cookies_to_delete) {
+ if (cookies_to_delete.empty()) {
+ return;
+ }
+
+ sql::Statement delete_statement(db()->GetCachedStatement(
+ SQL_FROM_HERE,
+ "DELETE FROM cookies WHERE "
+ "name=? AND host_key=? AND top_frame_site_key=? AND path=? AND "
+ "source_scheme=? AND source_port=? AND has_cross_site_ancestor=?"));
+ if (!delete_statement.is_valid()) {
+ return;
+ }
+
+ for (const auto& info : cookies_to_delete) {
+ delete_statement.Reset(true);
+ delete_statement.BindString(0, info.name);
+ delete_statement.BindString(1, info.host_key);
+ delete_statement.BindString(2, info.top_frame_site_key);
+ delete_statement.BindString(3, info.path);
+ delete_statement.BindInt(4, info.source_scheme);
+ delete_statement.BindInt(5, info.source_port);
+ delete_statement.BindBool(6, info.has_cross_site_ancestor);
+ if (!delete_statement.Run()) {
+ RecordCookieLoadProblem(CookieLoadProblem::KRecoveryFailed);
+ }
+ }
+}
+
bool SQLitePersistentCookieStore::Backend::MakeCookiesFromSQLStatement(
std::vector<std::unique_ptr<CanonicalCookie>>& cookies,
sql::Statement& statement,
- absl::flat_hash_set<std::string>& top_frame_site_keys_to_delete) {
+ absl::flat_hash_set<std::string>& top_frame_site_keys_to_delete,
+ std::vector<NonCanonicalCookieDeleteInfo>&
+ non_canonical_cookies_to_delete) {
DCHECK(background_task_runner()->RunsTasksInCurrentSequence());
bool ok = true;
while (statement.Step()) {
@@ -1052,7 +1102,15 @@
cookies.push_back(std::move(cc));
} else {
RecordCookieLoadProblem(CookieLoadProblem::kNotCanonical);
- ok = false;
+ non_canonical_cookies_to_delete.push_back({
+ .name = statement.ColumnString(3),
+ .host_key = domain,
+ .top_frame_site_key = statement.ColumnString(2),
+ .path = statement.ColumnString(5),
+ .source_scheme = statement.ColumnInt(15),
+ .source_port = statement.ColumnInt(16),
+ .has_cross_site_ancestor = statement.ColumnBool(19),
+ });
}
}
diff --git a/net/cookies/canonical_cookie_unittest.cc b/net/cookies/canonical_cookie_unittest.cc
index fa69297..65a5bd1 100644
--- a/net/cookies/canonical_cookie_unittest.cc
+++ b/net/cookies/canonical_cookie_unittest.cc
@@ -579,6 +579,76 @@
}
}
+TEST(CanonicalCookieTest, CreateSanitizedCookieRejectEmptyNameAmbiguous) {
+ CookieInclusionStatus status;
+ std::unique_ptr<CanonicalCookie> cc;
+ GURL url("https://www.example.com");
+ base::Time now = base::Time::Now();
+
+ constexpr std::string_view kAmbiguousValues[] = {
+ "=__Host-session=evil",
+ "foo=bar",
+ "session=123",
+ "a=",
+ };
+
+ // With the feature explicitly enabled, setting a nameless cookie with an
+ // ambiguous value (contains '=') should fail for CreateSanitizedCookie and
+ // FromStorage.
+ {
+ base::test::ScopedFeatureList features;
+ features.InitAndEnableFeature(
+ features::kCookieParseRejectEmptyNameAmbiguous);
+
+ for (std::string_view ambiguous_value : kAmbiguousValues) {
+ status = CookieInclusionStatus();
+ cc = CanonicalCookie::CreateSanitizedCookie(
+ url, "", ambiguous_value.data(), "", "/", base::Time(), base::Time(),
+ base::Time(), /*secure=*/true, /*http_only=*/false,
+ CookieSameSite::NO_RESTRICTION, COOKIE_PRIORITY_DEFAULT,
+ /*partition_key=*/std::nullopt, &status);
+ EXPECT_FALSE(cc);
+ EXPECT_TRUE(
+ status.HasExclusionReason(CookieInclusionStatus::ExclusionReason::
+ EXCLUDE_AMBIGUOUS_SERIALIZATION));
+
+ cc = CanonicalCookie::FromStorage(
+ "", ambiguous_value.data(), "example.com", "/", now,
+ now + base::Hours(1), now, now, /*secure=*/true, /*httponly=*/false,
+ CookieSameSite::NO_RESTRICTION, COOKIE_PRIORITY_DEFAULT,
+ /*partition_key=*/std::nullopt, CookieSourceScheme::kSecure, 443,
+ CookieSourceType::kOther, CanonicalCookieFromStorageCallSite::kTests);
+ EXPECT_FALSE(cc);
+ }
+ }
+
+ // Now run both with the feature disabled.
+ {
+ base::test::ScopedFeatureList features;
+ features.InitAndDisableFeature(
+ features::kCookieParseRejectEmptyNameAmbiguous);
+
+ for (std::string_view ambiguous_value : kAmbiguousValues) {
+ status = CookieInclusionStatus();
+ cc = CanonicalCookie::CreateSanitizedCookie(
+ url, "", ambiguous_value.data(), "", "/", base::Time(), base::Time(),
+ base::Time(), /*secure=*/true, /*http_only=*/false,
+ CookieSameSite::NO_RESTRICTION, COOKIE_PRIORITY_DEFAULT,
+ /*partition_key=*/std::nullopt, &status);
+ EXPECT_TRUE(cc);
+ EXPECT_TRUE(status.IsInclude());
+
+ cc = CanonicalCookie::FromStorage(
+ "", ambiguous_value.data(), "example.com", "/", now,
+ now + base::Hours(1), now, now, /*secure=*/true, /*httponly=*/false,
+ CookieSameSite::NO_RESTRICTION, COOKIE_PRIORITY_DEFAULT,
+ /*partition_key=*/std::nullopt, CookieSourceScheme::kSecure, 443,
+ CookieSourceType::kOther, CanonicalCookieFromStorageCallSite::kTests);
+ EXPECT_TRUE(cc);
+ }
+ }
+}
+
// Test that a cookie string with an empty domain attribute generates a
// canonical host cookie.
TEST(CanonicalCookieTest, CreateHostCookieFromString) {
@@ -4507,10 +4577,18 @@
std::string(), base::Time(), base::Time(), base::Time(), false /*secure*/,
false /*httponly*/, CookieSameSite::NO_RESTRICTION,
COOKIE_PRIORITY_DEFAULT, std::nullopt /*partition_key*/, &status);
- EXPECT_TRUE(cc);
- std::vector<std::unique_ptr<CanonicalCookie>> cookies;
- cookies.push_back(std::move(cc));
- MatchCookieLineToVector("ambiguous=value", cookies);
+ if (base::FeatureList::IsEnabled(
+ features::kCookieParseRejectEmptyNameAmbiguous)) {
+ EXPECT_FALSE(cc);
+ EXPECT_TRUE(
+ status.HasExclusionReason(CookieInclusionStatus::ExclusionReason::
+ EXCLUDE_AMBIGUOUS_SERIALIZATION));
+ } else {
+ EXPECT_TRUE(cc);
+ std::vector<std::unique_ptr<CanonicalCookie>> cookies;
+ cookies.push_back(std::move(cc));
+ MatchCookieLineToVector("ambiguous=value", cookies);
+ }
// Check that name can't contain an equal sign ("ambiguous=name=value" should
// correctly be parsed as name: "ambiguous" and value "name=value", so
@@ -4638,8 +4716,16 @@
one_hour_from_now, one_hour_ago, true, false,
CookieSameSite::NO_RESTRICTION, CookiePriority::COOKIE_PRIORITY_DEFAULT,
std::nullopt /*partition_key*/, &status));
- EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
- {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX}));
+ if (base::FeatureList::IsEnabled(
+ features::kCookieParseRejectEmptyNameAmbiguous)) {
+ EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
+ {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX,
+ CookieInclusionStatus::ExclusionReason::
+ EXCLUDE_AMBIGUOUS_SERIALIZATION}));
+ } else {
+ EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
+ {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX}));
+ }
EXPECT_FALSE(CanonicalCookie::CreateSanitizedCookie(
GURL("https://www.foo.com"), "", "__Host-A", "", "/", two_hours_ago,
@@ -4654,8 +4740,16 @@
one_hour_from_now, one_hour_ago, true, false,
CookieSameSite::NO_RESTRICTION, CookiePriority::COOKIE_PRIORITY_DEFAULT,
std::nullopt /*partition_key*/, &status));
- EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
- {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX}));
+ if (base::FeatureList::IsEnabled(
+ features::kCookieParseRejectEmptyNameAmbiguous)) {
+ EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
+ {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX,
+ CookieInclusionStatus::ExclusionReason::
+ EXCLUDE_AMBIGUOUS_SERIALIZATION}));
+ } else {
+ EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
+ {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX}));
+ }
EXPECT_FALSE(CanonicalCookie::CreateSanitizedCookie(
GURL("https://www.foo.com"), "", "__Secure-A", "", "/", two_hours_ago,
diff --git a/net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc b/net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc
index fc1aa432..9c6aa55 100644
--- a/net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc
+++ b/net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc
@@ -812,9 +812,8 @@
{"google.izzle", "A=", "B", "/path"},
{"google.izzle", "C ", "D", "/path"},
- // A canonical cookie for same eTLD+1. This one will get
- // dropped out of precaution to avoid confusing the site,
- // even though there is nothing wrong with it.
+ // A canonical cookie for same eTLD+1. This one will be
+ // preserved while non-canonical cookies are purged.
{"sub.google.izzle", "E", "F", "/path"},
// A canonical cookie for another eTLD+1
@@ -841,19 +840,36 @@
stmt.Clear();
db.reset();
- // Reopen the store and confirm that the only cookie loaded is the
- // canonical one on an unrelated domain.
+ // Reopen the store and confirm that valid cookies (including sibling domain
+ // sub.google.izzle) are preserved while only the invalid non-canonical
+ // cookies are purged.
CanonicalCookieVector cookies = CreateAndLoad(
/*crypt_cookies=*/false, /*restore_old_session_cookies=*/false);
- ASSERT_EQ(1U, cookies.size());
- EXPECT_STREQ("chromium.org", cookies[0]->Domain().c_str());
- EXPECT_STREQ("G", cookies[0]->Name().c_str());
- EXPECT_STREQ("H", cookies[0]->Value().c_str());
- EXPECT_STREQ("/dir", cookies[0]->Path().c_str());
- EXPECT_EQ(last_update, cookies[0]->LastUpdateDate());
+ ASSERT_EQ(2U, cookies.size());
+
+ // Find sub.google.izzle and chromium.org cookies.
+ const CanonicalCookie* sub_cookie = nullptr;
+ const CanonicalCookie* chrom_cookie = nullptr;
+ for (const auto& cookie : cookies) {
+ if (cookie->Domain() == "sub.google.izzle") {
+ sub_cookie = cookie.get();
+ } else if (cookie->Domain() == "chromium.org") {
+ chrom_cookie = cookie.get();
+ }
+ }
+ ASSERT_TRUE(sub_cookie);
+ ASSERT_TRUE(chrom_cookie);
... (file diff truncated)
Regression Test / PoC
diff --git a/net/cookies/canonical_cookie_unittest.cc b/net/cookies/canonical_cookie_unittest.cc
index fa69297..65a5bd1 100644
--- a/net/cookies/canonical_cookie_unittest.cc
+++ b/net/cookies/canonical_cookie_unittest.cc
@@ -579,6 +579,76 @@
}
}
+TEST(CanonicalCookieTest, CreateSanitizedCookieRejectEmptyNameAmbiguous) {
+ CookieInclusionStatus status;
+ std::unique_ptr<CanonicalCookie> cc;
+ GURL url("https://www.example.com");
+ base::Time now = base::Time::Now();
+
+ constexpr std::string_view kAmbiguousValues[] = {
+ "=__Host-session=evil",
+ "foo=bar",
+ "session=123",
+ "a=",
+ };
+
+ // With the feature explicitly enabled, setting a nameless cookie with an
+ // ambiguous value (contains '=') should fail for CreateSanitizedCookie and
+ // FromStorage.
+ {
+ base::test::ScopedFeatureList features;
+ features.InitAndEnableFeature(
+ features::kCookieParseRejectEmptyNameAmbiguous);
+
+ for (std::string_view ambiguous_value : kAmbiguousValues) {
+ status = CookieInclusionStatus();
+ cc = CanonicalCookie::CreateSanitizedCookie(
+ url, "", ambiguous_value.data(), "", "/", base::Time(), base::Time(),
+ base::Time(), /*secure=*/true, /*http_only=*/false,
+ CookieSameSite::NO_RESTRICTION, COOKIE_PRIORITY_DEFAULT,
+ /*partition_key=*/std::nullopt, &status);
+ EXPECT_FALSE(cc);
+ EXPECT_TRUE(
+ status.HasExclusionReason(CookieInclusionStatus::ExclusionReason::
+ EXCLUDE_AMBIGUOUS_SERIALIZATION));
+
+ cc = CanonicalCookie::FromStorage(
+ "", ambiguous_value.data(), "example.com", "/", now,
+ now + base::Hours(1), now, now, /*secure=*/true, /*httponly=*/false,
+ CookieSameSite::NO_RESTRICTION, COOKIE_PRIORITY_DEFAULT,
+ /*partition_key=*/std::nullopt, CookieSourceScheme::kSecure, 443,
+ CookieSourceType::kOther, CanonicalCookieFromStorageCallSite::kTests);
+ EXPECT_FALSE(cc);
+ }
+ }
+
+ // Now run both with the feature disabled.
+ {
+ base::test::ScopedFeatureList features;
+ features.InitAndDisableFeature(
+ features::kCookieParseRejectEmptyNameAmbiguous);
+
+ for (std::string_view ambiguous_value : kAmbiguousValues) {
+ status = CookieInclusionStatus();
+ cc = CanonicalCookie::CreateSanitizedCookie(
+ url, "", ambiguous_value.data(), "", "/", base::Time(), base::Time(),
+ base::Time(), /*secure=*/true, /*http_only=*/false,
+ CookieSameSite::NO_RESTRICTION, COOKIE_PRIORITY_DEFAULT,
+ /*partition_key=*/std::nullopt, &status);
+ EXPECT_TRUE(cc);
+ EXPECT_TRUE(status.IsInclude());
+
+ cc = CanonicalCookie::FromStorage(
+ "", ambiguous_value.data(), "example.com", "/", now,
+ now + base::Hours(1), now, now, /*secure=*/true, /*httponly=*/false,
+ CookieSameSite::NO_RESTRICTION, COOKIE_PRIORITY_DEFAULT,
+ /*partition_key=*/std::nullopt, CookieSourceScheme::kSecure, 443,
+ CookieSourceType::kOther, CanonicalCookieFromStorageCallSite::kTests);
+ EXPECT_TRUE(cc);
+ }
+ }
+}
+
// Test that a cookie string with an empty domain attribute generates a
// canonical host cookie.
TEST(CanonicalCookieTest, CreateHostCookieFromString) {
@@ -4507,10 +4577,18 @@
std::string(), base::Time(), base::Time(), base::Time(), false /*secure*/,
false /*httponly*/, CookieSameSite::NO_RESTRICTION,
COOKIE_PRIORITY_DEFAULT, std::nullopt /*partition_key*/, &status);
- EXPECT_TRUE(cc);
- std::vector<std::unique_ptr<CanonicalCookie>> cookies;
- cookies.push_back(std::move(cc));
- MatchCookieLineToVector("ambiguous=value", cookies);
+ if (base::FeatureList::IsEnabled(
+ features::kCookieParseRejectEmptyNameAmbiguous)) {
+ EXPECT_FALSE(cc);
+ EXPECT_TRUE(
+ status.HasExclusionReason(CookieInclusionStatus::ExclusionReason::
+ EXCLUDE_AMBIGUOUS_SERIALIZATION));
+ } else {
+ EXPECT_TRUE(cc);
+ std::vector<std::unique_ptr<CanonicalCookie>> cookies;
+ cookies.push_back(std::move(cc));
+ MatchCookieLineToVector("ambiguous=value", cookies);
+ }
// Check that name can't contain an equal sign ("ambiguous=name=value" should
// correctly be parsed as name: "ambiguous" and value "name=value", so
@@ -4638,8 +4716,16 @@
one_hour_from_now, one_hour_ago, true, false,
CookieSameSite::NO_RESTRICTION, CookiePriority::COOKIE_PRIORITY_DEFAULT,
std::nullopt /*partition_key*/, &status));
- EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
- {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX}));
+ if (base::FeatureList::IsEnabled(
+ features::kCookieParseRejectEmptyNameAmbiguous)) {
+ EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
+ {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX,
+ CookieInclusionStatus::ExclusionReason::
+ EXCLUDE_AMBIGUOUS_SERIALIZATION}));
+ } else {
+ EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
+ {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX}));
+ }
EXPECT_FALSE(CanonicalCookie::CreateSanitizedCookie(
GURL("https://www.foo.com"), "", "__Host-A", "", "/", two_hours_ago,
@@ -4654,8 +4740,16 @@
one_hour_from_now, one_hour_ago, true, false,
CookieSameSite::NO_RESTRICTION, CookiePriority::COOKIE_PRIORITY_DEFAULT,
std::nullopt /*partition_key*/, &status));
- EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
- {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX}));
+ if (base::FeatureList::IsEnabled(
+ features::kCookieParseRejectEmptyNameAmbiguous)) {
+ EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
+ {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX,
+ CookieInclusionStatus::ExclusionReason::
+ EXCLUDE_AMBIGUOUS_SERIALIZATION}));
+ } else {
+ EXPECT_TRUE(status.HasExactlyExclusionReasonsForTesting(
+ {CookieInclusionStatus::ExclusionReason::EXCLUDE_INVALID_PREFIX}));
+ }
EXPECT_FALSE(CanonicalCookie::CreateSanitizedCookie(
GURL("https://www.foo.com"), "", "__Secure-A", "", "/", two_hours_ago,
diff --git a/net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc b/net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc
index fc1aa432..9c6aa55 100644
--- a/net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc
+++ b/net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc
@@ -812,9 +812,8 @@
{"google.izzle", "A=", "B", "/path"},
{"google.izzle", "C ", "D", "/path"},
- // A canonical cookie for same eTLD+1. This one will get
- // dropped out of precaution to avoid confusing the site,
- // even though there is nothing wrong with it.
+ // A canonical cookie for same eTLD+1. This one will be
+ // preserved while non-canonical cookies are purged.
{"sub.google.izzle", "E", "F", "/path"},
// A canonical cookie for another eTLD+1
@@ -841,19 +840,36 @@
stmt.Clear();
db.reset();
- // Reopen the store and confirm that the only cookie loaded is the
- // canonical one on an unrelated domain.
+ // Reopen the store and confirm that valid cookies (including sibling domain
+ // sub.google.izzle) are preserved while only the invalid non-canonical
+ // cookies are purged.
CanonicalCookieVector cookies = CreateAndLoad(
/*crypt_cookies=*/false, /*restore_old_session_cookies=*/false);
- ASSERT_EQ(1U, cookies.size());
- EXPECT_STREQ("chromium.org", cookies[0]->Domain().c_str());
- EXPECT_STREQ("G", cookies[0]->Name().c_str());
- EXPECT_STREQ("H", cookies[0]->Value().c_str());
- EXPECT_STREQ("/dir", cookies[0]->Path().c_str());
- EXPECT_EQ(last_update, cookies[0]->LastUpdateDate());
+ ASSERT_EQ(2U, cookies.size());
+
+ // Find sub.google.izzle and chromium.org cookies.
+ const CanonicalCookie* sub_cookie = nullptr;
+ const CanonicalCookie* chrom_cookie = nullptr;
+ for (const auto& cookie : cookies) {
+ if (cookie->Domain() == "sub.google.izzle") {
+ sub_cookie = cookie.get();
+ } else if (cookie->Domain() == "chromium.org") {
+ chrom_cookie = cookie.get();
+ }
+ }
+ ASSERT_TRUE(sub_cookie);
+ ASSERT_TRUE(chrom_cookie);
+
+ EXPECT_STREQ("sub.google.izzle", sub_cookie->Domain().c_str());
+ EXPECT_STREQ("E", sub_cookie->Name().c_str());
+ EXPECT_STREQ("F", sub_cookie->Value().c_str());
+
+ EXPECT_STREQ("chromium.org", chrom_cookie->Domain().c_str());
+ EXPECT_STREQ("G", chrom_cookie->Name().c_str());
+ EXPECT_STREQ("H", chrom_cookie->Value().c_str());
DestroyStore();
- // Make sure that we only have one row left.
+ // Make sure that we only have two rows left in the database.
db = std::make_unique<sql::Database>(sql::test::kTestTag);
ASSERT_TRUE(db->Open(store_name));
sql::Statement verify_stmt(db->GetUniqueStatement("SELECT * FROM COOKIES"));
@@ -861,7 +877,83 @@
EXPECT_TRUE(verify_stmt.Step());
EXPECT_TRUE(verify_stmt.Succeeded());
- // Confirm only one match.
+ EXPECT_TRUE(verify_stmt.Step());
+ EXPECT_TRUE(verify_stmt.Succeeded());
+ // Confirm only two matches.
+ EXPECT_FALSE(verify_stmt.Step());
+}
+
+TEST_F(SQLitePersistentCookieStoreTest,
+ TestAmbiguousNamelessCookieSingleDeletion) {
+ base::test::ScopedFeatureList features;
+ features.InitAndEnableFeature(
+ net::features::kCookieParseRejectEmptyNameAmbiguous);
+
+ CreateAndLoad(/*crypt_cookies=*/false, /*restore_old_session_cookies=*/false);
+ DestroyStore();
+
+ // Insert a valid cookie and a pre-existing ambiguous nameless cookie for
+ // example.com.
+ base::FilePath store_name(temp_dir_.GetPath().Append(kCookieFilename));
+ std::unique_ptr<sql::Database> db(
+ std::make_unique<sql::Database>(sql::test::kTestTag));
+ ASSERT_TRUE(db->Open(store_name));
+ sql::Statement stmt(db->GetUniqueStatement(
+ "INSERT INTO cookies (creation_utc, host_key, top_frame_site_key, name, "
+ "value, encrypted_value, path, expires_utc, is_secure, is_httponly, "
+ "samesite, last_access_utc, has_expires, is_persistent, priority, "
+ "source_scheme, source_port, last_update_utc, source_type, "
+ "has_cross_site_ancestor) "
+ "VALUES (?,?,?,?,?,'',?,0,0,0,0,0,1,1,0,?,?,?,0,0)"));
+ ASSERT_TRUE(stmt.is_valid());
+
+ struct CookieInfo {
+ const char* domain;
+ const char* name;
+ const char* value;
+ const char* path;
+ } cookies_info[] = {
+ {"example.com", "", "=__Host-session=evil", "/"},
+ {"example.com", "sid", "123", "/"},
+ };
+
+ int64_t creation_time = 1;
+ base::Time last_update(base::Time::Now());
+ for (auto& cookie_info : cookies_info) {
+ stmt.Reset(true);
+ stmt.BindInt64(0, creation_time++);
+ stmt.BindString(1, cookie_info.domain);
+ stmt.BindString(2, net::kEmptyCookiePartitionKey);
+ stmt.BindString(3, cookie_info.name);
+ stmt.BindString(4, cookie_info.value);
+ stmt.BindString(5, cookie_info.path);
+ stmt.BindInt(6, static_cast<int>(CookieSourceScheme::kUnset));
+ stmt.BindInt(7, SQLitePersistentCookieStore::kDefaultUnknownPort);
+ stmt.BindTime(8, last_update);
+ ASSERT_TRUE(stmt.Run());
+ }
+ stmt.Clear();
+ db.reset();
+
+ // Load store and verify that only the valid "sid" cookie is returned, while
+ // the ambiguous nameless cookie is discarded without dropping valid sibling
+ // cookies.
+ CanonicalCookieVector cookies = CreateAndLoad(
+ /*crypt_cookies=*/false, /*restore_old_session_cookies=*/false);
+ ASSERT_EQ(1U, cookies.size());
+ EXPECT_EQ("sid", cookies[0]->Name());
+ EXPECT_EQ("123", cookies[0]->Value());
+ DestroyStore();
+
+ // Verify only the valid cookie remains in DB.
+ db = std::make_unique<sql::Database>(sql::test::kTestTag);
+ ASSERT_TRUE(db->Open(store_name));
+ sql::Statement verify_stmt(
+ db->GetUniqueStatement("SELECT name, value FROM cookies"));
+ ASSERT_TRUE(verify_stmt.is_valid());
+ EXPECT_TRUE(verify_stmt.Step());
+ EXPECT_EQ("sid", verify_stmt.ColumnString(0));
+ EXPECT_EQ("123", verify_stmt.ColumnString(1));
EXPECT_FALSE(verify_stmt.Step());
}
diff --git a/services/network/restricted_cookie_manager_unittest.cc b/services/network/restricted_cookie_manager_unittest.cc
index 0dd3fb4..a2a7ee9 100644
--- a/services/network/restricted_cookie_manager_unittest.cc
+++ b/services/network/restricted_cookie_manager_unittest.cc
@@ -1387,6 +1387,79 @@
ASSERT_TRUE(received_bad_message());
}
+TEST_P(RestrictedCookieManagerTest,
+ SetCanonicalCookieRejectEmptyNameAmbiguous) {
+ base::test::ScopedFeatureList features;
+ features.InitAndEnableFeature(
... (truncated)