Skip to content

Commit

Permalink
[SAA] Extending Storage Access API to omit unpartitioned cookies
Browse files Browse the repository at this point in the history
The current Storage Access API requires that unpartitioned cookie access
is granted if any unpartitioned storage access is needed. This forces
unpartitioned cookies to be included in network requests which may not
need them, having impacts on network performance and security. Before
the extension ships, we have a chance to fix this behavior without a
compatibility break.

Design Doc:
https://docs.google.com/document/d/19qCGb4qwOcGiNrQM3ptWvRmB4JtpaTFgFVlWLXNOQ6c/edit

Explainer:
https://arichiv.github.io/saa-non-cookie-storage/omit-unpartitioned-cookies.html

Bug: 1484966
Change-Id: Id0d29df0de173667b70d14761acdb95c543fea0f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5228597
Commit-Queue: Chris Fredrickson <[email protected]>
Commit-Queue: Ari Chivukula <[email protected]>
Auto-Submit: Ari Chivukula <[email protected]>
Reviewed-by: Chris Fredrickson <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1252321}
  • Loading branch information
arichiv authored and chromium-wpt-export-bot committed Jan 25, 2024
1 parent 9d3b164 commit 35de43e
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<meta charset="utf-8">
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/storage-access-api/helpers.js"></script>
<script>
(async function() {
test_driver.set_test_context(window.top);
Expand All @@ -10,10 +11,52 @@
let message = "HasAccess for " + type;
// Step 6 (storage-access-api/storage-access-beyond-cookies.{}.tentative.sub.https.html)
try {
await MaybeSetStorageAccess("*", "*", "blocked");
await test_driver.set_permission({ name: 'storage-access' }, 'granted');
switch (type) {
case "none": {
let couldRequestStorageAccessForNone = true;
try {
await document.requestStorageAccess({});
} catch (_) {
couldRequestStorageAccessForNone = false;
}
if (couldRequestStorageAccessForNone) {
message = "Requesting access for {} should fail."
}
let couldRequestStorageAccessForAllFalse = true;
try {
await document.requestStorageAccess({all:false});
} catch (_) {
couldRequestStorageAccessForAllFalse = false;
}
if (couldRequestStorageAccessForAllFalse) {
message = "Requesting access for {all:false} should fail."
}
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
break;
}
case "cookies": {
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess || document.cookie.includes("test="+id)) {
message = "First-party cookies should not be readable before handle is loaded.";
}
await document.requestStorageAccess({cookies: true});
hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (!hasUnpartitionedCookieAccess || !document.cookie.includes("test="+id)) {
message = "First-party cookies should be readable if cookies were requested.";
}
break;
}
case "sessionStorage": {
const handle = await document.requestStorageAccess({sessionStorage: true});
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
if (id != handle.sessionStorage.getItem("test")) {
message = "No first-party Session Storage access";
}
Expand All @@ -35,6 +78,10 @@
}
case "localStorage": {
const handle = await document.requestStorageAccess({localStorage: true});
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
if (id != handle.localStorage.getItem("test")) {
message = "No first-party Local Storage access";
}
Expand All @@ -56,6 +103,10 @@
}
case "indexedDB": {
const handle = await document.requestStorageAccess({indexedDB: true});
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
const handle_dbs = await handle.indexedDB.databases();
if (handle_dbs.length != 1 || handle_dbs[0].name != id) {
message = "No first-party IndexedDB access";
Expand All @@ -69,6 +120,10 @@
}
case "locks": {
const handle = await document.requestStorageAccess({locks: true});
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
const handle_state = await handle.locks.query();
if (handle_state.held.length != 1 || handle_state.held[0].name != id) {
message = "No first-party Web Lock access";
Expand All @@ -82,6 +137,10 @@
}
case "caches": {
const handle = await document.requestStorageAccess({caches: true});
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
const handle_has = await handle.caches.has(id);
if (!handle_has) {
message = "No first-party Cache Storage access";
Expand All @@ -95,6 +154,10 @@
}
case "getDirectory": {
const handle = await document.requestStorageAccess({getDirectory: true});
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
const handle_root = await handle.getDirectory();
let handle_has = await handle_root.getFileHandle(id).then(() => true, () => false);
if (!handle_has) {
Expand All @@ -110,6 +173,10 @@
}
case "estimate": {
const handle = await document.requestStorageAccess({estimate: true});
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
const handle_estimate = await handle.estimate();
if (handle_estimate.usage <= 0) {
message = "No first-party quota access";
Expand All @@ -122,6 +189,10 @@
}
case "blobStorage": {
const handle = await document.requestStorageAccess({createObjectURL: true, revokeObjectURL: true});
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
let blob = await fetch(atob(id)).then(
(response) => response.text(),
() => "");
Expand Down Expand Up @@ -160,6 +231,10 @@
}
case "BroadcastChannel": {
const handle = await document.requestStorageAccess({BroadcastChannel: true});
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable if not requested.";
}
const handle_channel = handle.BroadcastChannel(id);
handle_channel.postMessage("Same-origin handle access");
handle_channel.close();
Expand All @@ -177,6 +252,7 @@
message = "Unable to load handle in same-origin context for " + type;
}
// Step 7 (storage-access-api/storage-access-beyond-cookies.{}.tentative.sub.https.html)
await MaybeSetStorageAccess("*", "*", "allowed");
await test_driver.set_permission({ name: 'storage-access' }, 'prompt');
window.top.postMessage(message, "*");
})();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<meta charset="utf-8">
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/storage-access-api/helpers.js"></script>
<body>
<script>
(async function() {
Expand All @@ -11,9 +12,27 @@
let message = "";
// Step 4 (storage-access-api/storage-access-beyond-cookies.{}.tentative.sub.https.html)
try {
await MaybeSetStorageAccess("*", "*", "blocked");
await test_driver.set_permission({ name: 'storage-access' }, 'granted');
let hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (hasUnpartitionedCookieAccess) {
message = "First-party cookies should not be readable before handle is loaded.";
}
const handle = await document.requestStorageAccess({all: true});
hasUnpartitionedCookieAccess = await document.hasUnpartitionedCookieAccess();
if (!hasUnpartitionedCookieAccess) {
message = "First-party cookies should be readable after handle is loaded.";
}
switch (type) {
case "none": {
break;
}
case "cookies": {
if (document.cookie.includes("test="+id)) {
message = "Cross-site first-party cookies should be empty";
}
break;
}
case "sessionStorage": {
if (!!handle.sessionStorage.getItem("test")) {
message = "Cross-site first-party Session Storage should be empty";
Expand Down Expand Up @@ -97,6 +116,7 @@
} catch (_) {
message = "Unable to load handle in cross-site context for all";
}
await MaybeSetStorageAccess("*", "*", "allowed");
await test_driver.set_permission({ name: 'storage-access' }, 'prompt');
if (message) {
window.top.postMessage(message, "*");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js

'use strict';

// Here's the set-up for this test:
// Step 1 (top-frame) Set up listener for "HasAccess" message.
// Step 2 (top-frame) Add data to first-party cookies.
// Step 3 (top-frame) Embed an iframe that's cross-site with top-frame.
// Step 4 (sub-frame) Try to use storage access API and read first-party data.
// Step 5 (sub-frame) Embed an iframe that's same-origin with top-frame.
// Step 6 (sub-sub-frame) Try to use storage access API and read first-party data.
// Step 7 (sub-sub-frame) Send "HasAccess for cookies" message to top-frame.
// Step 8 (top-frame) Cleanup.

async_test(t => {
// Step 1
window.addEventListener("message", t.step_func(e => {
// Step 8
assert_equals(e.data, "HasAccess for cookies", "Storage Access API should be accessible and return first-party data");
test_driver.delete_all_cookies().then(t.step_func(() => {
t.done();
}));
}));

// Step 2
const id = String(Date.now());
document.cookie = "test=" + id + "; SameSite=None; Secure";

// Step 3
let iframe = document.createElement("iframe");
iframe.src = "https://{{hosts[alt][]}}:{{ports[https][0]}}/storage-access-api/resources/storage-access-beyond-cookies-iframe.sub.html?type=cookies&id="+id;
document.body.appendChild(iframe);
}, "Verify StorageAccessAPIBeyondCookies for Cookies");
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// META: script=/resources/testdriver.js
// META: script=/resources/testdriver-vendor.js

'use strict';

// Here's the set-up for this test:
// Step 1 (top-frame) Set up listener for "HasAccess" message.
// Step 2 (top-frame) Skipped in this test, but numbering must be consistent with other tests.
// Step 3 (top-frame) Embed an iframe that's cross-site with top-frame.
// Step 4 (sub-frame) Skipped in this test, but numbering must be consistent with other tests.
// Step 5 (sub-frame) Embed an iframe that's same-origin with top-frame.
// Step 6 (sub-sub-frame) Try to use storage access API without requesting anything.
// Step 7 (sub-sub-frame) Send "HasAccess for none" message to top-frame.
// Step 8 (top-frame) Cleanup.

async_test(t => {
// Step 1
window.addEventListener("message", t.step_func(e => {
// Step 8
assert_equals(e.data, "HasAccess for none", "Storage Access API should not allow access for empty requests.");
t.done();
}));

// Step 2
// Step 3
let iframe = document.createElement("iframe");
iframe.src = "https://{{hosts[alt][]}}:{{ports[https][0]}}/storage-access-api/resources/storage-access-beyond-cookies-iframe.sub.html?type=none&id=";
document.body.appendChild(iframe);
}, "Verify StorageAccessAPIBeyondCookies for None");

0 comments on commit 35de43e

Please sign in to comment.