Bug 2026913 - Clean up nameless cookies from DB r=baku,cookie-reviewers
authorValentin Gosu <valentin.gosu@gmail.com>
Tue, 28 Apr 2026 07:29:26 +0000
changeset 845401 01a837103137ea893c7583aba08d61a0a4345323
parent 845400 9b08fb675c7eaa5a3d9226f995a49d644cfe5f88
child 845402 02c52f1578d2da4f0c4e9d886e6e5e170c4c4d1f
push id44422
push usersstanca@mozilla.com
push dateTue, 28 Apr 2026 21:44:21 +0000
treeherdermozilla-central@499b89bd5492 [default view] [failures only]
git commit991fe75b38e2aeafb9844fc995b265268687a730
reviewersbaku, cookie-reviewers
bugs2026913
milestone152.0a1
first release with
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 2026913 - Clean up nameless cookies from DB r=baku,cookie-reviewers Differential Revision: https://phabricator.services.mozilla.com/D294370
netwerk/cookie/CookiePersistentStorage.cpp
netwerk/cookie/test/unit/test_valueless_cookie_migration.js
netwerk/cookie/test/unit/xpcshell.toml
--- a/netwerk/cookie/CookiePersistentStorage.cpp
+++ b/netwerk/cookie/CookiePersistentStorage.cpp
@@ -1999,16 +1999,29 @@ CookiePersistentStorage::OpenDBResult Co
            host.get()));
       CookieDomainTuple* cleanupTuple = mCleanupArray.AppendElement();
       cleanupTuple->key = CookieKey(baseDomain, attrs);
       cleanupTuple->originAttributes = attrs;
       cleanupTuple->cookie = Cookie::Create(*cookieStruct, attrs);
       continue;
     }
 
