Bug 1873412 - Storing partitioned attribute in cookie db. r=bvandersloot,cookie-reviewers,valentin

Differential Revision: https://phabricator.services.mozilla.com/D198984
This commit is contained in:
Leander Schwarz 2024-02-08 13:46:03 +00:00
parent 872ee7e60c
commit 6cd98ee53a
12 changed files with 483 additions and 73 deletions

View File

@ -90,6 +90,7 @@ class Cookie final : public nsICookie {
inline bool IsDomain() const { return *mData.host().get() == '.'; }
inline bool IsSecure() const { return mData.isSecure(); }
inline bool IsHttpOnly() const { return mData.isHttpOnly(); }
inline bool RawIsPartitioned() const { return mData.isPartitioned(); }
inline const OriginAttributes& OriginAttributesRef() const {
return mOriginAttributes;
}

View File

@ -31,7 +31,7 @@
// This is a hack to hide HttpOnly cookies from older browsers
#define HTTP_ONLY_PREFIX "#HttpOnly_"
constexpr auto COOKIES_SCHEMA_VERSION = 12;
constexpr auto COOKIES_SCHEMA_VERSION = 13;
// parameter indexes; see |Read|
constexpr auto IDX_NAME = 0;
@ -47,6 +47,7 @@ constexpr auto IDX_ORIGIN_ATTRIBUTES = 9;
constexpr auto IDX_SAME_SITE = 10;
constexpr auto IDX_RAW_SAME_SITE = 11;
constexpr auto IDX_SCHEME_MAP = 12;
constexpr auto IDX_PARTITIONED_ATTRIBUTE_SET = 13;
#define COOKIES_FILE "cookies.sqlite"
@ -109,6 +110,10 @@ void BindCookieParameters(mozIStorageBindingParamsArray* aParamsArray,
rv = params->BindInt32ByName("schemeMap"_ns, aCookie->SchemeMap());
MOZ_ASSERT(NS_SUCCEEDED(rv));
rv = params->BindInt32ByName("isPartitionedAttributeSet"_ns,
aCookie->RawIsPartitioned());
MOZ_ASSERT(NS_SUCCEEDED(rv));
// Bind the params to the array.
rv = aParamsArray->AddParams(params);
MOZ_ASSERT(NS_SUCCEEDED(rv));
@ -1397,6 +1402,23 @@ CookiePersistentStorage::OpenDBResult CookiePersistentStorage::TryInitDB(
}
[[fallthrough]];
case 12: {
// Add the isPartitionedAttributeSet column to the table.
rv = mSyncConn->ExecuteSimpleSQL(
nsLiteralCString("ALTER TABLE moz_cookies ADD "
"isPartitionedAttributeSet INTEGER DEFAULT 0;"));
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
COOKIE_LOGSTRING(LogLevel::Debug,
("Upgraded database to schema version 13"));
// No more upgrades. Update the schema version.
rv = mSyncConn->SetSchemaVersion(COOKIES_SCHEMA_VERSION);
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
[[fallthrough]];
}
case COOKIES_SCHEMA_VERSION:
break;
@ -1423,23 +1445,25 @@ CookiePersistentStorage::OpenDBResult CookiePersistentStorage::TryInitDB(
default: {
// check if all the expected columns exist
nsCOMPtr<mozIStorageStatement> stmt;
rv = mSyncConn->CreateStatement(nsLiteralCString("SELECT "
"id, "
"originAttributes, "
"name, "
"value, "
"host, "
"path, "
"expiry, "
"lastAccessed, "
"creationTime, "
"isSecure, "
"isHttpOnly, "
"sameSite, "
"rawSameSite, "
"schemeMap "
"FROM moz_cookies"),
getter_AddRefs(stmt));
rv = mSyncConn->CreateStatement(
nsLiteralCString("SELECT "
"id, "
"originAttributes, "
"name, "
"value, "
"host, "
"path, "
"expiry, "
"lastAccessed, "
"creationTime, "
"isSecure, "
"isHttpOnly, "
"sameSite, "
"rawSameSite, "
"schemeMap, "
"isPartitionedAttributeSet "
"FROM moz_cookies"),
getter_AddRefs(stmt));
if (NS_SUCCEEDED(rv)) {
break;
}
@ -1597,22 +1621,24 @@ CookiePersistentStorage::OpenDBResult CookiePersistentStorage::Read() {
// Read in the data synchronously.
// see IDX_NAME, etc. for parameter indexes
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mSyncConn->CreateStatement(nsLiteralCString("SELECT "
"name, "
"value, "
"host, "
"path, "
"expiry, "
"lastAccessed, "
"creationTime, "
"isSecure, "
"isHttpOnly, "
"originAttributes, "
"sameSite, "
"rawSameSite, "
"schemeMap "
"FROM moz_cookies"),
getter_AddRefs(stmt));
nsresult rv =
mSyncConn->CreateStatement(nsLiteralCString("SELECT "
"name, "
"value, "
"host, "
"path, "
"expiry, "
"lastAccessed, "
"creationTime, "
"isSecure, "
"isHttpOnly, "
"originAttributes, "
"sameSite, "
"rawSameSite, "
"schemeMap, "
"isPartitionedAttributeSet "
"FROM moz_cookies"),
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, RESULT_RETRY);
@ -1691,11 +1717,13 @@ UniquePtr<CookieStruct> CookiePersistentStorage::GetCookieFromRow(
int32_t sameSite = aRow->AsInt32(IDX_SAME_SITE);
int32_t rawSameSite = aRow->AsInt32(IDX_RAW_SAME_SITE);
int32_t schemeMap = aRow->AsInt32(IDX_SCHEME_MAP);
bool isPartitionedAttributeSet =
0 != aRow->AsInt32(IDX_PARTITIONED_ATTRIBUTE_SET);
// Create a new constCookie and assign the data.
return MakeUnique<CookieStruct>(
name, value, host, path, expiry, lastAccessed, creationTime, isHttpOnly,
false, isSecure, false, sameSite, rawSameSite,
false, isSecure, isPartitionedAttributeSet, sameSite, rawSameSite,
static_cast<nsICookie::schemeType>(schemeMap));
}
@ -1854,37 +1882,39 @@ nsresult CookiePersistentStorage::InitDBConnInternal() {
mDBConn->ExecuteSimpleSQL("PRAGMA wal_autocheckpoint = 16"_ns);
// cache frequently used statements (for insertion, deletion, and updating)
rv =
mDBConn->CreateAsyncStatement(nsLiteralCString("INSERT INTO moz_cookies ("
"originAttributes, "
"name, "
"value, "
"host, "
"path, "
"expiry, "
"lastAccessed, "
"creationTime, "
"isSecure, "
"isHttpOnly, "
"sameSite, "
"rawSameSite, "
"schemeMap "
") VALUES ("
":originAttributes, "
":name, "
":value, "
":host, "
":path, "
":expiry, "
":lastAccessed, "
":creationTime, "
":isSecure, "
":isHttpOnly, "
":sameSite, "
":rawSameSite, "
":schemeMap "
")"),
getter_AddRefs(mStmtInsert));
rv = mDBConn->CreateAsyncStatement(
nsLiteralCString("INSERT INTO moz_cookies ("
"originAttributes, "
"name, "
"value, "
"host, "
"path, "
"expiry, "
"lastAccessed, "
"creationTime, "
"isSecure, "
"isHttpOnly, "
"sameSite, "
"rawSameSite, "
"schemeMap, "
"isPartitionedAttributeSet "
") VALUES ("
":originAttributes, "
":name, "
":value, "
":host, "
":path, "
":expiry, "
":lastAccessed, "
":creationTime, "
":isSecure, "
":isHttpOnly, "
":sameSite, "
":rawSameSite, "
":schemeMap, "
":isPartitionedAttributeSet "
")"),
getter_AddRefs(mStmtInsert));
NS_ENSURE_SUCCESS(rv, rv);
rv = mDBConn->CreateAsyncStatement(
@ -1927,6 +1957,7 @@ nsresult CookiePersistentStorage::CreateTableWorker(const char* aName) {
"sameSite INTEGER DEFAULT 0, "
"rawSameSite INTEGER DEFAULT 0, "
"schemeMap INTEGER DEFAULT 0, "
"isPartitionedAttributeSet INTEGER DEFAULT 0, "
"CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)"
")");
return mSyncConn->ExecuteSimpleSQL(command);

View File

@ -59,7 +59,7 @@ conn.executeSimpleSQL(
// Get sessionCookies to wait for the initialization in cookie thread
Services.cookies.sessionCookies;
Assert.equal(conn.schemaVersion, 12);
Assert.equal(conn.schemaVersion, 13);
let stmt = conn.createStatement(
"SELECT sql FROM sqlite_master " +
"WHERE type = 'table' AND " +

View File

@ -101,7 +101,7 @@ add_task(async _ => {
await promise;
conn = storage.openDatabase(dbFile);
Assert.equal(conn.schemaVersion, 12);
Assert.equal(conn.schemaVersion, 13);
let stmt = conn.createStatement(
"SELECT sameSite, rawSameSite FROM moz_cookies"

View File

@ -71,7 +71,7 @@ add_task(async function test_timestamp_fixup() {
Math.floor(Services.cookies.cookies[0].creationTime / 1000),
now
);
Assert.equal(conn.schemaVersion, 12);
Assert.equal(conn.schemaVersion, 13);
Assert.equal(
await Glean.networking.cookieTimestampFixedCount.creationTime.testGetValue(),

View File

@ -206,7 +206,8 @@ function Cookie(
originAttributes = {},
sameSite = Ci.nsICookie.SAMESITE_NONE,
rawSameSite = Ci.nsICookie.SAMESITE_NONE,
schemeMap = Ci.nsICookie.SCHEME_UNSET
schemeMap = Ci.nsICookie.SCHEME_UNSET,
isPartitioned = false
) {
this.name = name;
this.value = value;
@ -223,6 +224,7 @@ function Cookie(
this.sameSite = sameSite;
this.rawSameSite = rawSameSite;
this.schemeMap = schemeMap;
this.isPartitioned = isPartitioned;
let strippedHost = host.charAt(0) == "." ? host.slice(1) : host;
@ -688,6 +690,83 @@ function CookieDatabaseConnection(file, schema) {
break;
}
case 13: {
if (!exists) {
this.db.executeSimpleSQL(
"CREATE TABLE moz_cookies ( \
id INTEGER PRIMARY KEY, \
originAttributes TEXT NOT NULL DEFAULT '', \
name TEXT, \
value TEXT, \
host TEXT, \
path TEXT, \
expiry INTEGER, \
lastAccessed INTEGER, \
creationTime INTEGER, \
isSecure INTEGER, \
isHttpOnly INTEGER, \
inBrowserElement INTEGER DEFAULT 0, \
sameSite INTEGER DEFAULT 0, \
rawSameSite INTEGER DEFAULT 0, \
schemeMap INTEGER DEFAULT 0, \
isPartitionedAttributeSet INTEGER DEFAULT 0, \
CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes))"
);
this.db.executeSimpleSQL("PRAGMA journal_mode = WAL");
this.db.executeSimpleSQL("PRAGMA wal_autocheckpoint = 16");
}
this.stmtInsert = this.db.createStatement(
"INSERT INTO moz_cookies ( \
name, \
value, \
host, \
path, \
expiry, \
lastAccessed, \
creationTime, \
isSecure, \
isHttpOnly, \
inBrowserElement, \
originAttributes, \
sameSite, \
rawSameSite, \
schemeMap, \
isPartitionedAttributeSet \
) VALUES ( \
:name, \
:value, \
:host, \
:path, \
:expiry, \
:lastAccessed, \
:creationTime, \
:isSecure, \
:isHttpOnly, \
:inBrowserElement, \
:originAttributes, \
:sameSite, \
:rawSameSite, \
:schemeMap, \
:isPartitionedAttributeSet)"
);
this.stmtDelete = this.db.createStatement(
"DELETE FROM moz_cookies \
WHERE name = :name AND host = :host AND path = :path AND \
originAttributes = :originAttributes"
);
this.stmtUpdate = this.db.createStatement(
"UPDATE moz_cookies SET lastAccessed = :lastAccessed \
WHERE name = :name AND host = :host AND path = :path AND \
originAttributes = :originAttributes"
);
break;
}
default:
do_throw("unrecognized schemaVersion!");
}
@ -808,6 +887,30 @@ CookieDatabaseConnection.prototype = {
this.stmtInsert.bindByName("schemeMap", cookie.schemeMap);
break;
case 13:
this.stmtInsert.bindByName("name", cookie.name);
this.stmtInsert.bindByName("value", cookie.value);
this.stmtInsert.bindByName("host", cookie.host);
this.stmtInsert.bindByName("path", cookie.path);
this.stmtInsert.bindByName("expiry", cookie.expiry);
this.stmtInsert.bindByName("lastAccessed", cookie.lastAccessed);
this.stmtInsert.bindByName("creationTime", cookie.creationTime);
this.stmtInsert.bindByName("isSecure", cookie.isSecure);
this.stmtInsert.bindByName("isHttpOnly", cookie.isHttpOnly);
this.stmtInsert.bindByName("inBrowserElement", cookie.inBrowserElement);
this.stmtInsert.bindByName(
"originAttributes",
ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
);
this.stmtInsert.bindByName("sameSite", cookie.sameSite);
this.stmtInsert.bindByName("rawSameSite", cookie.rawSameSite);
this.stmtInsert.bindByName("schemeMap", cookie.schemeMap);
this.stmtInsert.bindByName(
"isPartitionedAttributeSet",
cookie.isPartitioned
);
break;
default:
do_throw("unrecognized schemaVersion!");
}
@ -836,6 +939,7 @@ CookieDatabaseConnection.prototype = {
case 10:
case 11:
case 12:
case 13:
this.stmtDelete.bindByName("name", cookie.name);
this.stmtDelete.bindByName("host", cookie.host);
this.stmtDelete.bindByName("path", cookie.path);
@ -880,6 +984,7 @@ CookieDatabaseConnection.prototype = {
case 10:
case 11:
case 12:
case 13:
this.stmtDelete.bindByName("name", cookie.name);
this.stmtDelete.bindByName("host", cookie.host);
this.stmtDelete.bindByName("path", cookie.path);

View File

@ -28,7 +28,7 @@ let now;
let futureExpiry;
let cookie;
var COOKIE_DATABASE_SCHEMA_CURRENT = 12;
var COOKIE_DATABASE_SCHEMA_CURRENT = 13;
var test_generator = do_run_test();

View File

@ -51,7 +51,7 @@ add_task(async function () {
);
// check for upgraded schema.
Assert.equal(12, getDBVersion(destFile));
Assert.equal(13, getDBVersion(destFile));
// Check that the index was deleted
Assert.ok(!indexExists(destFile, "moz_basedomain"));

View File

@ -111,7 +111,8 @@ add_task(async function test_migrate_invalid_cookie() {
Assert.ok(cookie1Exists, "Cookie 1 was inadvertently removed");
Assert.ok(cookie2Exists, "Cookie 2 was inadvertently removed");
Assert.ok(!badcookieExists, "Bad cookie was not filtered by migration");
Assert.equal(schema12db.db.schemaVersion, 12);
// Schema was upgraded by cookie service
Assert.equal(schema12db.db.schemaVersion, 13);
// reload to make sure removal was written correctly
await promise_close_profile();

View File

@ -0,0 +1,181 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test cookie database migration from version 12 to the current version,
// presently 13, which added the "partitioned" cookie attribute.
"use strict";
var test_generator = do_run_test();
function run_test() {
do_test_pending();
test_generator.next();
}
function finish_test() {
executeSoon(function () {
test_generator.return();
do_test_finished();
});
}
function* do_run_test() {
// Set up a profile.
let profile = do_get_profile();
// Start the cookieservice, to force creation of a database.
// Get the sessionCookies to join the initialization in cookie thread
Services.cookies.sessionCookies;
// Close the profile.
do_close_profile(test_generator);
yield;
// Remove the cookie file in order to create another database file.
do_get_cookie_file(profile).remove(false);
// Create a schema 12 database.
let schema12db = new CookieDatabaseConnection(
do_get_cookie_file(profile),
12
);
let now = Date.now() * 1000;
let futureExpiry = Math.round(now / 1e6 + 1000);
let pastExpiry = Math.round(now / 1e6 - 1000);
// Populate it, with:
// 1) Unexpired, unique cookies.
for (let i = 0; i < 20; ++i) {
let cookie = new Cookie(
"oh" + i,
"hai",
"foo.com",
"/",
futureExpiry,
now,
now,
false,
false,
false
);
schema12db.insertCookie(cookie);
}
// 2) Expired, unique cookies.
for (let i = 20; i < 40; ++i) {
let cookie = new Cookie(
"oh" + i,
"hai",
"bar.com",
"/",
pastExpiry,
now,
now,
false,
false,
false
);
schema12db.insertCookie(cookie);
}
// 3) Many copies of the same cookie, some of which have expired and
// some of which have not.
for (let i = 40; i < 45; ++i) {
let cookie = new Cookie(
"oh",
"hai",
"baz.com",
"/",
futureExpiry + i,
now,
now,
false,
false,
false
);
try {
schema12db.insertCookie(cookie);
} catch (e) {}
}
for (let i = 45; i < 50; ++i) {
let cookie = new Cookie(
"oh",
"hai",
"baz.com",
"/",
pastExpiry - i,
now,
now,
false,
false,
false
);
try {
schema12db.insertCookie(cookie);
} catch (e) {}
}
for (let i = 50; i < 55; ++i) {
let cookie = new Cookie(
"oh",
"hai",
"baz.com",
"/",
futureExpiry - i,
now,
now,
false,
false,
false
);
try {
schema12db.insertCookie(cookie);
} catch (e) {}
}
for (let i = 55; i < 60; ++i) {
let cookie = new Cookie(
"oh",
"hai",
"baz.com",
"/",
pastExpiry + i,
now,
now,
false,
false,
false
);
try {
schema12db.insertCookie(cookie);
} catch (e) {}
}
// Close it.
schema12db.close();
schema12db = null;
// Load the database, forcing migration to the current schema version. Then
// test the expected set of cookies:
do_load_profile();
// 1) All unexpired, unique cookies exist.
Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), 20);
// 2) All expired, unique cookies exist.
Assert.equal(Services.cookies.countCookiesFromHost("bar.com"), 20);
// 3) Only one cookie remains, and it's the one with the highest expiration
// time.
Assert.equal(Services.cookies.countCookiesFromHost("baz.com"), 1);
let cookies = Services.cookies.getCookiesFromHost("baz.com", {});
let cookie = cookies[0];
Assert.equal(cookie.expiry, futureExpiry + 40);
finish_test();
}

View File

@ -0,0 +1,87 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test cookie database schema 13
"use strict";
add_task(async function test_schema_13_db() {
// Set up a profile.
let profile = do_get_profile();
// Start the cookieservice, to force creation of a database.
Services.cookies.sessionCookies;
// Assert schema 13 cookie db was created
Assert.ok(do_get_cookie_file(profile).exists());
let dbConnection = Services.storage.openDatabase(do_get_cookie_file(profile));
let version = dbConnection.schemaVersion;
dbConnection.close();
Assert.equal(version, 13);
// Close the profile.
await promise_close_profile();
// Open CookieDatabaseConnection to manipulate DB without using services.
let schema13db = new CookieDatabaseConnection(
do_get_cookie_file(profile),
13
);
let now = Date.now() * 1000;
let futureExpiry = Math.round(now / 1e6 + 1000);
let N = 20;
// Populate db with N unexpired, unique, partially partitioned cookies.
for (let i = 0; i < N; ++i) {
let cookie = new Cookie(
"test" + i,
"Some data",
"foo.com",
"/",
futureExpiry,
now,
now,
false,
false,
false,
false,
{},
Ci.nsICookie.SAMESITE_NONE,
Ci.nsICookie.SAMESITE_NONE,
Ci.nsICookie.SCHEME_UNSET,
!!(i % 2) // isPartitioned
);
schema13db.insertCookie(cookie);
}
schema13db.close();
schema13db = null;
// Reload profile.
await promise_load_profile();
// Assert inserted cookies are in the db and correctly handled by services.
Assert.equal(Services.cookies.countCookiesFromHost("foo.com"), N);
// Open connection to manipulated db
dbConnection = Services.storage.openDatabase(do_get_cookie_file(profile));
// Check that schema is still correct after profile reload / db opening
Assert.equal(dbConnection.schemaVersion, 13);
// Count cookies with isPartitionedAttributeSet set to 1 (true)
let stmt = dbConnection.createStatement(
"SELECT COUNT(1) FROM moz_cookies WHERE isPartitionedAttributeSet = 1"
);
let success = stmt.executeStep();
Assert.ok(success);
// Assert the correct number of partitioned cookies was inserted / read
let isPartitionedAttributeSetCount = stmt.getInt32(0);
stmt.finalize();
Assert.equal(isPartitionedAttributeSetCount, N / 2);
// Cleanup
Services.cookies.removeAll();
stmt.finalize();
dbConnection.close();
do_close_profile();
});

View File

@ -1034,6 +1034,10 @@ run-sequentially = "tlsserver uses fixed port"
["test_safeoutputstream_append.js"]
["test_schema_13_db.js"]
["test_schema_12_migration.js"]
["test_schema_10_migration.js"]
["test_schema_2_migration.js"]