Skip to content

Commit 74bc5de

Browse files
committed
WIP: [SI] Add a Storage domain for cookie inspection
https://bugs.webkit.org/show_bug.cgi?id=308900 rdar://171780948 Reviewed by Qianlang Chen. Introduce a new Storage domain (cookies only for this pass) that runs entirely UIProcess-side and talks to the authoritative store via API::HTTPCookieStore -- the same path BidiStorageAgent already uses. Reading the NetworkProcess-owned store in one call sees cookies across every WebContent process, including those set inside cross-origin iframes under Site Isolation, with no per-process fan- out or routing. The old Page cookie commands are left untouched. InspectorStorageAgent does no WebContent IPC (all work is UIProcess-local), so unlike ProxyingPageAgent/ProxyingNetworkAgent it needs no MessageReceiver and lives in the AgentRegistry. It is created unconditionally in WebPageInspectorController::createLazyAgents alongside InspectorBrowserAgent, not gated on siteIsolationEnabled(): the domain is useful with or without SI, and reading the authoritative store is the correct model in both cases. Its protocol domain is declared with targetTypes ["web-page"] so, like Browser, it is owned by the UIProcess (multiplexing) target rather than the WebContent page target; Target.js gains the matching StorageAgent accessor. getCookies reads the flat authoritative store (API::HTTPCookieStore::cookies, backed by WebCookieManager::GetAllCookies). This read surfaces BOTH unpartitioned and partitioned (CHIPS) cookies; each cookie carries its partition in Cookie.partitionKey, so partitioned cookies are visible and labeled (verified by storage-get-partitioned-cookies.https.html). The only remaining partition work is a documented per-URL+partition scoped lens (the storageKey PartitionDescriptor arm), deferred to webkit.org/b/292393 -- not basic visibility. The Storage domain exposes an explicit PartitionDescriptor parameter (context now, storageKey stubbed for webkit.org/b/292393) and populates Cookie.partitionKey: it is emitted on getCookies and, on setCookie, written onto the WebCore::Cookie only when the caller supplies an explicit partitionKey. setCookie's returned PartitionKey echoes where the cookie actually landed -- the explicit key if given, otherwise empty -- rather than synthesizing the page's origin, so it never claims an unpartitioned cookie lives in a partition. WebCore::Cookie::partitionKey already existed and already rides the IPC to NetworkProcess; BiDi simply never set it. A follow-up should bring BidiStorageAgent to parity via the shared helper. deleteCookies requires an explicit filter: a request with no filter is rejected rather than clearing the entire store (which, given the flat read above, would include partitioned cookies). This matches BidiStorageAgent and keeps a destructive clear from being the empty-argument default of a filtered command. Cookie filtering is factored out of BidiStorageAgent into a shared CookieStoreHelpers::cookieMatchesFilter so both frontends filter identically; BidiStorageAgent is refactored onto it with no behavior change. Opt-in cookie partitioning (CHIPS) is gated on HAVE(ALLOW_ONLY_PARTITIONED_COOKIES) (macOS 26.2+); on older OSes it is compiled out and no partitionKey is populated. storage-get-partitioned-cookies.https.html asserts a populated partitionKey, so it is [ Skip ] by default in LayoutTests/TestExpectations and re-enabled with [ Tahoe+ ] [ Pass ] in platform/mac-wk2/TestExpectations -- the same two-file split as the existing CHIPS suite. The other three storage tests run everywhere. Tests: http/tests/site-isolation/inspector/storage/storage-get-cookies.html http/tests/site-isolation/inspector/storage/storage-get-cookies-cross-origin-iframe.html http/tests/site-isolation/inspector/storage/storage-get-partitioned-cookies.https.html http/tests/site-isolation/inspector/storage/storage-set-and-delete-cookies.html * LayoutTests/TestExpectations: * LayoutTests/http/tests/site-isolation/inspector/storage/resources/set-cookie-via-script.html: Added. * LayoutTests/http/tests/site-isolation/inspector/storage/resources/set-cookie.py: Added. * LayoutTests/http/tests/site-isolation/inspector/storage/storage-get-cookies-cross-origin-iframe-expected.txt: Added. * LayoutTests/http/tests/site-isolation/inspector/storage/storage-get-cookies-cross-origin-iframe.html: Added. * LayoutTests/http/tests/site-isolation/inspector/storage/storage-get-cookies-expected.txt: Added. * LayoutTests/http/tests/site-isolation/inspector/storage/storage-get-cookies.html: Added. * LayoutTests/http/tests/site-isolation/inspector/storage/storage-get-partitioned-cookies.https-expected.txt: Added. * LayoutTests/http/tests/site-isolation/inspector/storage/storage-get-partitioned-cookies.https.html: Added. * LayoutTests/http/tests/site-isolation/inspector/storage/storage-set-and-delete-cookies-expected.txt: Added. * LayoutTests/http/tests/site-isolation/inspector/storage/storage-set-and-delete-cookies.html: Added. * LayoutTests/platform/mac-wk2/TestExpectations: * Source/JavaScriptCore/CMakeLists.txt: * Source/JavaScriptCore/DerivedSources-input.xcfilelist: * Source/JavaScriptCore/DerivedSources.make: * Source/JavaScriptCore/inspector/protocol/Storage.json: Added. * Source/WebInspectorUI/UserInterface/Protocol/Target.js: (WI.Target.prototype.get StorageAgent): Added. * Source/WebKit/Sources.txt: * Source/WebKit/UIProcess/Automation/BidiStorageAgent.cpp: (WebKit::BidiStorageAgent::getCookies): (WebKit::BidiStorageAgent::deleteCookies): (WebKit::cookieMatchesFilter): Deleted. * Source/WebKit/UIProcess/Inspector/Agents/InspectorStorageAgent.cpp: Added. (WebKit::toProtocol): (WebKit::fromProtocol): (WebKit::buildObjectForCookie): (WebKit::buildArrayForCookies): (WebKit::InspectorStorageAgent::InspectorStorageAgent): (WebKit::InspectorStorageAgent::didCreateFrontendAndBackend): (WebKit::InspectorStorageAgent::willDestroyFrontendAndBackend): (WebKit::InspectorStorageAgent::enable): (WebKit::InspectorStorageAgent::disable): (WebKit::InspectorStorageAgent::cookieStoreForPartition): (WebKit::InspectorStorageAgent::makePartitionKey): (WebKit::InspectorStorageAgent::getCookies): (WebKit::InspectorStorageAgent::setCookie): (WebKit::deleteCookiesSequentially): (WebKit::InspectorStorageAgent::deleteCookies): * Source/WebKit/UIProcess/Inspector/Agents/InspectorStorageAgent.h: Added. * Source/WebKit/UIProcess/Inspector/InspectorCookieStoreHelpers.cpp: Added. (WebKit::CookieFilter::fromProtocol): (WebKit::CookieFilter::matches const): * Source/WebKit/UIProcess/Inspector/InspectorCookieStoreHelpers.h: Added. * Source/WebKit/UIProcess/Inspector/WebPageInspectorController.cpp: (WebKit::WebPageInspectorController::createLazyAgents): * Source/WebKit/WebKit.xcodeproj/project.pbxproj: Canonical link: https://commits.webkit.org/317129@main
1 parent 4bcd765 commit 74bc5de

