Low watch CVSS 6.5 chrome Input Validation 🔧 Commit mapped

Overview

Low
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Cookies
DescriptionImproper input validation in Cookies
ComponentCookies
Sub-componentNet/cookies
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker533511967
Fix commitbbf5240096c5 (chromium/src) +362/-26
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-10 (per vendor advisory)
Fix landed2026-07-23 (public git)
Disclosed2026-08-25
Patch gap33 days — fix public in source before the advisory

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=bar can be ambiguously reparsed as name foo, value bar by 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 CookieInclusionStatus exclusion reason that rejects a nameless cookie whose value contains =, because such a cookie serializes ambiguously; it is gated on the kCookieParseRejectEmptyNameAmbiguous feature.
Creation surfaces
Cookies can be created via standard parsing (CanonicalCookie::Create), the Mojo-facing CanonicalCookie::CreateSanitizedCookie path, and are re-validated on load by IsCanonicalForFromStorage.

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.

Key insight
The core mistake was enforcing the nameless-cookie = 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

  1. 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.
  2. Use an unvalidated creation surface The cookie is set through the Mojo-facing CreateSanitizedCookie path (or loaded from storage and validated by IsCanonicalForFromStorage), which pre-patch did not apply the EXCLUDE_AMBIGUOUS_SERIALIZATION check and thus accepted it.
  3. Cookie is stored/sent ambiguously Because the cookie serializes with no name, it is transmitted as just its value.
  4. 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

An attacker able to set cookies via the sanitized/Mojo creation path (or seed storage) could persist a nameless cookie whose value contains = 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

FunctionChangeNotes
if
net/extras/sqlite/sqlite_persistent_cookie_store.cc
modified
for
net/extras/sqlite/sqlite_persistent_cookie_store.cc
modified
TEST
net/cookies/canonical_cookie_unittest.cc
modified
for
net/cookies/canonical_cookie_unittest.cc
modified
for
net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc
modified

Files Changed

  • net/extras/sqlite/sqlite_persistent_cookie_store_unittest.cc
  • net/cookies/canonical_cookie_unittest.cc
  • services/network/restricted_cookie_manager_unittest.cc
  • net/extras/sqlite/sqlite_persistent_cookie_store.cc
  • net/cookies/canonical_cookie.cc
  • net/cookies/canonical_cookie.h

Audit Directions

  • Validation only on the primary parse path
    Look 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 values
    Audit 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 semantics
    Review 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.
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)
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.