Skip to content
Permalink
Browse files
Align ITP client side cookie cap with other script-writable storage
https://bugs.webkit.org/show_bug.cgi?id=240248
rdar://90468671

Reviewed by Alex Christensen.

* LayoutTests/http/tests/resourceLoadStatistics/capped-lifetime-for-cookie-set-in-js-24-hours.html: Added.
* LayoutTests/http/tests/resourceLoadStatistics/website-data-removal-for-site-navigated-to-with-link-decoration-js-cookie-checking.html: Added.
* LayoutTests/http/tests/resourceLoadStatistics/website-data-removal-for-site-without-user-interaction-js-cookie-checking.html: Added.
* LayoutTests/platform/mac-wk2/TestExpectations:
* LayoutTests/platform/wk2/TestExpectations:
Add new tests for this behavior. Skip them for now because they rely on internal bits.
This patch adds a slight behavior change for ITP with client side cookies set from a page that is a link decoration target in
that it deletes client side cookies either 24 hours after they are set OR after 7 days of no user interaction, whichever comes first.
This maintains the strictest level of privacy for client side cookies based on all heuristics.

* Source/WTF/wtf/PlatformEnableCocoa.h:
* Source/WebCore/platform/network/NetworkStorageSession.cpp:
(WebCore::NetworkStorageSession::setAgeCapForClientSideCookies):
(WebCore::NetworkStorageSession::clientSideCookieCap const):
Update client-side cookie cap code to only handle the 24 hour link-decoration case now that we
no longer need the 7 day cap.

(WebCore::NetworkStorageSession::deleteCookiesForHostnames):
* Source/WebCore/platform/network/NetworkStorageSession.h:
* Source/WebCore/platform/network/cf/NetworkStorageSessionCFNetWin.cpp:
(WebCore::NetworkStorageSession::deleteCookiesForHostnames):
* Source/WebCore/platform/network/cocoa/NetworkStorageSessionCocoa.mm:
(WebCore::parseDOMCookie):
Update JS cookie code so that we mark cookies as set in javascript and can distinguish them
from server side cookies. Since there are some underlying assumptions in CFNetwork code about
NSHTTPCookie not being mutable, we need to re-create the cookie here with the JS bit set to true.

(WebCore::NetworkStorageSession::deleteCookiesForHostnames):
* Source/WebCore/platform/network/curl/NetworkStorageSessionCurl.cpp:
(WebCore::NetworkStorageSession::deleteCookiesForHostnames):
* Source/WebCore/platform/network/soup/NetworkStorageSessionSoup.cpp:
(WebCore::NetworkStorageSession::deleteCookiesForHostnames):
* Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.cpp:
(WebKit::ResourceLoadStatisticsDatabaseStore::merge):
(WebKit::ResourceLoadStatisticsDatabaseStore::logCrossSiteLoadWithLinkDecoration):
(WebKit::ResourceLoadStatisticsDatabaseStore::setIsScheduledForAllScriptWrittenStorageRemoval):
(WebKit::ResourceLoadStatisticsDatabaseStore::registrableDomainsToDeleteOrRestrictWebsiteDataFor):
(WebKit::ResourceLoadStatisticsDatabaseStore::setIsScheduledForAllButCookieDataRemoval): Deleted.
* Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsDatabaseStore.h:
* Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.cpp:
(WebKit::domainsToString):
(WebKit::ResourceLoadStatisticsStore::updateClientSideCookiesAgeCap):
(WebKit::ResourceLoadStatisticsStore::setAgeCapForClientSideCookies): Deleted.
* Source/WebKit/NetworkProcess/Classifier/ResourceLoadStatisticsStore.h:
* Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h:
(WebKit::RegistrableDomainsToDeleteOrRestrictWebsiteDataFor::isolatedCopy const):
(WebKit::RegistrableDomainsToDeleteOrRestrictWebsiteDataFor::isolatedCopy):
(WebKit::RegistrableDomainsToDeleteOrRestrictWebsiteDataFor::isEmpty const):
Rename "non cookie website data" to "script written storage" now that
we include client side cookies. This can't be done to the actual
database entry without a migration to a new database due to SQLite
restrictions around renames, so we will leave that one in place.

* Source/WebKit/NetworkProcess/NetworkProcess.cpp:
(WebKit::NetworkProcess::deleteAndRestrictWebsiteDataForRegistrableDomains):
Use the values stored in domainsToDeleteAllScriptWritableStorageFor to know which domains to
delete script-writable cookies for when deleting website data, instead of just looking at domainsToDeleteAllCookiesFor.