25 files changed

Lines changed: 1031 additions & 51 deletions

‎LayoutTests/TestExpectations‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,9 @@ http/tests/loading/resourceLoadStatistics/ [ Skip ]
270270
http/tests/cookies/multiple-cookies.https.html [ Skip ]
271271
http/tests/cookies/multiple-cookies-iframes.https.html [ Skip ]
272272
http/tests/cookies/max-partitioned-cookies.https.html [ Skip ]
273+
# Skipped by default: asserts a partitioned-cookie partitionKey that needs HAVE(ALLOW_ONLY_PARTITIONED_COOKIES)
274+
# (macOS 26.2+). platform/mac-wk2/TestExpectations re-enables it (expected Pass) on [ Tahoe+ ].
275+
http/tests/site-isolation/inspector/storage/storage-get-partitioned-cookies.https.html [ Skip ]
273276
http/tests/resourceLoadStatistics/ [ Skip ]
274277
http/tests/websocket/connection-refusal-in-frame-resource-load-statistics.html [ Skip ]
275278
http/tests/storageAccess/ [ Skip ]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
</head>
6+
<body>
7+
<script>
8+
// Runs inside the cross-origin iframe, which is a separate WebContent process under Site Isolation.
9+
// Set a cookie from script for a path the parent frame never requests any resource from, so the
10+
// parent's process has no cached-resource URL that would match it. The in-process Page.getCookies
11+
// looks cookies up by the parent process's known resource URLs, so it cannot discover this cookie;
12+
// Storage.getCookies reads the authoritative NetworkProcess store and sees it regardless.
13+
document.cookie = "CrossOriginScriptCookie=PASS; path=/site-isolation/inspector/storage/private-scope/";
14+
</script>
15+
</body>
16+
</html>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import sys
5+
from datetime import datetime, timedelta, timezone
6+
from urllib.parse import parse_qs
7+
8+
query = parse_qs(os.environ.get('QUERY_STRING', ''), keep_blank_values=True)
9+
name = query.get('name', [''])[0]
10+
value = query.get('value', [''])[0]
11+
secure = query.get('secure', [None])[0]
12+
exp_time = datetime.now(timezone.utc) + timedelta(hours=1)
13+
14+
cookie = '{}={}; expires={} GMT; Max-Age=3600'.format(name, value, exp_time.strftime('%a, %d-%b-%Y %H:%M:%S'))
15+
if secure:
16+
cookie += '; Secure'
17+
18+
sys.stdout.write(
19+
'Set-Cookie: {}\r\n'
20+
'Content-Type: text/html\r\n\r\n'.format(cookie)
21+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Tests that Storage.getCookies sees a cookie set by script inside a cross-origin iframe under Site Isolation, which the in-process Page.getCookies cannot.
2+
3+
4+
== Running test suite: SiteIsolation.Storage.getCookies
5+
-- Running test case: SiteIsolation.Storage.getCookies.CrossOriginIframe
6+
PASS: Cross-origin iframe should be its own Frame Target (out-of-process).
7+
PASS: Storage.getCookies should see the cross-origin iframe's cookie.
8+
PASS: Cross-origin cookie value should be 'PASS'.
9+
PASS: Page.getCookies must NOT see the cross-origin iframe's cookie (it is out-of-process and the URL is unknown to the inspected page).
10+
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!-- webkit-test-runner [ SiteIsolationEnabled=true ] -->
2+
<!DOCTYPE html>
3+
<html>
4+
<head>
5+
<meta charset="utf-8">
6+
<script src="../../../inspector/resources/inspector-test.js"></script>
7+
<script>
8+
function addCrossOriginIframeThatSetsCookie() {
9+
let crossOriginHost = location.hostname === "localhost" ? "127.0.0.1" : "localhost";
10+
let iframe = document.createElement("iframe");
11+
iframe.name = "cookie-frame";
12+
// Cross-origin => separate WebContent process under Site Isolation. The iframe's own script
13+
// sets a cookie for a path the parent process never fetches.
14+
iframe.src = `http://${crossOriginHost}:8000/site-isolation/inspector/storage/resources/set-cookie-via-script.html`;
15+
document.body.appendChild(iframe);
16+
}
17+
18+
function test()
19+
{
20+
let suite = InspectorTest.createAsyncSuite("SiteIsolation.Storage.getCookies");
21+
22+
suite.addTestCase({
23+
name: "SiteIsolation.Storage.getCookies.CrossOriginIframe",
24+
description: "A cookie set by script inside a cross-origin iframe (a separate WebContent process under Site Isolation) must be visible via Storage.getCookies, which reads the authoritative NetworkProcess store, and must NOT be visible via the in-process Page.getCookies, which can only find cookies for URLs the inspected page's process already knows about.",
25+
async test() {
26+
await WI.backendTarget.StorageAgent.enable();
27+
await InspectorTest.evaluateInPage(`if (window.testRunner) testRunner.setAlwaysAcceptCookies(true);`);
28+
29+
// Adding the cross-origin iframe spawns a Frame Target under Site Isolation. Await it
30+
// before asserting anything: TargetAdded is dispatched asynchronously, so the FrameTarget
31+
// is not registered synchronously after evaluateInPage() returns. Awaiting the event is
32+
// both the reliable "Site Isolation is active" signal and what lets the child's script run.
33+
let targetAddedPromise = WI.targetManager.awaitEvent(WI.TargetManager.Event.TargetAdded);
34+
InspectorTest.evaluateInPage("addCrossOriginIframeThatSetsCookie()");
35+
36+
let frameTarget = (await targetAddedPromise).data.target;
37+
InspectorTest.expectThat(frameTarget instanceof WI.FrameTarget, "Cross-origin iframe should be its own Frame Target (out-of-process).");
38+
39+
// A freshly-added cross-origin frame target starts provisional; wait for it to commit so
40+
// its script (which sets the cookie) has actually run in its own process.
41+
if (frameTarget.isProvisional)
42+
await WI.targetManager.awaitEvent(WI.TargetManager.Event.DidCommitProvisionalTarget);
43+
44+
// The cookie write from the child's script round-trips to the NetworkProcess; poll until
45+
// Storage.getCookies observes it.
46+
let cookie = null;
47+
for (let attempt = 0; attempt < 50 && !cookie; ++attempt) {
48+
let {cookies} = await WI.backendTarget.StorageAgent.getCookies();
49+
cookie = cookies.find((c) => c.name === "CrossOriginScriptCookie");
50+
if (!cookie)
51+
await new Promise((resolve) => setTimeout(resolve, 20));
52+
}
53+
54+
InspectorTest.expectNotNull(cookie, "Storage.getCookies should see the cross-origin iframe's cookie.");
55+
if (cookie)
56+
InspectorTest.expectEqual(cookie.value, "PASS", "Cross-origin cookie value should be 'PASS'.");
57+
58+
// Negative control: Page.getCookies runs in the inspected page's process and looks up
59+
// cookies only for URLs that process knows as resources. It has no resource URL for the
60+
// private-scope path, so it must not see this cookie. If it did, the iframe would not
61+
// be out-of-process and the test would not be exercising the Site Isolation path.
62+
let {cookies: pageCookies} = await WI.pageTarget.PageAgent.getCookies();
63+
let pageCookie = pageCookies.find((c) => c.name === "CrossOriginScriptCookie");
64+
InspectorTest.expectNull(pageCookie, "Page.getCookies must NOT see the cross-origin iframe's cookie (it is out-of-process and the URL is unknown to the inspected page).");
65+
}
66+
});
67+
68+
suite.runTestCasesAndFinish();
69+
}
70+
</script>
71+
</head>
72+
<body onload="runTest()">
73+
<p>Tests that Storage.getCookies sees a cookie set by script inside a cross-origin iframe under Site Isolation, which the in-process Page.getCookies cannot.</p>
74+
</body>
75+
</html>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Tests that Storage.getCookies returns cookies from the NetworkProcess-backed cookie store under Site Isolation.
2+
3+
4+
== Running test suite: Storage.getCookies
5+
-- Running test case: Storage.getCookies.Enable
6+
Storage domain enabled.
7+
8+
-- Running test case: Storage.getCookies.InitiallyEmpty
9+
PASS: Should be no cookies.
10+
11+
-- Running test case: Storage.getCookies.AfterSet
12+
PASS: Should have found the 'Baseline' cookie.
13+
PASS: Cookie value should be 'PASS'.
14+
PASS: Cookie domain should be '127.0.0.1'.
15+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<!-- webkit-test-runner [ SiteIsolationEnabled=true ] -->
2+
<!DOCTYPE html>
3+
<html>
4+
<head>
5+
<meta charset="utf-8">
6+
<script src="../../../inspector/resources/inspector-test.js"></script>
7+
<script>
8+
function loadScriptWithURL(url) {
9+
let script = document.createElement("script");
10+
script.src = url;
11+
script.addEventListener("load", (event) => {
12+
TestPage.dispatchEventToFrontend("ScriptLoad");
13+
});
14+
document.body.appendChild(script);
15+
}
16+
17+
function test()
18+
{
19+
let suite = InspectorTest.createAsyncSuite("Storage.getCookies");
20+
21+
suite.addTestCase({
22+
name: "Storage.getCookies.Enable",
23+
description: "Enable the Storage domain.",
24+
async test() {
25+
await WI.backendTarget.StorageAgent.enable();
26+
InspectorTest.log("Storage domain enabled.");
27+
}
28+
});
29+
30+
suite.addTestCase({
31+
name: "Storage.getCookies.InitiallyEmpty",
32+
description: "There should be no cookies before any are set.",
33+
async test() {
34+
let {cookies} = await WI.backendTarget.StorageAgent.getCookies();
35+
InspectorTest.expectEqual(cookies.length, 0, "Should be no cookies.");
36+
}
37+
});
38+
39+
suite.addTestCase({
40+
name: "Storage.getCookies.AfterSet",
41+
description: "A cookie set by a same-origin resource should be visible.",
42+
async test() {
43+
await Promise.all([
44+
InspectorTest.awaitEvent("ScriptLoad"),
45+
InspectorTest.evaluateInPage(`loadScriptWithURL("http://127.0.0.1:8000/site-isolation/inspector/storage/resources/set-cookie.py?name=Baseline&value=PASS")`),
46+
]);
47+
48+
let {cookies} = await WI.backendTarget.StorageAgent.getCookies();
49+
let cookie = cookies.find((c) => c.name === "Baseline");
50+
51+
InspectorTest.expectNotNull(cookie, "Should have found the 'Baseline' cookie.");
52+
InspectorTest.expectEqual(cookie.value, "PASS", "Cookie value should be 'PASS'.");
53+
InspectorTest.expectEqual(cookie.domain, "127.0.0.1", "Cookie domain should be '127.0.0.1'.");
54+
}
55+
});
56+
57+
suite.runTestCasesAndFinish();
58+
}
59+
</script>
60+
</head>
61+
<body onload="runTest()">
62+
<p>Tests that Storage.getCookies returns cookies from the NetworkProcess-backed cookie store under Site Isolation.</p>
63+
</body>
64+
</html>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Probe: does Storage.getCookies surface a CHIPS partitioned cookie (set via the Partitioned attribute) with its partitionKey populated?
2+
3+
4+
== Running test suite: Storage.getCookies.Partitioned
5+
-- Running test case: Storage.getCookies.Partitioned.Probe
6+
PASS: Storage.getCookies should surface the partitioned cookie from the flat store.
7+
PASS: Partitioned cookie value should be 'PASS'.
8+
PASS: Partitioned cookie should carry a non-empty partitionKey.
9+
Observed partitionKey is a non-empty string: true
10+
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<!-- webkit-test-runner [ SiteIsolationEnabled=true OptInPartitionedCookiesEnabled=true ] -->
2+
<!DOCTYPE html>
3+
<html>
4+
<head>
5+
<meta charset="utf-8">
6+
<script src="../../../inspector/resources/inspector-test.js"></script>
7+
<script>
8+
// Set a CHIPS partitioned cookie from the page itself. The page is a top-level https document, so
9+
// the cookie is partitioned under this document's own site (first-party partition). Partitioned
10+
// cookies require Secure + SameSite=None + the Partitioned attribute, hence the .https.html context
11+
// and the OptInPartitionedCookiesEnabled preference above.
12+
function setPartitionedCookie() {
13+
document.cookie = "PartitionedProbe=PASS; SameSite=None; Secure; Partitioned; path=/";
14+
}
15+
16+
function test()
17+
{
18+
let suite = InspectorTest.createAsyncSuite("Storage.getCookies.Partitioned");
19+
20+
suite.addTestCase({
21+
name: "Storage.getCookies.Partitioned.Probe",
22+
description: "Probe whether Storage.getCookies (which reads the flat authoritative store via API::HTTPCookieStore) surfaces a CHIPS partitioned cookie, and whether its partitionKey is populated. This settles whether partitioned cookies are visible now or genuinely need a separate per-partition read path.",
23+
async test() {
24+
await WI.backendTarget.StorageAgent.enable();
25+
await InspectorTest.evaluateInPage(`if (window.testRunner) testRunner.setAlwaysAcceptCookies(true);`);
26+
27+
await InspectorTest.evaluateInPage("setPartitionedCookie()");
28+
29+
// The write round-trips to the NetworkProcess; poll until getCookies observes the cookie
30+
// (or we exhaust attempts, in which case the probe result is "not visible").
31+
let cookie = null;
32+
for (let attempt = 0; attempt < 50 && !cookie; ++attempt) {
33+
let {cookies} = await WI.backendTarget.StorageAgent.getCookies();
34+
cookie = cookies.find((c) => c.name === "PartitionedProbe");
35+
if (!cookie)
36+
await new Promise((resolve) => setTimeout(resolve, 20));
37+
}
38+
39+
InspectorTest.expectNotNull(cookie, "Storage.getCookies should surface the partitioned cookie from the flat store.");
40+
if (cookie) {
41+
InspectorTest.expectEqual(cookie.value, "PASS", "Partitioned cookie value should be 'PASS'.");
42+
// The interesting bit: does the flat read carry the partition label through to the
43+
// protocol object? Log presence explicitly so the expected output records reality.
44+
InspectorTest.expectNotNull(cookie.partitionKey, "Partitioned cookie should carry a non-empty partitionKey.");
45+
InspectorTest.log("Observed partitionKey is a non-empty string: " + (typeof cookie.partitionKey === "string" && cookie.partitionKey.length > 0));
46+
}
47+
}
48+
});
49+
50+
suite.runTestCasesAndFinish();
51+
}
52+
</script>
53+
</head>
54+
<body onload="runTest()">
55+
<p>Probe: does Storage.getCookies surface a CHIPS partitioned cookie (set via the Partitioned attribute) with its partitionKey populated?</p>
56+
</body>
57+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Tests Storage.setCookie (with partitioning) and Storage.deleteCookies by filter under Site Isolation.
2+
3+
4+
== Running test suite: Storage.setCookie and Storage.deleteCookies
5+
-- Running test case: Storage.setCookie.Enable
6+
Storage domain enabled.
7+
8+
-- Running test case: Storage.setCookie.Roundtrip
9+
PASS: setCookie should return a partitionKey.
10+
PASS: An unpartitioned cookie's returned partitionKey should have no sourceOrigin.
11+
PASS: Should read back exactly one matching cookie.
12+
PASS: Cookie value should be 'PASS'.
13+
14+
-- Running test case: Storage.setCookie.ExplicitPartitionKeyEchoed
15+
PASS: setCookie should return a partitionKey.
16+
PASS: Returned partitionKey should echo the explicit cookie partitionKey.
17+
18+
-- Running test case: Storage.deleteCookies.ByFilter
19+
PASS: 'DeleteMe' should be gone.
20+
PASS: 'KeepMe' should remain.
21+
22+
-- Running test case: Storage.deleteCookies.NoFilterRejected
23+
PASS: deleteCookies with no filter should produce an error.
24+
PASS: 'KeepMe' should still remain after the rejected delete.
25+

0 commit comments

Comments
 (0)