+    // When the valueless_cookie pref is enabled, discard legacy nameless
+    // cookies (name="") that were stored under the old behavior.
+    // Under the new behavior these cookies are unreachable since new cookies
+    // without '=' are stored with the token as name, not value.
+    if (StaticPrefs::network_cookie_valueless_cookie() &&
+        cookieStruct->name().IsEmpty()) {
+      CookieDomainTuple* cleanupTuple = mCleanupArray.AppendElement();
+      cleanupTuple->key = CookieKey(baseDomain, attrs);
+      cleanupTuple->originAttributes = attrs;
+      cleanupTuple->cookie = Cookie::Create(*cookieStruct, attrs);
+      continue;
+    }
+
     // Create the Cookie on the background thread. CreateValidated fixes up
     // any timestamps in the DB that are far in the future.
     RefPtr<Cookie> cookie = Cookie::CreateValidated(*cookieStruct, attrs);
 
     // Clean up invalid first-party partitioned cookies without the
     // 'Partitioned' attribute. Use Cookie::Create to preserve the original DB
     // timestamps for the deletion query.
     if (CookieCommons::IsFirstPartyPartitionedCookieWithoutCHIPS(
new file mode 100644
--- /dev/null
+++ b/netwerk/cookie/test/unit/test_valueless_cookie_migration.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+  Tests that when network.cookie.valueless_cookie=true (the default), legacy
+  nameless cookies (name="", value!="") that were stored on disk under the old
+  behavior are discarded on load rather than surfacing with an unreachable key.
+*/
+
+const { TestUtils } = ChromeUtils.importESModule(
+  "resource://testing-common/TestUtils.sys.mjs"
+);
+
+const USEC_PER_MSEC = 1000;
+const ONE_DAY_IN_USEC = 60 * 60 * 24 * USEC_PER_MSEC * 1000;
+
+function initDBWithNamelessCookie(conn, now) {
+  conn.schemaVersion = 7;
+  conn.executeSimpleSQL(
+    "CREATE TABLE moz_cookies (" +
+      "id INTEGER PRIMARY KEY, " +
+      "baseDomain TEXT, " +
+      "originAttributes TEXT NOT NULL DEFAULT '', " +
+      "name TEXT, " +
+      "value TEXT, " +
+      "host TEXT, " +
+      "path TEXT, " +
+      "expiry INTEGER, " +
+      "lastAccessed INTEGER, " +
+      "creationTime INTEGER, " +
+      "isSecure INTEGER, " +
+      "isHttpOnly INTEGER, " +
+      "appId INTEGER DEFAULT 0, " +
+      "inBrowserElement INTEGER DEFAULT 0, " +
+      "CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)" +
+      ")"
+  );
+  conn.executeSimpleSQL(
+    "CREATE INDEX moz_basedomain ON moz_cookies (baseDomain, originAttributes)"
+  );
+  conn.executeSimpleSQL("PRAGMA synchronous = OFF");
+  conn.executeSimpleSQL("PRAGMA journal_mode = WAL");
+  conn.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
+
+  // A legacy nameless cookie: name="", value="nameless" (created by old
+  // behavior where `Set-Cookie: foo` produced name="" value="foo").
+  conn.executeSimpleSQL(
+    `INSERT INTO moz_cookies(baseDomain, host, name, value, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly)
+    VALUES ('example.com', 'example.com', '', 'nameless', '/',
+    ${now + ONE_DAY_IN_USEC}, ${now}, ${now - ONE_DAY_IN_USEC}, 0, 0)`
+  );
+
+  // A normal cookie with a name, which must always be retained.
+  conn.executeSimpleSQL(
+    `INSERT INTO moz_cookies(baseDomain, host, name, value, path, expiry, lastAccessed, creationTime, isSecure, isHttpOnly)
+    VALUES ('example.com', 'example.com', 'regular', 'cookie', '/',
+    ${now + ONE_DAY_IN_USEC}, ${now}, ${now - ONE_DAY_IN_USEC}, 0, 0)`
+  );
+}
+
+add_task(async function test_nameless_cookie_discarded_when_pref_enabled() {
+  let now = Date.now() * USEC_PER_MSEC;
+  Services.prefs.setBoolPref("network.cookie.valueless_cookie", true);
+  Services.prefs.setIntPref("network.cookie.cookieBehavior", 0);
+  Services.prefs.setBoolPref(
+    "network.cookieJarSettings.unblocked_for_testing",
+    true
+  );
+  do_get_profile();
+
+  let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+  dbFile.append("cookies.sqlite");
+  let conn = Services.storage.openDatabase(dbFile);
+  initDBWithNamelessCookie(conn, now);
+  conn.close();
+
+  // Arm the observer before triggering init so we don't miss the notification.
+  let dbReadPromise = TestUtils.topicObserved("cookie-db-read");
+
+  // Trigger cookie service initialization by accessing cookies.
+  let { cookies } = Services.cookies;
+
+  let exampleCookies = cookies.filter(c => c.host === "example.com");
+  Assert.equal(
+    exampleCookies.length,
+    1,
+    "Only one cookie should be loaded (nameless one discarded)"
+  );
+  Assert.equal(
+    exampleCookies[0].name,
+    "regular",
+    "The remaining cookie has a name"
+  );
+  Assert.equal(
+    exampleCookies[0].value,
+    "cookie",
+    "The remaining cookie has the right value"
+  );
+
+  // Wait for InitDBConn to complete; RemoveCookieFromDB is dispatched inside it.
+  await dbReadPromise;
+
+  // RemoveCookieFromDB uses an async statement, so poll until the row is gone.
+  await TestUtils.waitForCondition(() => {
+    let checkConn = Services.storage.openDatabase(dbFile);
+    try {
+      let stmt = checkConn.createStatement(
+        "SELECT COUNT(*) FROM moz_cookies WHERE name = '' AND host = 'example.com'"
+      );
+      try {
+        stmt.executeStep();
+        return stmt.getInt32(0) === 0;
+      } finally {
+        stmt.finalize();
+      }
+    } finally {
+      checkConn.close();
+    }
+  }, "Nameless cookie should be removed from the SQLite database");
+});
--- a/netwerk/cookie/test/unit/xpcshell.toml
+++ b/netwerk/cookie/test/unit/xpcshell.toml
@@ -22,8 +22,10 @@ prefs = ["dom.security.https_first=false
 
 ["test_parser_0001.js"]
 
 ["test_parser_0019.js"]
 
 ["test_schemeMap.js"]
 
 ["test_timestamp_fixup.js"]
+
+["test_valueless_cookie_migration.js"]