(WebKit::NetworkProcess::setAgeCapForClientSideCookies): Deleted.
* Source/WebKit/NetworkProcess/NetworkProcess.h:
* Source/WebKit/NetworkProcess/NetworkProcess.messages.in:
* Source/WebKit/UIProcess/Network/NetworkProcessProxy.cpp:
(WebKit::NetworkProcessProxy::setAgeCapForClientSideCookies): Deleted.
* Source/WebKit/UIProcess/Network/NetworkProcessProxy.h:
Unused function.

Canonical link: https://commits.webkit.org/251397@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@295391 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
kcheney1 committed Jun 8, 2022
1 parent dc533fa commit a2db53cd97dc8136ac5c2a22d4cd2b53d0d717d6
Show file tree
Hide file tree
Showing 22 changed files with 696 additions and 78 deletions.
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
<script src="/js-test-resources/js-test.js"></script>
<script src="/cookies/resources/cookie-utilities.js"></script>
</head>
<body>
<script>
description("Check that cookies created by JavaScript with max-age or expiry longer than a week get capped to a week.");
jsTestIsAsync = true;

if (internals)
internals.setResourceLoadStatisticsEnabled(true);

let passedTests = 0;
function checkThatCookieDoesNotExpireAfter(cookieData, maxAgeInSeconds) {
let now = new Date();
let maxExpiryDateInMilliseconds = now.getTime() + (maxAgeInSeconds * 1000);

if (maxExpiryDateInMilliseconds > cookieData["expires"])
++passedTests;
else
testFailed("Cookie named " + cookieData["name"] + " expires in more than " + maxAgeInSeconds + " seconds.");
}

const twoDaysInSeconds = 2 * 24 * 60 * 60;
const shortLivedCookieMaxAge = { name : "shortLivedCookieMaxAge", lifetime : "Max-Age=" + twoDaysInSeconds + ";" };
document.cookie = shortLivedCookieMaxAge.name + "=foobar; " + shortLivedCookieMaxAge.lifetime + " path=/";

const twoDaysAsExpiresDate = createExpiresDateFromMaxAge(twoDaysInSeconds);
const shortLivedCookieExpires = { name : "shortLivedCookieExpires", lifetime : "Expires=" + twoDaysAsExpiresDate + ";" };
document.cookie = shortLivedCookieExpires.name + "=foobar; " + shortLivedCookieExpires.lifetime + " path=/";

const oneWeekInSeconds = 7 * 24 * 60 * 60;
const twoWeeksInSeconds = 2 * oneWeekInSeconds;
const longLivedCookieMaxAge = { name : "longLivedCookieMaxAge", lifetime : "Max-Age=" + twoWeeksInSeconds + ";" };
document.cookie = longLivedCookieMaxAge.name + "=foobar; " + longLivedCookieMaxAge.lifetime + " path=/";

const twoWeeksAsExpiresDate = createExpiresDateFromMaxAge(twoWeeksInSeconds);
const longLivedCookieExpires = { name : "longLivedCookieExpires", lifetime : "Expires=" + twoWeeksAsExpiresDate + ";" };
document.cookie = longLivedCookieExpires.name + "=foobar; " + longLivedCookieExpires.lifetime + " path=/";

const overTwoDaysInSeconds = twoDaysInSeconds + 30;
const overTwoWeeksInSeconds = twoWeeksInSeconds + 30;
if (internals) {
let cookies = internals.getCookies();
if (!cookies.length)
testFailed("No cookies found.");
for (let cookie of cookies) {
switch (cookie.name) {
case shortLivedCookieMaxAge.name:
checkThatCookieDoesNotExpireAfter(cookie, overTwoDaysInSeconds);
break;
case shortLivedCookieExpires.name:
checkThatCookieDoesNotExpireAfter(cookie, overTwoDaysInSeconds);
break;
case longLivedCookieMaxAge.name:
checkThatCookieDoesNotExpireAfter(cookie, overTwoWeeksInSeconds);
break;
case longLivedCookieExpires.name:
checkThatCookieDoesNotExpireAfter(cookie, overTwoWeeksInSeconds);
break;
}
}

resetCookiesForCurrentOrigin();

if (passedTests === 4) {
testPassed("The two short-lived cookies don't expire after more than " + overTwoDaysInSeconds + " seconds.");
testPassed("The two long-lived cookies don't expire after more than " + overTwoWeeksInSeconds + " seconds.");
} else
testFailed("At least one cookie's expiry attribute was beyond the test thresholds.");
} else
testFailed("No internals object.");

