Bug 1685355 - Use storage principal when clearing data via clear-site-data header. r=johannh

Differential Revision: https://phabricator.services.mozilla.com/D101192
This commit is contained in:
Paul Zuehlcke 2021-01-19 10:04:05 +00:00
Родитель 34292507e2
Коммит 55b95d81de
5 изменённых файлов: 627 добавлений и 16 удалений

Просмотреть файл

@ -167,3 +167,6 @@ support-files =
file_saveAsVideo.sjs
file_saveAsPageInfo.html
file_video.ogv
[browser_partitionedClearSiteDataHeader.js]
support-files =
clearSiteData.sjs

Просмотреть файл

@ -0,0 +1,575 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use-strict";
/**
* Tests that when receiving the "clear-site-data" header - with dFPI enabled -
* we clear storage under the correct partition.
*/
const { SiteDataTestUtils } = ChromeUtils.import(
"resource://testing-common/SiteDataTestUtils.jsm"
);
const HOST_A = "example.com";
const HOST_B = "example.org";
const ORIGIN_A = `https://${HOST_A}`;
const ORIGIN_B = `https://${HOST_B}`;
const CLEAR_SITE_DATA_PATH = `/${TEST_PATH}clearSiteData.sjs`;
const CLEAR_SITE_DATA_URL_ORIGIN_B = ORIGIN_B + CLEAR_SITE_DATA_PATH;
const CLEAR_SITE_DATA_URL_ORIGIN_A = ORIGIN_A + CLEAR_SITE_DATA_PATH;
const THIRD_PARTY_FRAME_ID_ORIGIN_B = "thirdPartyFrame";
const THIRD_PARTY_FRAME_ID_ORIGIN_A = "thirdPartyFrame2";
const STORAGE_KEY = "testKey";
/**
* Creates an iframe in the passed browser and waits for it to load.
* @param {Browser} browser - Browser to create the frame in.
* @param {String} src - Frame source url.
* @param {String} id - Frame id.
* @param {boolean} sandbox - Whether the frame should be sandboxed.
* @returns {Promise} - Resolves once the frame has loaded.
*/
function createFrame(browser, src, id, sandbox) {
return SpecialPowers.spawn(
browser,
[{ page: src, frameId: id, sandbox }],
async function(obj) {
await new content.Promise(resolve => {
let frame = content.document.createElement("iframe");
if (obj.sandbox) {
frame.setAttribute("sandbox", "allow-scripts");
}
frame.src = obj.page;
frame.id = obj.frameId;
frame.addEventListener("load", resolve, { once: true });
content.document.body.appendChild(frame);
});
}
);
}
/**
* Creates a new tab, loads a url and creates an iframe.
* Callers need to clean up the tab before the test ends.
* @param {String} firstPartyUrl - Url to load in tab.
* @param {String} thirdPartyUrl - Url to load in frame.
* @param {String} frameId - Id of iframe element.
* @param {boolean} sandbox - Whether the frame should be sandboxed.
* @returns {Promise} - Resolves with the tab and the frame BrowsingContext once
* the tab and the frame have loaded.
*/
async function createTabWithFrame(
firstPartyUrl,
thirdPartyUrl,
frameId,
sandbox
) {
// Create tab and wait for it to be loaded.
let tab = BrowserTestUtils.addTab(gBrowser, firstPartyUrl);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
// Create cross origin iframe.
await createFrame(tab.linkedBrowser, thirdPartyUrl, frameId, sandbox);
// Return BrowsingContext of created iframe.
return { tab, frameBC: tab.linkedBrowser.browsingContext.children[0] };
}
/**
* Test wrapper for the ClearSiteData tests.
* Loads ORIGIN_A and ORIGIN_B in two tabs and inserts a cross origin pointing
* to the other iframe each.
* Both frames ORIGIN_B under ORIGIN_A and ORIGIN_A under ORIGIN_B will be
* storage partitioned.
* Depending on the clearDataContext variable we then either navigate ORIGIN_A
* (as top level) or ORIGIN_B (as third party frame) to the clear-site-data
* endpoint.
* @param {function} cbPreClear - Called after initial setup, once top levels
* and frames have been loaded.
* @param {function} cbPostClear - Called after data has been cleared via the
* "Clear-Site-Data" header.
* @param {("firstParty"|"thirdPartyPartitioned")} clearDataContext - Whether to
* navigate to the path that sets the "Clear-Site-Data" header with the first or
* third party.
* @param {boolean} [sandboxFrame] - Whether the frames should be sandboxed. No
* sandbox by default.
*/
async function runClearSiteDataTest(
cbPreClear,
cbPostClear,
clearDataContext,
sandboxFrame = false
) {
// Create a tabs for origin A and B with cross origins frames B and A
let [
{ frameBC: frameContextB, tab: tabA },
{ frameBC: frameContextA, tab: tabB },
] = await Promise.all([
createTabWithFrame(
ORIGIN_A,
ORIGIN_B,
THIRD_PARTY_FRAME_ID_ORIGIN_B,
sandboxFrame
),
createTabWithFrame(
ORIGIN_B,
ORIGIN_A,
THIRD_PARTY_FRAME_ID_ORIGIN_A,
sandboxFrame
),
]);
let browserA = tabA.linkedBrowser;
let contextA = browserA.browsingContext;
let browserB = tabB.linkedBrowser;
let contextB = browserB.browsingContext;
// Run test callback before clear-site-data
if (cbPreClear) {
await cbPreClear(contextA, contextB, frameContextB, frameContextA);
}
// Navigate to path with clear-site-data header
// Depending on the clearDataContext variable we either do this with the
// top browser or the third party storage partitioned frame (B embedded in A).
info(`Opening path with clear-site-data-header for ${clearDataContext}`);
if (clearDataContext == "firstParty") {
// Open in new tab so we keep our current test tab intact. The
// post-clear-callback might need it.
await BrowserTestUtils.withNewTab(
(browserA, CLEAR_SITE_DATA_URL_ORIGIN_A),
() => {}
);
} else if (clearDataContext == "thirdPartyPartitioned") {
// Navigate frame to path with clear-site-data header
await SpecialPowers.spawn(
browserA,
[
{
page: CLEAR_SITE_DATA_URL_ORIGIN_B,
frameId: THIRD_PARTY_FRAME_ID_ORIGIN_B,
},
],
async function(obj) {
await new content.Promise(resolve => {
let frame = content.document.getElementById(obj.frameId);
frame.addEventListener("load", resolve, { once: true });
frame.src = obj.page;
});
}
);
} else {
ok(false, "Invalid context requested for clear-site-data");
}
if (cbPostClear) {
await cbPostClear(contextA, contextB, frameContextB, frameContextA);
}
info("Cleaning up.");
BrowserTestUtils.removeTab(tabA);
BrowserTestUtils.removeTab(tabB);
await new Promise(resolve => {
Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, resolve);
});
}
/**
* Create an origin with partitionKey.
* @param {String} originNoSuffix - Origin without origin attributes.
* @param {String} [firstParty] - First party to create partitionKey.
* @returns {String} Origin with suffix. If not passed this will return the
* umodified origin.
*/
function getOrigin(originNoSuffix, firstParty) {
let origin = originNoSuffix;
if (firstParty) {
let [scheme, host] = firstParty.split("://");
origin += `^partitionKey=(${scheme},${host})`;
}
return origin;
}
/**
* Sets a storage item for an origin.
* @param {("cookie"|"localStorage")} storageType - Which storage type to use.
* @param {String} originNoSuffix - Context to set storage item in.
* @param {String} [firstParty] - Optional first party domain to partition
* under.
* @param {String} key - Key of the entry.
* @param {String} value - Value of the entry.
*/
function setStorageEntry(storageType, originNoSuffix, firstParty, key, value) {
if (storageType != "cookie" && storageType != "localStorage") {
ok(false, "Invalid storageType passed");
return;
}
let origin = getOrigin(originNoSuffix, firstParty);
if (storageType == "cookie") {
SiteDataTestUtils.addToCookies(origin, key, value);
return;
}
// localStorage
SiteDataTestUtils.addToLocalStorage(origin, key, value);
}
/**
* Tests whether a host sets a cookie.
* For the purpose of this test we assume that there is either one or no cookie
* set.
* This performs cookie lookups directly via the cookie service.
* @param {boolean} hasCookie - Whether we expect to see a cookie.
* @param {String} originNoSuffix - Origin the cookie is stored for.
* @param {String|null} firstParty - Whether to test for a partitioned cookie.
* If set this will be used to construct the partitionKey.
* @param {String} [key] - Expected key / name of the cookie.
* @param {String} [value] - Expected value of the cookie.
*/
function testHasCookie(hasCookie, originNoSuffix, firstParty, key, value) {
let origin = getOrigin(originNoSuffix, firstParty);
let label = `${originNoSuffix}${
firstParty ? ` (partitioned under ${firstParty})` : ""
}`;
if (!hasCookie) {
ok(
!SiteDataTestUtils.hasCookies(origin),
`Cookie for ${label} is not set for key ${key}`
);
return;
}
ok(
SiteDataTestUtils.hasCookies(origin, [{ key, value }]),
`Cookie for ${label} is set ${key}=${value}`
);
}
/**
* Tests whether a context has a localStorage entry.
* @param {boolean} hasEntry - Whether we expect to see an entry.
* @param {String} originNoSuffix - Origin to test localStorage for.
* @param {String} [firstParty] - First party context to test under.
* @param {String} key - key of the localStorage item.
* @param {String} [expectedValue] - Expected value of the item.
*/
function testHasLocalStorageEntry(
hasEntry,
originNoSuffix,
firstParty,
key,
expectedValue
) {
if (key == null) {
ok(false, "localStorage key is mandatory");
return;
}
let label = `${originNoSuffix}${
firstParty ? ` (partitioned under ${firstParty})` : ""
}`;
let origin = getOrigin(originNoSuffix, firstParty);
if (hasEntry) {
let hasEntry = SiteDataTestUtils.hasLocalStorage(origin, [
{ key, value: expectedValue },
]);
ok(
hasEntry,
`localStorage for ${label} has expected value ${key}=${expectedValue}`
);
} else {
let hasEntry = SiteDataTestUtils.hasLocalStorage(origin);
ok(!hasEntry, `localStorage for ${label} is not set for key ${key}`);
}
}
/**
* Sets the initial storage entries used by the storage tests in this file.
* 1. first party ( A )
* 2. first party ( B )
* 3. third party partitioned ( B under A)
* 4. third party partitioned ( A under B)
* The entry values reflect which context they are set for.
* @param {("cookie"|"localStorage")} storageType - Storage type to initialize.
*/
async function setupInitialStorageState(storageType) {
if (storageType != "cookie" && storageType != "localStorage") {
ok(false, "Invalid storageType passed");
return;
}
// Set a first party entry
setStorageEntry(storageType, ORIGIN_A, null, STORAGE_KEY, "firstParty");
// Set a storage entry in the storage partitioned third party frame
setStorageEntry(
storageType,
ORIGIN_B,
ORIGIN_A,
STORAGE_KEY,
"thirdPartyPartitioned"
);
// Set a storage entry in the non storage partitioned third party page
setStorageEntry(storageType, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
// Set a storage entry in the second storage partitioned third party frame.
setStorageEntry(
storageType,
ORIGIN_A,
ORIGIN_B,
STORAGE_KEY,
"thirdPartyPartitioned2"
);
info("Test that storage entries are set for all contexts");
if (storageType == "cookie") {
testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
testHasCookie(
true,
ORIGIN_B,
ORIGIN_A,
STORAGE_KEY,
"thirdPartyPartitioned"
);
testHasCookie(
true,
ORIGIN_A,
ORIGIN_B,
STORAGE_KEY,
"thirdPartyPartitioned2"
);
return;
}
testHasLocalStorageEntry(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
testHasLocalStorageEntry(
true,
ORIGIN_B,
ORIGIN_A,
STORAGE_KEY,
"thirdPartyPartitioned"
);
testHasLocalStorageEntry(
true,
ORIGIN_A,
ORIGIN_B,
STORAGE_KEY,
"thirdPartyPartitioned2"
);
}
add_task(async function setup() {
info("Starting ClearSiteData test");
await SpecialPowers.flushPrefEnv();
await SpecialPowers.pushPrefEnv({
set: [
["dom.storage_access.enabled", true],
[
"network.cookie.cookieBehavior",
Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN,
],
["privacy.trackingprotection.enabled", false],
["privacy.trackingprotection.pbmode.enabled", false],
// Needed for SiteDataTestUtils#hasLocalStorage
["dom.storage.client_validation", false],
],
});
});
/**
* Test clearing partitioned cookies via clear-site-data header
* (Cleared via the cookie service).
*/
/**
* Tests that when a storage partitioned third party frame loads a site with
* "Clear-Site-Data", the cookies are cleared for only that partitioned frame.
*/
add_task(async function cookieClearThirdParty() {
await runClearSiteDataTest(
// Pre Clear-Site-Data
() => setupInitialStorageState("cookie"),
// Post Clear-Site-Data
() => {
info("Testing: First party cookie has not changed");
testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
info("Testing: Unpartitioned cookie has not changed");
testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
info("Testing: Partitioned cookie for HOST_B (HOST_A) has been cleared");
testHasCookie(false, ORIGIN_B, ORIGIN_A);
info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed");
testHasCookie(
true,
ORIGIN_A,
ORIGIN_B,
STORAGE_KEY,
"thirdPartyPartitioned2"
);
},
// Send clear-site-data header in partitioned third party context.
"thirdPartyPartitioned"
);
});
/**
* Tests that when a sandboxed storage partitioned third party frame loads a
* site with "Clear-Site-Data", no cookies are cleared and we don't crash.
* Crash details in Bug 1686938.
*/
add_task(async function cookieClearThirdPartySandbox() {
await runClearSiteDataTest(
// Pre Clear-Site-Data
() => setupInitialStorageState("cookie"),
// Post Clear-Site-Data
() => {
info("Testing: First party cookie has not changed");
testHasCookie(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
info("Testing: Unpartitioned cookie has not changed");
testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
info("Testing: Partitioned cookie for HOST_B (HOST_A) has not changed");
testHasCookie(
true,
ORIGIN_B,
ORIGIN_A,
STORAGE_KEY,
"thirdPartyPartitioned"
);
info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed");
testHasCookie(
true,
ORIGIN_A,
ORIGIN_B,
STORAGE_KEY,
"thirdPartyPartitioned2"
);
},
// Send clear-site-data header in partitioned third party context.
"thirdPartyPartitioned",
true
);
});
/**
* Tests that when the we load a path with "Clear-Site-Data" at top level, the
* cookies are cleared only in the first party context.
*/
add_task(async function cookieClearFirstParty() {
await runClearSiteDataTest(
// Pre Clear-Site-Data
() => setupInitialStorageState("cookie"),
// Post Clear-Site-Data
() => {
info("Testing: First party cookie has been cleared");
testHasCookie(false, ORIGIN_A, null);
info("Testing: Unpartitioned cookie has not changed");
testHasCookie(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
info("Testing: Partitioned cookie for HOST_B (HOST_A) has not changed");
testHasCookie(
true,
ORIGIN_B,
ORIGIN_A,
STORAGE_KEY,
"thirdPartyPartitioned"
);
info("Testing: Partitioned cookie for HOST_A (HOST_B) has not changed");
testHasCookie(
true,
ORIGIN_A,
ORIGIN_B,
STORAGE_KEY,
"thirdPartyPartitioned2"
);
},
// Send clear-site-data header in first party context.
"firstParty"
);
});
/**
* Test clearing partitioned localStorage via clear-site-data header
* (Cleared via the quota manager).
*/
/**
* Tests that when a storage partitioned third party frame loads a site with
* "Clear-Site-Data", localStorage is cleared for only that partitioned frame.
*/
add_task(async function localStorageClearThirdParty() {
await runClearSiteDataTest(
// Pre Clear-Site-Data
() => setupInitialStorageState("localStorage"),
// Post Clear-Site-Data
async () => {
info("Testing: First party localStorage has not changed");
testHasLocalStorageEntry(true, ORIGIN_A, null, STORAGE_KEY, "firstParty");
info("Testing: Unpartitioned localStorage has not changed");
testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
info(
"Testing: Partitioned localStorage for HOST_B (HOST_A) has been cleared"
);
testHasLocalStorageEntry(false, ORIGIN_B, ORIGIN_A, STORAGE_KEY);
info(
"Testing: Partitioned localStorage for HOST_A (HOST_B) has not changed"
);
testHasLocalStorageEntry(
true,
ORIGIN_A,
ORIGIN_B,
STORAGE_KEY,
"thirdPartyPartitioned2"
);
},
// Send clear-site-data header in partitioned third party context.
"thirdPartyPartitioned"
);
});
/**
* Tests that when the we load a path with "Clear-Site-Data" at top level,
* localStorage is cleared only in the first party context.
*/
add_task(async function localStorageClearFirstParty() {
await runClearSiteDataTest(
// Pre Clear-Site-Data
() => setupInitialStorageState("localStorage"),
// Post Clear-Site-Data
() => {
info("Testing: First party localStorage has been cleared");
testHasLocalStorageEntry(false, ORIGIN_A, null, STORAGE_KEY);
info("Testing: Unpartitioned thirdParty localStorage has not changed");
testHasLocalStorageEntry(true, ORIGIN_B, null, STORAGE_KEY, "thirdParty");
info(
"Testing: Partitioned localStorage for HOST_B (HOST_A) has not changed"
);
testHasLocalStorageEntry(
true,
ORIGIN_B,
ORIGIN_A,
STORAGE_KEY,
"thirdPartyPartitioned"
);
info(
"Testing: Partitioned localStorage for HOST_A (HOST_B) has not changed"
);
testHasLocalStorageEntry(
true,
ORIGIN_A,
ORIGIN_B,
STORAGE_KEY,
"thirdPartyPartitioned2"
);
},
// Send clear-site-data header in first party context.
"firstParty"
);
});

Просмотреть файл

@ -0,0 +1,6 @@
function handleRequest(aRequest, aResponse) {
aResponse.setStatusLine(aRequest.httpVersion, 200);
aResponse.setHeader("Clear-Site-Data", '"*"');
aResponse.setHeader("Content-Type", "text/plain");
aResponse.write("Clear-Site-Data");
}

Просмотреть файл

@ -109,8 +109,8 @@ var SiteDataTestUtils = {
* Adds a new localStorage entry for the specified origin, with the specified contents.
*
* @param {String} origin - the origin of the site to add test data for
* @param {String} key [optional] - the localStorage key
* @param {String} value [optional] - the localStorage value
* @param {String} [key] - the localStorage key
* @param {String} [value] - the localStorage value
*/
addToLocalStorage(origin, key = "foo", value = "bar") {
let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
@ -122,17 +122,19 @@ var SiteDataTestUtils = {
principal,
""
);
storage.setItem("key", "value");
storage.setItem(key, value);
},
/**
* Checks whether the given origin is storing data in localStorage
*
* @param {String} origin - the origin of the site to check
* @param {{key: String, value: String}[]} [testEntries] - An array of entries
* to test for.
*
* @returns {Boolean} whether the origin has localStorage data
*/
hasLocalStorage(origin) {
hasLocalStorage(origin, testEntries) {
let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
origin
);
@ -142,7 +144,16 @@ var SiteDataTestUtils = {
principal,
""
);
return !!storage.length;
if (!storage.length) {
return false;
}
if (!testEntries) {
return true;
}
return (
storage.length >= testEntries.length &&
testEntries.every(({ key, value }) => storage.getItem(key) == value)
);
},
/**
@ -180,22 +191,37 @@ var SiteDataTestUtils = {
});
},
hasCookies(origin) {
hasCookies(origin, testEntries) {
let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin(
origin
);
for (let cookie of Services.cookies.cookies) {
if (
let filterFn = cookie => {
return (
ChromeUtils.isOriginAttributesEqual(
principal.originAttributes,
cookie.originAttributes
) &&
cookie.host.includes(principal.host)
) {
return true;
}
) && cookie.host.includes(principal.host)
);
};
// Return on first cookie found for principal.
if (!testEntries) {
return Services.cookies.cookies.some(filterFn);
}
return false;
// Collect all cookies that match the principal
let cookies = Services.cookies.cookies.filter(filterFn);
if (cookies.length < testEntries.length) {
return false;
}
// This code isn't very efficient. It should only be used for testing
// a small amount of cookies.
return testEntries.every(({ key, value }) =>
cookies.some(cookie => cookie.name == key && cookie.value == value)
);
},
hasIndexedDB(origin) {

Просмотреть файл

@ -158,8 +158,9 @@ void ClearSiteData::ClearDataFromChannel(nsIHttpChannel* aChannel) {
}
nsCOMPtr<nsIPrincipal> principal;
rv = ssm->GetChannelResultPrincipal(aChannel, getter_AddRefs(principal));
if (NS_WARN_IF(NS_FAILED(rv))) {
rv = ssm->GetChannelResultStoragePrincipal(aChannel,
getter_AddRefs(principal));
if (NS_WARN_IF(NS_FAILED(rv) || !principal)) {
return;
}