if (internals)
internals.setResourceLoadStatisticsEnabled(false);

finishJSTest();
</script>
</body>
</html>
@@ -0,0 +1,254 @@
<!DOCTYPE html>
<html>
<head>
<script src="/cookies/resources/cookie-utilities.js"></script>
<script src="resources/util.js"></script>
</head>
<body onload="setTimeout('runTest()', 0)">
<div id="description">Check that non-cookie website data gets removed after a navigation with link decoration from a prevalent resource.</div>
<br>
<div id="output"></div>
<br>
<script>
testRunner.waitUntilDone();
testRunner.dumpAsText();

const httpOnlyCookieName = "http-only-cookie";
const serverSideCookieName = "server-side-cookie";
const clientSideCookieName = "client-side-cookie";

function sortStringArray(a, b) {
a = a.toLowerCase();
b = b.toLowerCase();

return a > b ? 1 : b > a ? -1 : 0;
}

function addLinebreakToOutput() {
let element = document.createElement("br");
output.appendChild(element);
}

function addOutput(message) {
let element = document.createElement("div");
element.innerText = message;
output.appendChild(element);
}

function checkCookies(isAfterDeletion) {
let unsortedTestPassedMessages = [];
let cookies = internals.getCookies();
if (!cookies.length)
testFailed((isAfterDeletion ? "After" : "Before") + " script-accessible deletion: No cookies found.");
for (let cookie of cookies) {
switch (cookie.name) {
case httpOnlyCookieName:
unsortedTestPassedMessages.push((isAfterDeletion ? "After" : "Before") + " deletion: " + (isAfterDeletion ? " " : "") + "HttpOnly cookie exists.");
break;
case serverSideCookieName:
unsortedTestPassedMessages.push((isAfterDeletion ? "After" : "Before") + " deletion: Regular server-side cookie exists.");
break;
case clientSideCookieName:
unsortedTestPassedMessages.push((isAfterDeletion ? "After" : "Before") + " deletion: Client-side cookie exists.");
break;
}
}

if (!cookies.includes(clientSideCookieName) && isAfterDeletion)
unsortedTestPassedMessages.push("After deletion: Client-side cookie does not exist.");

let sortedTestPassedMessages = unsortedTestPassedMessages.sort(sortStringArray);
for (let testPassedMessage of sortedTestPassedMessages) {
addOutput(testPassedMessage);
}
}

const dbName = "TestDatabase";

function createIDBDataStore(callback) {
let request = indexedDB.open(dbName);
request.onerror = function() {
addOutput("Couldn't create indexedDB.");
finishTest();
};
request.onupgradeneeded = function(event) {
let db = event.target.result;
let objStore = db.createObjectStore("test", {autoIncrement: true});
objStore.add("value");
callback();
}
}

const maxIntervals = 20;

let intervalCounterIDB;
let checkIDBCallback;
let checkIDBIntervalID;
let semaphoreIDBCheck = false;
function checkIDBDataStoreExists(isAfterDeletion, callback) {
let request;
intervalCounterIDB = 0;
checkIDBCallback = callback;
if (!isAfterDeletion) {
// Check until there is a IDB.
checkIDBIntervalID = setInterval(function() {
if (semaphoreIDBCheck)
return;
semaphoreIDBCheck = true;

if (++intervalCounterIDB >= maxIntervals) {
clearInterval(checkIDBIntervalID);
addOutput("Before deletion: IDB entry does not exist.");
semaphoreIDBCheck = false;
checkIDBCallback();
} else {
request = indexedDB.open(dbName);
request.onerror = function () {
clearInterval(checkIDBIntervalID);
addOutput("Couldn't open indexedDB.");
semaphoreIDBCheck = false;
finishTest();
};
request.onupgradeneeded = function () {
// Let the next interval check again.
semaphoreIDBCheck = false;
};
request.onsuccess = function () {
clearInterval(checkIDBIntervalID);
addOutput("Before deletion: IDB entry does exist.");
semaphoreIDBCheck = false;
checkIDBCallback();
};
}
}, 200);
} else {
// Check until there is no IDB.
checkIDBIntervalID = setInterval(function () {
if (semaphoreIDBCheck)
return;
semaphoreIDBCheck = true;

if (++intervalCounterIDB >= maxIntervals) {
clearInterval(checkIDBIntervalID);
addOutput("After deletion: IDB entry checks exhausted.");
semaphoreIDBCheck = false;
checkIDBCallback();
} else {
request = indexedDB.open(dbName);
request.onerror = function () {
clearInterval(checkIDBIntervalID);
addOutput("Couldn't open indexedDB.");
semaphoreIDBCheck = false;
finishTest();
};
request.onupgradeneeded = function () {
clearInterval(checkIDBIntervalID);
addOutput("After deletion: IDB entry does not exist.");
semaphoreIDBCheck = false;
checkIDBCallback();
};
request.onsuccess = function () {
// Let the next interval check again.
semaphoreIDBCheck = false;
};
}
}, 200);
}
}

let intervalCounterLocalStorage;
let checkLocalStorageCallback;
let checkLocalStorageIntervalID;
const localStorageName = "test";
const localStorageValue = "value";
function checkLocalStorageExists(isAfterDeletion, callback) {
intervalCounterLocalStorage = 0;
checkLocalStorageCallback = callback;
if (!isAfterDeletion) {
// Check until there is LocalStorage.
checkLocalStorageIntervalID = setInterval(function() {
if (++intervalCounterLocalStorage >= maxIntervals) {
clearInterval(checkLocalStorageIntervalID);
let value = localStorage.getItem(localStorageName);
addOutput("Before deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
checkLocalStorageCallback();
} else if (testRunner.isStatisticsHasLocalStorage(destinationOrigin)) {
clearInterval(checkLocalStorageIntervalID);
let value = localStorage.getItem(localStorageName);
addOutput("Before deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
checkLocalStorageCallback();
}
}, 100);
} else {
// Check until there is no LocalStorage.
checkLocalStorageIntervalID = setInterval(function() {
if (++intervalCounterLocalStorage >= maxIntervals) {
clearInterval(checkLocalStorageIntervalID);
let value = localStorage.getItem(localStorageName);
addOutput("After deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
checkLocalStorageCallback();
} else if (!testRunner.isStatisticsHasLocalStorage(destinationOrigin)) {
clearInterval(checkLocalStorageIntervalID);
let value = localStorage.getItem(localStorageName);
addOutput("After deletion: LocalStorage entry " + (value === localStorageValue ? "does" : "does not") + " exist.");
checkLocalStorageCallback();
}
}, 100);
}
}

async function writeWebsiteDataAndContinue() {
// Write cookies.
await fetch("/cookies/resources/set-http-only-cookie.py?cookieName=" + httpOnlyCookieName, { credentials: "same-origin" });
await fetch("/cookies/resources/setCookies.cgi", { headers: { "Set-Cookie": serverSideCookieName + "=1; path=/;" }, credentials: "same-origin" });
document.cookie = clientSideCookieName + "=1";

checkCookies(false);

// Write LocalStorage
localStorage.setItem(localStorageName, localStorageValue);
checkLocalStorageExists(false, function() {

// Write IndexedDB.
createIDBDataStore(function () {
checkIDBDataStoreExists(false, function() {
addLinebreakToOutput();
processWebsiteDataAndContinue();
});
});
});
}

function processWebsiteDataAndContinue() {
testRunner.installStatisticsDidScanDataRecordsCallback(checkWebsiteDataAndContinue);
testRunner.statisticsProcessStatisticsAndDataRecords();
}

function checkWebsiteDataAndContinue() {
checkCookies(true);
checkLocalStorageExists(true, function () {
checkIDBDataStoreExists(true, finishTest);
});
}

async function finishTest() {
await resetCookiesITP();
testRunner.dumpResourceLoadStatistics();
setEnableFeature(false, function() {
testRunner.notifyDone();
});
}

const prevalentResourceOrigin = "http://localhost:8000";
const destinationOrigin = "http://127.0.0.1:8000";
function runTest() {
setEnableFeature(true, function () {
testRunner.setStatisticsPrevalentResource(prevalentResourceOrigin, true, function() {
testRunner.setStatisticsCrossSiteLoadWithLinkDecoration(prevalentResourceOrigin, destinationOrigin);
writeWebsiteDataAndContinue();
});
});
}
</script>
</body>
</html>

0 comments on commit a2db53c

Please sign in to comment.