@ai-ecoverse/slicc-browsers 1.23.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/fixture-server.mjs +19 -0
- package/guard-runner.mjs +33 -2
- package/guard-vectors.json +202 -1
- package/package.json +5 -4
- package/protected-parent-iframes.mjs +567 -0
- package/schema/guard-vectors.schema.json +16 -0
package/README.md
CHANGED
|
@@ -195,7 +195,7 @@ For stock Playwright, Puppeteer and raw CDP clients, a guard that declares `stoc
|
|
|
195
195
|
- `Page.addScriptToEvaluateOnNewDocument` whose `source` matches `^(?:[ \t\n]|//[^\n\r\u2028\u2029]*)*$` on the whole string without the multiline flag (no code), with any `worldName` or `runImmediately`, and its removal;
|
|
196
196
|
- exactly `Emulation.setFocusEmulationEnabled`, `setEmulatedMedia`, `setDeviceMetricsOverride` and `setTouchEmulationEnabled`;
|
|
197
197
|
- `Page.createIsolatedWorld` on a frame the guard already classifies as ordinary: strip `grantUniveralAccess` and `contentSecurityPolicy`, return no `executionContextId` for any other frame; `Runtime.addBinding` scoped to such a world by a non-empty `executionContextName` or its `executionContextId`, with `Runtime.bindingCalled` only from those worlds;
|
|
198
|
-
- `Target.setAutoAttach`: either narrow the filter to tab targets or hide per target; no protected-origin page, iframe
|
|
198
|
+
- `Target.setAutoAttach`: either narrow the filter to tab targets or hide per target; no protected-origin page, iframe (including ordinary-URL children of a protected parent) or worker may reach the client; pages under the tab are held like pages (same URL redaction in `targetInfo`); a transport that synthesizes it declares `syntheticAutoAttach`.
|
|
199
199
|
|
|
200
200
|
The strip holds whether or not a given Chrome lets that world reach another origin. Code init scripts, main-world bindings and other overrides stay refused or harmless. Admitted display overrides and no-code init scripts must not remain in effect on a protected document — refuse that navigation, or clear them before it runs. Undeclared, they skip, never pass.
|
|
201
201
|
|
|
@@ -223,7 +223,7 @@ await fixture.close();
|
|
|
223
223
|
You supply:
|
|
224
224
|
- **a guarded client** per row: your guard's CDP connection, with `send(method, params, sessionId)`, `waitEvent(predicate, ms)` and an `events` array;
|
|
225
225
|
- **an unguarded observer**: a direct CDP connection to the same browser, which runs the controls and checks liveness;
|
|
226
|
-
- **origins**: the fixture server reachable as seven, the vault and
|
|
226
|
+
- **origins**: the fixture server reachable as seven, the vault, an ordinary site, and for some rows a cross-site ordinary `{cross}` (e.g. `cross.test`). Point those hostnames at it with Chrome's `--host-resolver-rules`, so tests never touch the real seven;
|
|
227
227
|
- **capabilities**: what your transport can do. A row that needs something you don't declare is skipped as `not-supported:<capability>`.
|
|
228
228
|
|
|
229
229
|
| capability | means the transport can |
|
package/fixture-server.mjs
CHANGED
|
@@ -101,6 +101,25 @@ function route(req, url, send, html, redirects) {
|
|
|
101
101
|
`<script>navigator.sendBeacon(${JSON.stringify(pre)})</script><iframe src="${escape(to)}"></iframe>`
|
|
102
102
|
);
|
|
103
103
|
},
|
|
104
|
+
'/oopif-nest': () => {
|
|
105
|
+
const mid = p.get('mid') ?? 'about:blank';
|
|
106
|
+
return html(
|
|
107
|
+
'oopif-nest',
|
|
108
|
+
`<p>oopif-nest ${escape(p.get('n') ?? '')}</p><iframe src="${escape(mid)}"></iframe>`
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
'/oopif-mid': () => {
|
|
112
|
+
const inner = p.get('inner') ?? 'about:blank';
|
|
113
|
+
return html(
|
|
114
|
+
'oopif-mid',
|
|
115
|
+
`<p>oopif-mid ${escape(p.get('n') ?? '')}</p><iframe src="${escape(inner)}"></iframe>`
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
'/oopif-srcdoc': () =>
|
|
119
|
+
html(
|
|
120
|
+
'oopif-srcdoc',
|
|
121
|
+
`<p>oopif-srcdoc ${escape(p.get('n') ?? '')}</p><iframe sandbox="allow-scripts allow-forms" srcdoc="${escape('<!doctype html><title>srcdoc</title><p>ordinary srcdoc</p>')}"></iframe>`
|
|
122
|
+
),
|
|
104
123
|
'/bfcache': () => {
|
|
105
124
|
const marker = JSON.stringify(`bfcache-marker-${p.get('n') ?? ''}`).replaceAll(
|
|
106
125
|
'<',
|
package/guard-runner.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from './guard-driver.mjs';
|
|
33
33
|
import { createObserverTarget, discoveryViolation } from './observer-target.mjs';
|
|
34
34
|
import { protectedErrorPage } from './protected-error-page.mjs';
|
|
35
|
+
import { protectedParentIframes } from './protected-parent-iframes.mjs';
|
|
35
36
|
import {
|
|
36
37
|
leftoverPersistentWorker,
|
|
37
38
|
persistentWorkerTypes,
|
|
@@ -901,6 +902,7 @@ class Execution {
|
|
|
901
902
|
'unparseable',
|
|
902
903
|
'errorPage',
|
|
903
904
|
'closeProtectedShape',
|
|
905
|
+
'protectedParentIframes',
|
|
904
906
|
'extensionWorld',
|
|
905
907
|
'extensionFrame',
|
|
906
908
|
'frameRead',
|
|
@@ -912,6 +914,7 @@ class Execution {
|
|
|
912
914
|
[
|
|
913
915
|
'errorPage',
|
|
914
916
|
'closeProtectedShape',
|
|
917
|
+
'protectedParentIframes',
|
|
915
918
|
'redirectSecret',
|
|
916
919
|
'extensionFrame',
|
|
917
920
|
'frameRead',
|
|
@@ -1016,6 +1019,7 @@ class Execution {
|
|
|
1016
1019
|
step.redirectSecret ||
|
|
1017
1020
|
step.errorPage ||
|
|
1018
1021
|
step.closeProtectedShape ||
|
|
1022
|
+
step.protectedParentIframes ||
|
|
1019
1023
|
step.spawnWorker ||
|
|
1020
1024
|
step.probeSessions
|
|
1021
1025
|
)
|
|
@@ -1163,6 +1167,7 @@ class Execution {
|
|
|
1163
1167
|
stepProbe(step, session) {
|
|
1164
1168
|
if (step.redirectSecret || step.errorPage) return this.originProbe(step, session);
|
|
1165
1169
|
if (step.closeProtectedShape) return this.runCloseProtectedShape(step);
|
|
1170
|
+
if (step.protectedParentIframes) return this.runProtectedParentIframes(step);
|
|
1166
1171
|
if (step.spawnWorker) return this.spawnWorker(step);
|
|
1167
1172
|
return this.probeSessions(step);
|
|
1168
1173
|
}
|
|
@@ -1228,6 +1233,27 @@ class Execution {
|
|
|
1228
1233
|
);
|
|
1229
1234
|
}
|
|
1230
1235
|
|
|
1236
|
+
async runProtectedParentIframes(step) {
|
|
1237
|
+
const out = await outcome(
|
|
1238
|
+
protectedParentIframes({
|
|
1239
|
+
client: this.client,
|
|
1240
|
+
reader: this.reader,
|
|
1241
|
+
protectedOrigin: this.vars.target,
|
|
1242
|
+
cross: this.options.targets?.cross,
|
|
1243
|
+
ordinary: this.vars.ordinary,
|
|
1244
|
+
mode: step.protectedParentIframes,
|
|
1245
|
+
tag: this.vars.tag,
|
|
1246
|
+
internalSessions: this.internalSessions,
|
|
1247
|
+
notes: this.notes,
|
|
1248
|
+
budget: this.bound(5000),
|
|
1249
|
+
discovery: supported(['discovery'], this.options.capabilities),
|
|
1250
|
+
eventStart: this.eventStart,
|
|
1251
|
+
})
|
|
1252
|
+
);
|
|
1253
|
+
if (out.ok && out.result?.pageId) this.targets.push(out.result.pageId);
|
|
1254
|
+
return out;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1231
1257
|
spawnWorkerKind() {
|
|
1232
1258
|
return allSteps([...(this.vector.setup ?? []), ...(this.vector.trigger ?? [])]).find(
|
|
1233
1259
|
(step) => step.spawnWorker
|
|
@@ -2214,10 +2240,15 @@ const observerRequired = (check) => {
|
|
|
2214
2240
|
return check?.not ? observerRequired(check.not) : false;
|
|
2215
2241
|
};
|
|
2216
2242
|
const needsObserver = (vector) =>
|
|
2217
|
-
vector.control.via === 'unguarded' ||
|
|
2218
2243
|
allSteps([...(vector.setup ?? []), ...(vector.trigger ?? [])]).some(
|
|
2219
|
-
(step) =>
|
|
2244
|
+
(step) =>
|
|
2245
|
+
step.isolatedWorld ||
|
|
2246
|
+
step.observer ||
|
|
2247
|
+
step.spawnWorker ||
|
|
2248
|
+
step.closeProtectedShape ||
|
|
2249
|
+
step.protectedParentIframes
|
|
2220
2250
|
) ||
|
|
2251
|
+
vector.control.via === 'unguarded' ||
|
|
2221
2252
|
observerRequired(vector.liveCheck) ||
|
|
2222
2253
|
Object.values(vector.observe ?? {}).some(
|
|
2223
2254
|
(observation) => observerRequired(observation.check) || observerRequired(observation.eitherWhen)
|
package/guard-vectors.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"setVersion": "1.
|
|
3
|
+
"setVersion": "1.24.0",
|
|
4
4
|
"sources": [
|
|
5
5
|
{
|
|
6
6
|
"repo": "ai-ecoverse/slicc-cli",
|
|
@@ -31562,6 +31562,207 @@
|
|
|
31562
31562
|
"workerReachable": false
|
|
31563
31563
|
},
|
|
31564
31564
|
"designDependent": ["attachRefused", "workerAttached"]
|
|
31565
|
+
},
|
|
31566
|
+
|
|
31567
|
+
{
|
|
31568
|
+
"id": "C3.protected-parent-iframes",
|
|
31569
|
+
"family": "C",
|
|
31570
|
+
"kind": "cdp",
|
|
31571
|
+
"scope": "any",
|
|
31572
|
+
"setup": [
|
|
31573
|
+
{
|
|
31574
|
+
"text": "a protected page frames a cross-site ordinary iframe that frames a further ordinary iframe; the guarded client must not list, discover, or attach to either ordinary iframe target"
|
|
31575
|
+
},
|
|
31576
|
+
{
|
|
31577
|
+
"page": "tab",
|
|
31578
|
+
"url": "{ordinary}/blank"
|
|
31579
|
+
}
|
|
31580
|
+
],
|
|
31581
|
+
"trigger": [
|
|
31582
|
+
{
|
|
31583
|
+
"protectedParentIframes": "nested-cross",
|
|
31584
|
+
"as": "probe",
|
|
31585
|
+
"requires": ["browserSession"]
|
|
31586
|
+
}
|
|
31587
|
+
],
|
|
31588
|
+
"targets": ["seven", "vault"],
|
|
31589
|
+
"requires": ["browserSession"],
|
|
31590
|
+
"observe": {
|
|
31591
|
+
"iframeListed": {
|
|
31592
|
+
"means": "C3.protected-parent-iframes: Target.getTargets through the client shows an ordinary iframe under the protected parent by id, URL, or parentId chain",
|
|
31593
|
+
"check": {
|
|
31594
|
+
"result": "probe",
|
|
31595
|
+
"path": "result.iframeListed",
|
|
31596
|
+
"equals": true,
|
|
31597
|
+
"observer": "client"
|
|
31598
|
+
},
|
|
31599
|
+
"absencePoll": true
|
|
31600
|
+
},
|
|
31601
|
+
"infoRead": {
|
|
31602
|
+
"means": "C3.protected-parent-iframes: Target.getTargetInfo through the client returns either ordinary iframe under the protected parent",
|
|
31603
|
+
"check": {
|
|
31604
|
+
"result": "probe",
|
|
31605
|
+
"path": "result.infoRead",
|
|
31606
|
+
"equals": true,
|
|
31607
|
+
"observer": "client"
|
|
31608
|
+
},
|
|
31609
|
+
"absencePoll": true
|
|
31610
|
+
},
|
|
31611
|
+
"eventsSeen": {
|
|
31612
|
+
"means": "C3.protected-parent-iframes: a discovery event carries either ordinary iframe under the protected parent",
|
|
31613
|
+
"requires": ["discovery"],
|
|
31614
|
+
"check": {
|
|
31615
|
+
"result": "probe",
|
|
31616
|
+
"path": "result.eventsSeen",
|
|
31617
|
+
"equals": true,
|
|
31618
|
+
"observer": "client"
|
|
31619
|
+
},
|
|
31620
|
+
"absencePoll": true
|
|
31621
|
+
},
|
|
31622
|
+
"attachSession": {
|
|
31623
|
+
"means": "C3.protected-parent-iframes: Target.attachToTarget on a raw ordinary iframe id returned a session",
|
|
31624
|
+
"check": {
|
|
31625
|
+
"result": "probe",
|
|
31626
|
+
"path": "result.attachSession",
|
|
31627
|
+
"equals": true,
|
|
31628
|
+
"observer": "client"
|
|
31629
|
+
}
|
|
31630
|
+
},
|
|
31631
|
+
"attachRefused": {
|
|
31632
|
+
"means": "C3.protected-parent-iframes: Target.attachToTarget on a raw ordinary iframe id was refused",
|
|
31633
|
+
"check": {
|
|
31634
|
+
"result": "probe",
|
|
31635
|
+
"path": "result.attachRefused",
|
|
31636
|
+
"equals": true,
|
|
31637
|
+
"observer": "client"
|
|
31638
|
+
}
|
|
31639
|
+
}
|
|
31640
|
+
},
|
|
31641
|
+
"live": "the unguarded reader opened the protected host page",
|
|
31642
|
+
"liveCheck": {
|
|
31643
|
+
"result": "probe",
|
|
31644
|
+
"path": "result.live",
|
|
31645
|
+
"equals": true,
|
|
31646
|
+
"observer": "client"
|
|
31647
|
+
},
|
|
31648
|
+
"control": {
|
|
31649
|
+
"origin": "ordinary",
|
|
31650
|
+
"via": "unguarded",
|
|
31651
|
+
"expected": {
|
|
31652
|
+
"iframeListed": true,
|
|
31653
|
+
"infoRead": true,
|
|
31654
|
+
"eventsSeen": true,
|
|
31655
|
+
"attachSession": true,
|
|
31656
|
+
"attachRefused": false
|
|
31657
|
+
}
|
|
31658
|
+
},
|
|
31659
|
+
"expected": {
|
|
31660
|
+
"iframeListed": false,
|
|
31661
|
+
"infoRead": false,
|
|
31662
|
+
"eventsSeen": false,
|
|
31663
|
+
"attachSession": false,
|
|
31664
|
+
"attachRefused": true
|
|
31665
|
+
}
|
|
31666
|
+
},
|
|
31667
|
+
{
|
|
31668
|
+
"id": "C3.protected-parent-srcdoc",
|
|
31669
|
+
"family": "C",
|
|
31670
|
+
"kind": "cdp",
|
|
31671
|
+
"scope": "any",
|
|
31672
|
+
"setup": [
|
|
31673
|
+
{
|
|
31674
|
+
"text": "a seven page frames a sandboxed srcdoc iframe without allow-same-origin; when Chrome gives it its own target the guarded client must hide it, otherwise the row is incomplete with that evidence"
|
|
31675
|
+
},
|
|
31676
|
+
{
|
|
31677
|
+
"page": "tab",
|
|
31678
|
+
"url": "{ordinary}/blank"
|
|
31679
|
+
}
|
|
31680
|
+
],
|
|
31681
|
+
"trigger": [
|
|
31682
|
+
{
|
|
31683
|
+
"protectedParentIframes": "sandboxed-srcdoc",
|
|
31684
|
+
"as": "probe",
|
|
31685
|
+
"requires": ["browserSession"]
|
|
31686
|
+
}
|
|
31687
|
+
],
|
|
31688
|
+
"targets": ["seven"],
|
|
31689
|
+
"requires": ["browserSession"],
|
|
31690
|
+
"observe": {
|
|
31691
|
+
"iframeListed": {
|
|
31692
|
+
"means": "C3.protected-parent-srcdoc: Target.getTargets through the client shows the sandboxed srcdoc iframe under seven by id, URL, or parentId chain",
|
|
31693
|
+
"check": {
|
|
31694
|
+
"result": "probe",
|
|
31695
|
+
"path": "result.iframeListed",
|
|
31696
|
+
"equals": true,
|
|
31697
|
+
"observer": "client"
|
|
31698
|
+
},
|
|
31699
|
+
"absencePoll": true
|
|
31700
|
+
},
|
|
31701
|
+
"infoRead": {
|
|
31702
|
+
"means": "C3.protected-parent-srcdoc: Target.getTargetInfo through the client returns the sandboxed srcdoc iframe under seven",
|
|
31703
|
+
"check": {
|
|
31704
|
+
"result": "probe",
|
|
31705
|
+
"path": "result.infoRead",
|
|
31706
|
+
"equals": true,
|
|
31707
|
+
"observer": "client"
|
|
31708
|
+
},
|
|
31709
|
+
"absencePoll": true
|
|
31710
|
+
},
|
|
31711
|
+
"eventsSeen": {
|
|
31712
|
+
"means": "C3.protected-parent-srcdoc: a discovery event carries the sandboxed srcdoc iframe under seven",
|
|
31713
|
+
"requires": ["discovery"],
|
|
31714
|
+
"check": {
|
|
31715
|
+
"result": "probe",
|
|
31716
|
+
"path": "result.eventsSeen",
|
|
31717
|
+
"equals": true,
|
|
31718
|
+
"observer": "client"
|
|
31719
|
+
},
|
|
31720
|
+
"absencePoll": true
|
|
31721
|
+
},
|
|
31722
|
+
"attachSession": {
|
|
31723
|
+
"means": "C3.protected-parent-srcdoc: Target.attachToTarget on the raw srcdoc iframe id returned a session",
|
|
31724
|
+
"check": {
|
|
31725
|
+
"result": "probe",
|
|
31726
|
+
"path": "result.attachSession",
|
|
31727
|
+
"equals": true,
|
|
31728
|
+
"observer": "client"
|
|
31729
|
+
}
|
|
31730
|
+
},
|
|
31731
|
+
"attachRefused": {
|
|
31732
|
+
"means": "C3.protected-parent-srcdoc: Target.attachToTarget on the raw srcdoc iframe id was refused",
|
|
31733
|
+
"check": {
|
|
31734
|
+
"result": "probe",
|
|
31735
|
+
"path": "result.attachRefused",
|
|
31736
|
+
"equals": true,
|
|
31737
|
+
"observer": "client"
|
|
31738
|
+
}
|
|
31739
|
+
}
|
|
31740
|
+
},
|
|
31741
|
+
"live": "the unguarded reader opened the protected host page",
|
|
31742
|
+
"liveCheck": {
|
|
31743
|
+
"result": "probe",
|
|
31744
|
+
"path": "result.live",
|
|
31745
|
+
"equals": true,
|
|
31746
|
+
"observer": "client"
|
|
31747
|
+
},
|
|
31748
|
+
"control": {
|
|
31749
|
+
"origin": "ordinary",
|
|
31750
|
+
"via": "unguarded",
|
|
31751
|
+
"expected": {
|
|
31752
|
+
"iframeListed": true,
|
|
31753
|
+
"infoRead": true,
|
|
31754
|
+
"eventsSeen": true,
|
|
31755
|
+
"attachSession": true,
|
|
31756
|
+
"attachRefused": false
|
|
31757
|
+
}
|
|
31758
|
+
},
|
|
31759
|
+
"expected": {
|
|
31760
|
+
"iframeListed": false,
|
|
31761
|
+
"infoRead": false,
|
|
31762
|
+
"eventsSeen": false,
|
|
31763
|
+
"attachSession": false,
|
|
31764
|
+
"attachRefused": true
|
|
31765
|
+
}
|
|
31565
31766
|
}
|
|
31566
31767
|
]
|
|
31567
31768
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-browsers",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"bfcache-restore.mjs",
|
|
68
68
|
"close-protected-shape.mjs",
|
|
69
69
|
"protected-worker.mjs",
|
|
70
|
+
"protected-parent-iframes.mjs",
|
|
70
71
|
"fixture-client.mjs"
|
|
71
72
|
],
|
|
72
73
|
"scripts": {
|
|
@@ -75,7 +76,7 @@
|
|
|
75
76
|
"validate": "node scripts/validate.mjs",
|
|
76
77
|
"lint": "slicc-lint && test -z \"$(gofmt -l .)\" && go mod tidy -diff && go vet ./... && GOOS=darwin go vet ./... && GOOS=windows go vet ./... && golangci-lint run && cargo fmt --check && cargo clippy --all-features --all-targets -- -D warnings && cargo check --target wasm32-wasip1 --no-default-features && cargo check --target wasm32-wasip1 --features serde && cargo check --target wasm32-wasip1 --all-features",
|
|
77
78
|
"format": "biome check --write . && gofmt -w . && cargo fmt",
|
|
78
|
-
"test": "export PATH=\"$HOME/.cargo/bin:$PATH\" && node --test --test-concurrency=1 test/integration/debugger-stage1-packaging.test.mjs test/integration/debugger-raw-control.test.mjs test/integration/debugger-cleanup.test.mjs test/integration/debugger-stage1-install-vectors.test.mjs test/integration/protected-premise.test.mjs test/integration/debugger-stage1-vectors.test.mjs test/integration/self-contained.test.mjs test/integration/seven-corpus.test.mjs test/integration/browser-runner.test.mjs test/integration/fixture-client.test.mjs test/integration/guard-redirect-carrier.test.mjs test/integration/guard-redirect-session-end.test.mjs test/integration/guard-redirect-history.test.mjs test/integration/guard-redirect-iframe.test.mjs test/integration/guard-incomplete.test.mjs test/integration/guard-unparseable-wipe.test.mjs test/integration/guard-browser-ui.test.mjs test/integration/guard-snapshot-settle.test.mjs test/integration/guard-stall.test.mjs test/integration/guard-forged.test.mjs test/integration/guard-trusted-emulation.test.mjs test/integration/guard-exit-steps.test.mjs test/integration/guard-exit-refusal.test.mjs test/integration/guard-frame-read.test.mjs test/integration/guard-bfcache.test.mjs test/integration/guard-bfcache-marker.test.mjs test/integration/guard-bfcache-history.test.mjs test/integration/guard-bfcache-review.test.mjs test/integration/guard-bfcache-quarantine.test.mjs test/integration/guard-error-page-navigation.test.mjs test/integration/guard-error-page-events.test.mjs test/integration/guard-error-page-reload.test.mjs test/integration/guard-close-protected.test.mjs test/integration/guard-close-protected-shapes.test.mjs test/integration/guard-close-protected-shapes-chrome.test.mjs test/integration/guard-world-paint.test.mjs test/integration/gates.test.mjs test/integration/validate.test.mjs test/integration/validate-guard-stock-setup.test.mjs test/integration/go.test.mjs test/integration/cargo.test.mjs test/integration/workflows.test.mjs test/integration/guard-refusable-nav.test.mjs test/integration/guard-protected-worker.test.mjs test/integration/guard-protected-worker-chrome-narrow.test.mjs test/integration/guard-protected-worker-chrome-hide.test.mjs test/integration/guard-protected-worker-chrome-hide-cut.test.mjs test/integration/guard-protected-worker-chrome-admit.test.mjs test/integration/guard-protected-worker-chrome-late.test.mjs test/integration/guard-protected-worker-chrome-rewrite.test.mjs test/integration/guard-protected-worker-chrome-rewrite-late.test.mjs test/integration/guard-sweep-union.test.mjs && node --test --test-concurrency=2 test/integration/guard-lifecycle.test.mjs test/integration/chromium-launch.test.mjs test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs test/integration/guard-late-pause.test.mjs test/integration/guard-legs.test.mjs test/integration/guard-fetch-enable.test.mjs test/integration/guard-diagnostics.test.mjs test/integration/guard-chrome.test.mjs test/integration/guard-worlds.test.mjs test/integration/guard-world-discovery.test.mjs test/integration/guard-world-guesses.test.mjs test/integration/guard-unparseable.test.mjs test/integration/guard-redirect-secret.test.mjs test/integration/guard-error-page.test.mjs test/integration/guard-discovery.test.mjs test/integration/guard-bfcache-chrome.test.mjs test/integration/guard-browser-page.test.mjs test/integration/guard-scales.test.mjs test/integration/guard-shared-log.test.mjs test/integration/guard-pre-beacon.test.mjs test/integration/guard-world-pause.test.mjs test/integration/guard-reveal-overlap.test.mjs test/integration/guard-extension-frame-mouse.test.mjs test/integration/guard-extension-frames.test.mjs test/integration/guard-stock-setup.test.mjs test/integration/guard-stock-attack.test.mjs test/integration/guard-parallel.test.mjs test/integration/guard-client-exit.test.mjs test/integration/guard-runner.test.mjs test/integration/guard-sweep-ordinary.test.mjs test/integration/guard-sweep-seven.test.mjs test/integration/guard-sweep-vault.test.mjs test/integration/guard-sweep-vault-x.test.mjs test/integration/guard-sweep-apex.test.mjs test/integration/guard-sweep-others.test.mjs",
|
|
79
|
+
"test": "export PATH=\"$HOME/.cargo/bin:$PATH\" && node --test --test-concurrency=1 test/integration/debugger-stage1-packaging.test.mjs test/integration/debugger-raw-control.test.mjs test/integration/debugger-cleanup.test.mjs test/integration/debugger-stage1-install-vectors.test.mjs test/integration/protected-premise.test.mjs test/integration/debugger-stage1-vectors.test.mjs test/integration/self-contained.test.mjs test/integration/seven-corpus.test.mjs test/integration/browser-runner.test.mjs test/integration/fixture-client.test.mjs test/integration/guard-redirect-carrier.test.mjs test/integration/guard-redirect-session-end.test.mjs test/integration/guard-redirect-history.test.mjs test/integration/guard-redirect-iframe.test.mjs test/integration/guard-incomplete.test.mjs test/integration/guard-unparseable-wipe.test.mjs test/integration/guard-browser-ui.test.mjs test/integration/guard-snapshot-settle.test.mjs test/integration/guard-stall.test.mjs test/integration/guard-forged.test.mjs test/integration/guard-trusted-emulation.test.mjs test/integration/guard-exit-steps.test.mjs test/integration/guard-exit-refusal.test.mjs test/integration/guard-frame-read.test.mjs test/integration/guard-bfcache.test.mjs test/integration/guard-bfcache-marker.test.mjs test/integration/guard-bfcache-history.test.mjs test/integration/guard-bfcache-review.test.mjs test/integration/guard-bfcache-quarantine.test.mjs test/integration/guard-error-page-navigation.test.mjs test/integration/guard-error-page-events.test.mjs test/integration/guard-error-page-reload.test.mjs test/integration/guard-close-protected.test.mjs test/integration/guard-close-protected-shapes.test.mjs test/integration/guard-close-protected-shapes-chrome.test.mjs test/integration/guard-protected-parent-iframes.test.mjs test/integration/guard-protected-parent-iframes-chrome.test.mjs test/integration/guard-world-paint.test.mjs test/integration/gates.test.mjs test/integration/validate.test.mjs test/integration/validate-guard-stock-setup.test.mjs test/integration/go.test.mjs test/integration/cargo.test.mjs test/integration/workflows.test.mjs test/integration/guard-refusable-nav.test.mjs test/integration/guard-protected-worker.test.mjs test/integration/guard-protected-worker-chrome-narrow.test.mjs test/integration/guard-protected-worker-chrome-hide.test.mjs test/integration/guard-protected-worker-chrome-hide-cut.test.mjs test/integration/guard-protected-worker-chrome-admit.test.mjs test/integration/guard-protected-worker-chrome-late.test.mjs test/integration/guard-protected-worker-chrome-rewrite.test.mjs test/integration/guard-protected-worker-chrome-rewrite-late.test.mjs test/integration/guard-sweep-union.test.mjs && node --test --test-concurrency=2 test/integration/guard-lifecycle.test.mjs test/integration/chromium-launch.test.mjs test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs test/integration/guard-late-pause.test.mjs test/integration/guard-legs.test.mjs test/integration/guard-fetch-enable.test.mjs test/integration/guard-diagnostics.test.mjs test/integration/guard-chrome.test.mjs test/integration/guard-worlds.test.mjs test/integration/guard-world-discovery.test.mjs test/integration/guard-world-guesses.test.mjs test/integration/guard-unparseable.test.mjs test/integration/guard-redirect-secret.test.mjs test/integration/guard-error-page.test.mjs test/integration/guard-discovery.test.mjs test/integration/guard-bfcache-chrome.test.mjs test/integration/guard-browser-page.test.mjs test/integration/guard-scales.test.mjs test/integration/guard-shared-log.test.mjs test/integration/guard-pre-beacon.test.mjs test/integration/guard-world-pause.test.mjs test/integration/guard-reveal-overlap.test.mjs test/integration/guard-extension-frame-mouse.test.mjs test/integration/guard-extension-frames.test.mjs test/integration/guard-stock-setup.test.mjs test/integration/guard-stock-attack.test.mjs test/integration/guard-parallel.test.mjs test/integration/guard-client-exit.test.mjs test/integration/guard-runner.test.mjs test/integration/guard-sweep-ordinary.test.mjs test/integration/guard-sweep-seven.test.mjs test/integration/guard-sweep-vault.test.mjs test/integration/guard-sweep-vault-x.test.mjs test/integration/guard-sweep-apex.test.mjs test/integration/guard-sweep-others.test.mjs",
|
|
79
80
|
"test:unit": "mkdir -p coverage/unit && node --experimental-test-coverage --test --test-concurrency=1 --test-reporter=lcov --test-reporter-destination=coverage/unit/lcov.info test/unit/*.test.mjs && go test -race -cover ./...",
|
|
80
81
|
"build": "go build ./... && cargo build --all-features && cargo build --target wasm32-wasip1 --all-features && npm pack --dry-run",
|
|
81
82
|
"prepublishOnly": "node scripts/set-crate-version.mjs \"$npm_package_version\"",
|
|
@@ -83,10 +84,10 @@
|
|
|
83
84
|
"trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
|
|
84
85
|
"release": "semantic-release",
|
|
85
86
|
"test:unit:redirect": "mkdir -p coverage/unit && node --experimental-test-coverage --test --test-concurrency=1 --test-coverage-include=redirect-secret.mjs --test-coverage-include=redirect-fragments.mjs --test-reporter=lcov --test-reporter-destination=coverage/unit/redirect.lcov test/unit/guard-redirect-*.test.mjs && slicc-diff-cover coverage/unit/redirect.lcov",
|
|
86
|
-
"test:unit:guard": "mkdir -p coverage/unit && node --experimental-vm-modules --no-warnings --experimental-test-coverage --test --test-concurrency=1 --test-coverage-include=guard-runner.mjs --test-coverage-include=guard-driver.mjs --test-coverage-include=extension-frame.mjs --test-coverage-include=fixture-server.mjs --test-coverage-include=fixture-client.mjs --test-coverage-include=observer-target.mjs --test-coverage-include=frame-read.mjs --test-coverage-include=bfcache-restore.mjs --test-coverage-include=protected-error-page.mjs --test-coverage-include=unparseable-url.mjs --test-coverage-include=scripts/browser-runner.mjs --test-coverage-include=scripts/validate.mjs --test-reporter=lcov --test-reporter-destination=coverage/unit/guard.lcov test/unit/guard-*.test.mjs && slicc-diff-cover coverage/unit/guard.lcov"
|
|
87
|
+
"test:unit:guard": "mkdir -p coverage/unit && node --experimental-vm-modules --no-warnings --experimental-test-coverage --test --test-concurrency=1 --test-coverage-include=guard-runner.mjs --test-coverage-include=guard-driver.mjs --test-coverage-include=extension-frame.mjs --test-coverage-include=fixture-server.mjs --test-coverage-include=fixture-client.mjs --test-coverage-include=observer-target.mjs --test-coverage-include=frame-read.mjs --test-coverage-include=bfcache-restore.mjs --test-coverage-include=protected-error-page.mjs --test-coverage-include=protected-parent-iframes.mjs --test-coverage-include=unparseable-url.mjs --test-coverage-include=scripts/browser-runner.mjs --test-coverage-include=scripts/validate.mjs --test-reporter=lcov --test-reporter-destination=coverage/unit/guard.lcov test/unit/guard-*.test.mjs && slicc-diff-cover coverage/unit/guard.lcov"
|
|
87
88
|
},
|
|
88
89
|
"devDependencies": {
|
|
89
|
-
"@ai-ecoverse/slicc-cdp": "1.48.
|
|
90
|
+
"@ai-ecoverse/slicc-cdp": "1.48.10",
|
|
90
91
|
"@ai-ecoverse/slicc-shared-web": "2.14.2",
|
|
91
92
|
"@biomejs/biome": "2.6.5",
|
|
92
93
|
"ajv": "8.20.0",
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { outcome, waitFor } from './guard-driver.mjs';
|
|
2
|
+
import { createObserverTarget } from './observer-target.mjs';
|
|
3
|
+
|
|
4
|
+
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
|
|
6
|
+
const refused = (out) => {
|
|
7
|
+
if (out.ok) return false;
|
|
8
|
+
const message = String(out.error?.message ?? out.error ?? '');
|
|
9
|
+
if (/^slicc: refused:/.test(message)) return true;
|
|
10
|
+
if (
|
|
11
|
+
/timed out|timeout|ECONN|ENOTFOUND|EPIPE|socket hang up|fetch failed|no answer/i.test(message)
|
|
12
|
+
)
|
|
13
|
+
return false;
|
|
14
|
+
return /Protocol error|Invalid parameters|No target with given id|No session with given id|Session with given id not found|Target closed|not found|Cannot find context/i.test(
|
|
15
|
+
message
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function answered(out) {
|
|
20
|
+
if (!out) return false;
|
|
21
|
+
if (out.ok) return true;
|
|
22
|
+
const err = out.error;
|
|
23
|
+
if (err && typeof err.code === 'number') return true;
|
|
24
|
+
return /^slicc: refused:/.test(String(err?.message ?? err ?? ''));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function until(check, ms, step = 25) {
|
|
28
|
+
const deadline = Date.now() + ms;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const value = await check();
|
|
31
|
+
if (value) return value;
|
|
32
|
+
if (Date.now() > deadline) return null;
|
|
33
|
+
await pause(step);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function note(notes, label, evidence) {
|
|
38
|
+
notes.push(`${label}: ${JSON.stringify(evidence)}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function walkFrames(tree, out = []) {
|
|
42
|
+
if (!tree?.frame) return out;
|
|
43
|
+
out.push(tree.frame);
|
|
44
|
+
for (const child of tree.childFrames ?? []) walkFrames(child, out);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function byIdMap(infos) {
|
|
49
|
+
return new Map((infos ?? []).map((info) => [info.targetId, info]));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ancestorsOf(info, byId) {
|
|
53
|
+
const chain = [];
|
|
54
|
+
let cur = info;
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
while (cur) {
|
|
57
|
+
if (seen.has(cur.targetId)) break;
|
|
58
|
+
seen.add(cur.targetId);
|
|
59
|
+
chain.push(cur);
|
|
60
|
+
if (!cur.parentId) break;
|
|
61
|
+
cur = byId.get(cur.parentId);
|
|
62
|
+
}
|
|
63
|
+
return chain;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function underProtected(info, byId, pageId) {
|
|
67
|
+
if (!info || info.targetId === pageId) return false;
|
|
68
|
+
return ancestorsOf(info, byId)
|
|
69
|
+
.slice(1)
|
|
70
|
+
.some((ancestor) => ancestor.targetId === pageId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collectIframes(infos, pageId) {
|
|
74
|
+
const byId = byIdMap(infos);
|
|
75
|
+
return (infos ?? []).filter(
|
|
76
|
+
(info) => info.type === 'iframe' && info.targetId && underProtected(info, byId, pageId)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function matchLeak(info, rawIds, rawUrls, pageId, rawParentIds) {
|
|
81
|
+
if (!info) return false;
|
|
82
|
+
if (rawIds.has(info.targetId)) return true;
|
|
83
|
+
if (typeof info.url === 'string' && info.url.length > 0 && rawUrls.has(info.url)) return true;
|
|
84
|
+
if (info.parentId && (info.parentId === pageId || rawParentIds.has(info.parentId))) return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function leakFromList(infos, rawIds, rawUrls, pageId, rawParentIds) {
|
|
89
|
+
return (infos ?? []).some((info) => matchLeak(info, rawIds, rawUrls, pageId, rawParentIds));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function leakFromEvents(events, rawIds, rawUrls, pageId, rawParentIds, from = 0) {
|
|
93
|
+
return (events ?? []).slice(from).some((event) => {
|
|
94
|
+
if (
|
|
95
|
+
['Target.targetCreated', 'Target.targetInfoChanged', 'Target.attachedToTarget'].includes(
|
|
96
|
+
event.method
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
return matchLeak(event.params?.targetInfo, rawIds, rawUrls, pageId, rawParentIds);
|
|
100
|
+
if (event.method === 'Target.targetDestroyed') return rawIds.has(event.params?.targetId);
|
|
101
|
+
return false;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function openProtected(reader, url, notes, internalSessions) {
|
|
106
|
+
const created = await outcome(
|
|
107
|
+
createObserverTarget(reader, { url }, notes).then(async (result) => {
|
|
108
|
+
const { sessionId } = await reader.send('Target.attachToTarget', {
|
|
109
|
+
targetId: result.targetId,
|
|
110
|
+
flatten: true,
|
|
111
|
+
});
|
|
112
|
+
await reader.send('Page.enable', {}, sessionId);
|
|
113
|
+
await reader.send('Runtime.enable', {}, sessionId);
|
|
114
|
+
return { targetId: result.targetId, sessionId };
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
if (!created.ok) return created;
|
|
118
|
+
internalSessions.add(created.result.sessionId);
|
|
119
|
+
const ready = await waitFor(async () => {
|
|
120
|
+
const read = await outcome(
|
|
121
|
+
reader.send(
|
|
122
|
+
'Runtime.evaluate',
|
|
123
|
+
{ expression: 'document.readyState', returnByValue: true },
|
|
124
|
+
created.result.sessionId
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
return read.ok && ['interactive', 'complete'].includes(read.result?.result?.value);
|
|
128
|
+
}, 4000);
|
|
129
|
+
note(notes, 'protected-parent.open', {
|
|
130
|
+
targetId: created.result.targetId,
|
|
131
|
+
url,
|
|
132
|
+
ready: Boolean(ready),
|
|
133
|
+
});
|
|
134
|
+
if (!ready)
|
|
135
|
+
return {
|
|
136
|
+
ok: true,
|
|
137
|
+
result: { incomplete: 'protected page did not become ready', ...created.result },
|
|
138
|
+
};
|
|
139
|
+
return created;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function readTree(reader, sessionId) {
|
|
143
|
+
const tree = await outcome(reader.send('Page.getFrameTree', {}, sessionId));
|
|
144
|
+
if (!tree.ok) return null;
|
|
145
|
+
return tree.result?.frameTree ?? null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function waitRawIframes(reader, pageId, budget, notes, want = 2) {
|
|
149
|
+
const found = await until(async () => {
|
|
150
|
+
const listed = await outcome(reader.send('Target.getTargets', {}));
|
|
151
|
+
if (!listed.ok) return null;
|
|
152
|
+
const iframes = collectIframes(listed.result?.targetInfos ?? [], pageId);
|
|
153
|
+
if (iframes.length >= want) return { infos: listed.result.targetInfos, iframes };
|
|
154
|
+
return null;
|
|
155
|
+
}, budget);
|
|
156
|
+
if (found) {
|
|
157
|
+
note(notes, 'protected-parent.raw-iframes', {
|
|
158
|
+
count: found.iframes.length,
|
|
159
|
+
iframes: found.iframes.map((info) => ({
|
|
160
|
+
targetId: info.targetId,
|
|
161
|
+
url: info.url,
|
|
162
|
+
parentId: info.parentId,
|
|
163
|
+
type: info.type,
|
|
164
|
+
})),
|
|
165
|
+
});
|
|
166
|
+
return found;
|
|
167
|
+
}
|
|
168
|
+
const listed = await outcome(reader.send('Target.getTargets', {}));
|
|
169
|
+
note(notes, 'protected-parent.raw-iframes-missing', {
|
|
170
|
+
want,
|
|
171
|
+
listed: (listed.result?.targetInfos ?? [])
|
|
172
|
+
.filter((info) => info.type === 'iframe' || info.targetId === pageId)
|
|
173
|
+
.map((info) => ({
|
|
174
|
+
targetId: info.targetId,
|
|
175
|
+
type: info.type,
|
|
176
|
+
url: info.url,
|
|
177
|
+
parentId: info.parentId,
|
|
178
|
+
})),
|
|
179
|
+
});
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function measureList(client, rawIds, rawUrls, pageId, rawParentIds) {
|
|
184
|
+
const listed = await outcome(client.send('Target.getTargets', {}));
|
|
185
|
+
if (!answered(listed)) return { out: listed, unanswered: true, leak: false };
|
|
186
|
+
return {
|
|
187
|
+
out: listed,
|
|
188
|
+
unanswered: false,
|
|
189
|
+
leak: listed.ok
|
|
190
|
+
? leakFromList(listed.result?.targetInfos ?? [], rawIds, rawUrls, pageId, rawParentIds)
|
|
191
|
+
: false,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function measureInfo(client, rawIds, rawUrls, pageId, rawParentIds) {
|
|
196
|
+
let out;
|
|
197
|
+
let leak = false;
|
|
198
|
+
let unanswered = false;
|
|
199
|
+
let unansweredId;
|
|
200
|
+
for (const targetId of rawIds) {
|
|
201
|
+
const info = await outcome(client.send('Target.getTargetInfo', { targetId }));
|
|
202
|
+
out ??= info;
|
|
203
|
+
if (!answered(info)) {
|
|
204
|
+
unanswered = true;
|
|
205
|
+
unansweredId ??= targetId;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (!info.ok) continue;
|
|
209
|
+
const targetInfo = info.result?.targetInfo;
|
|
210
|
+
if (matchLeak(targetInfo, rawIds, rawUrls, pageId, rawParentIds)) leak = true;
|
|
211
|
+
}
|
|
212
|
+
return { out, leak, unanswered, unansweredId };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function measureAttach(client, rawIds) {
|
|
216
|
+
let out;
|
|
217
|
+
let attachSession = false;
|
|
218
|
+
let attachRefused = false;
|
|
219
|
+
let unanswered = false;
|
|
220
|
+
let unansweredId;
|
|
221
|
+
for (const targetId of rawIds) {
|
|
222
|
+
const attached = await outcome(
|
|
223
|
+
client.send('Target.attachToTarget', { targetId, flatten: true })
|
|
224
|
+
);
|
|
225
|
+
out ??= attached;
|
|
226
|
+
if (!answered(attached)) {
|
|
227
|
+
unanswered = true;
|
|
228
|
+
unansweredId ??= targetId;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (attached.ok && attached.result?.sessionId) {
|
|
232
|
+
attachSession = true;
|
|
233
|
+
attachRefused = false;
|
|
234
|
+
await outcome(
|
|
235
|
+
client.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
236
|
+
);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (refused(attached)) attachRefused = true;
|
|
240
|
+
}
|
|
241
|
+
return { out, attachSession, attachRefused, unanswered, unansweredId };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function takeFirst(slot, sample, apply) {
|
|
245
|
+
if (!slot.out) slot.out = sample.out;
|
|
246
|
+
slot.unanswered ||= sample.unanswered;
|
|
247
|
+
slot.unansweredId ??= sample.unansweredId;
|
|
248
|
+
apply(sample);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function settleArm(result, key, unanswered, label, missing, fallback) {
|
|
252
|
+
if (unanswered) missing.push(label);
|
|
253
|
+
else if (result[key] === undefined) Object.assign(result, fallback);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function measureClient({
|
|
257
|
+
client,
|
|
258
|
+
rawIds,
|
|
259
|
+
rawUrls,
|
|
260
|
+
pageId,
|
|
261
|
+
rawParentIds,
|
|
262
|
+
discovery,
|
|
263
|
+
eventStart,
|
|
264
|
+
budget,
|
|
265
|
+
notes,
|
|
266
|
+
}) {
|
|
267
|
+
const result = {};
|
|
268
|
+
const missing = [];
|
|
269
|
+
const windowMs = Math.min(budget, 1500);
|
|
270
|
+
const deadline = Date.now() + windowMs;
|
|
271
|
+
const list = { out: undefined, unanswered: false };
|
|
272
|
+
const info = { out: undefined, unanswered: false };
|
|
273
|
+
const attach = { out: undefined, unanswered: false };
|
|
274
|
+
let firstEvents = false;
|
|
275
|
+
while (Date.now() <= deadline) {
|
|
276
|
+
const listed = await measureList(client, rawIds, rawUrls, pageId, rawParentIds);
|
|
277
|
+
takeFirst(list, listed, (sample) => {
|
|
278
|
+
if (sample.leak) result.iframeListed = true;
|
|
279
|
+
else if (!sample.unanswered) result.iframeListed ??= false;
|
|
280
|
+
});
|
|
281
|
+
const informed = await measureInfo(client, rawIds, rawUrls, pageId, rawParentIds);
|
|
282
|
+
takeFirst(info, informed, (sample) => {
|
|
283
|
+
if (sample.leak) result.infoRead = true;
|
|
284
|
+
else if (!sample.unanswered) result.infoRead ??= false;
|
|
285
|
+
});
|
|
286
|
+
if (
|
|
287
|
+
discovery &&
|
|
288
|
+
leakFromEvents(client.events ?? [], rawIds, rawUrls, pageId, rawParentIds, eventStart)
|
|
289
|
+
) {
|
|
290
|
+
result.eventsSeen = true;
|
|
291
|
+
firstEvents = true;
|
|
292
|
+
}
|
|
293
|
+
if (result.attachSession !== true) {
|
|
294
|
+
const attached = await measureAttach(client, rawIds);
|
|
295
|
+
takeFirst(attach, attached, (sample) => {
|
|
296
|
+
if (sample.attachSession) {
|
|
297
|
+
result.attachSession = true;
|
|
298
|
+
result.attachRefused = false;
|
|
299
|
+
} else if (!sample.unanswered) {
|
|
300
|
+
result.attachSession = sample.attachSession;
|
|
301
|
+
result.attachRefused = sample.attachRefused;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (
|
|
306
|
+
result.iframeListed === true &&
|
|
307
|
+
result.infoRead === true &&
|
|
308
|
+
(!discovery || result.eventsSeen === true) &&
|
|
309
|
+
result.attachSession === true
|
|
310
|
+
)
|
|
311
|
+
break;
|
|
312
|
+
await pause(50);
|
|
313
|
+
}
|
|
314
|
+
if (discovery && result.eventsSeen === undefined) result.eventsSeen = false;
|
|
315
|
+
settleArm(result, 'iframeListed', list.unanswered, 'Target.getTargets unanswered', missing, {
|
|
316
|
+
iframeListed: false,
|
|
317
|
+
});
|
|
318
|
+
settleArm(
|
|
319
|
+
result,
|
|
320
|
+
'infoRead',
|
|
321
|
+
info.unanswered,
|
|
322
|
+
info.unansweredId
|
|
323
|
+
? `Target.getTargetInfo unanswered (${info.unansweredId})`
|
|
324
|
+
: 'Target.getTargetInfo unanswered',
|
|
325
|
+
missing,
|
|
326
|
+
{ infoRead: false }
|
|
327
|
+
);
|
|
328
|
+
settleArm(
|
|
329
|
+
result,
|
|
330
|
+
'attachSession',
|
|
331
|
+
attach.unanswered,
|
|
332
|
+
attach.unansweredId
|
|
333
|
+
? `Target.attachToTarget unanswered (${attach.unansweredId})`
|
|
334
|
+
: 'Target.attachToTarget unanswered',
|
|
335
|
+
missing,
|
|
336
|
+
{ attachSession: false, attachRefused: false }
|
|
337
|
+
);
|
|
338
|
+
note(notes, 'protected-parent.client', {
|
|
339
|
+
iframeListed: result.iframeListed ?? 'unmeasured',
|
|
340
|
+
infoRead: result.infoRead ?? 'unmeasured',
|
|
341
|
+
eventsSeen: discovery ? (result.eventsSeen ?? false) : 'not-declared',
|
|
342
|
+
attachSession: result.attachSession ?? 'unmeasured',
|
|
343
|
+
attachRefused: result.attachRefused ?? 'unmeasured',
|
|
344
|
+
windowMs,
|
|
345
|
+
firstListOk: list.out?.ok,
|
|
346
|
+
firstInfoOk: info.out?.ok,
|
|
347
|
+
firstAttachOk: attach.out?.ok,
|
|
348
|
+
firstEvents,
|
|
349
|
+
unanswered: missing,
|
|
350
|
+
});
|
|
351
|
+
return missing.length ? { ...result, incomplete: missing.join('; ') } : result;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function nestedCross({
|
|
355
|
+
reader,
|
|
356
|
+
client,
|
|
357
|
+
protectedOrigin,
|
|
358
|
+
cross,
|
|
359
|
+
ordinary,
|
|
360
|
+
tag,
|
|
361
|
+
notes,
|
|
362
|
+
budget,
|
|
363
|
+
discovery,
|
|
364
|
+
internalSessions,
|
|
365
|
+
eventStart,
|
|
366
|
+
}) {
|
|
367
|
+
if (!cross) return { incomplete: 'no cross-site ordinary origin ({cross}) mapped' };
|
|
368
|
+
const mid = `${cross}/oopif-mid?inner=${encodeURIComponent(`${ordinary}/doc?n=inner-${tag}`)}&n=mid-${tag}`;
|
|
369
|
+
const page = `${protectedOrigin}/oopif-nest?mid=${encodeURIComponent(mid)}&n=host-${tag}`;
|
|
370
|
+
note(notes, 'protected-parent.nest-urls', {
|
|
371
|
+
host: protectedOrigin,
|
|
372
|
+
mid: cross,
|
|
373
|
+
inner: ordinary,
|
|
374
|
+
crossMapping:
|
|
375
|
+
'{cross} must be a different registrable domain from {ordinary} (e.g. cross.test); same-site {cross} leaves nested iframe targets missing and this row incomplete',
|
|
376
|
+
});
|
|
377
|
+
const opened = await openProtected(reader, page, notes, internalSessions);
|
|
378
|
+
if (!opened.ok)
|
|
379
|
+
return {
|
|
380
|
+
incomplete: `observer setup failed: ${opened.error?.message ?? opened.error}`,
|
|
381
|
+
};
|
|
382
|
+
if (opened.result.incomplete) return { live: false, incomplete: opened.result.incomplete };
|
|
383
|
+
const { targetId: pageId, sessionId } = opened.result;
|
|
384
|
+
await until(
|
|
385
|
+
async () => {
|
|
386
|
+
const tree = await readTree(reader, sessionId);
|
|
387
|
+
return walkFrames(tree).length >= 3 ? tree : null;
|
|
388
|
+
},
|
|
389
|
+
Math.min(budget, 2000)
|
|
390
|
+
);
|
|
391
|
+
const tree = await readTree(reader, sessionId);
|
|
392
|
+
note(notes, 'protected-parent.frame-tree', {
|
|
393
|
+
frames: walkFrames(tree).map((frame) => ({ id: frame.id, url: frame.url })),
|
|
394
|
+
});
|
|
395
|
+
const raw = await waitRawIframes(reader, pageId, budget, notes, 2);
|
|
396
|
+
if (!raw) {
|
|
397
|
+
const late = await readTree(reader, sessionId);
|
|
398
|
+
note(notes, 'protected-parent.frame-tree-late', {
|
|
399
|
+
frames: walkFrames(late).map((frame) => ({ id: frame.id, url: frame.url })),
|
|
400
|
+
});
|
|
401
|
+
return {
|
|
402
|
+
live: true,
|
|
403
|
+
incomplete: 'nested ordinary iframe targets did not appear in the raw leg within the budget',
|
|
404
|
+
pageId,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const rawIds = new Set(raw.iframes.map((info) => info.targetId));
|
|
408
|
+
const rawUrls = new Set(
|
|
409
|
+
raw.iframes.map((info) => info.url).filter((url) => typeof url === 'string' && url.length > 0)
|
|
410
|
+
);
|
|
411
|
+
const rawParentIds = new Set(
|
|
412
|
+
raw.iframes.map((info) => info.parentId).filter((id) => typeof id === 'string' && id.length > 0)
|
|
413
|
+
);
|
|
414
|
+
rawParentIds.add(pageId);
|
|
415
|
+
const measured = await measureClient({
|
|
416
|
+
client,
|
|
417
|
+
rawIds,
|
|
418
|
+
rawUrls,
|
|
419
|
+
pageId,
|
|
420
|
+
rawParentIds,
|
|
421
|
+
discovery,
|
|
422
|
+
eventStart,
|
|
423
|
+
budget,
|
|
424
|
+
notes,
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
live: true,
|
|
428
|
+
pageId,
|
|
429
|
+
iframeIds: [...rawIds],
|
|
430
|
+
iframeUrls: [...rawUrls],
|
|
431
|
+
...measured,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function sandboxedSrcdoc({
|
|
436
|
+
reader,
|
|
437
|
+
client,
|
|
438
|
+
protectedOrigin,
|
|
439
|
+
tag,
|
|
440
|
+
notes,
|
|
441
|
+
budget,
|
|
442
|
+
discovery,
|
|
443
|
+
internalSessions,
|
|
444
|
+
eventStart,
|
|
445
|
+
}) {
|
|
446
|
+
const page = `${protectedOrigin}/oopif-srcdoc?n=srcdoc-${tag}`;
|
|
447
|
+
const opened = await openProtected(reader, page, notes, internalSessions);
|
|
448
|
+
if (!opened.ok)
|
|
449
|
+
return {
|
|
450
|
+
incomplete: `observer setup failed: ${opened.error?.message ?? opened.error}`,
|
|
451
|
+
};
|
|
452
|
+
if (opened.result.incomplete) return { live: false, incomplete: opened.result.incomplete };
|
|
453
|
+
const { targetId: pageId, sessionId } = opened.result;
|
|
454
|
+
const tree = await readTree(reader, sessionId);
|
|
455
|
+
const frames = walkFrames(tree);
|
|
456
|
+
const srcdocFrame =
|
|
457
|
+
frames.find((frame) => frame.url === 'about:srcdoc') ??
|
|
458
|
+
frames.find((frame) => typeof frame.url === 'string' && frame.url.includes('srcdoc')) ??
|
|
459
|
+
frames.find((frame) => frame.id && frame.id !== (tree?.frame?.id ?? pageId));
|
|
460
|
+
note(notes, 'protected-parent.srcdoc-tree', {
|
|
461
|
+
frames: frames.map((frame) => ({ id: frame.id, url: frame.url })),
|
|
462
|
+
srcdocFrame: srcdocFrame ? { id: srcdocFrame.id, url: srcdocFrame.url } : null,
|
|
463
|
+
});
|
|
464
|
+
const raw = await waitRawIframes(reader, pageId, budget, notes, 1);
|
|
465
|
+
if (!raw) {
|
|
466
|
+
const reason = 'sandboxed srcdoc frame has no own target in this Chrome';
|
|
467
|
+
note(notes, 'protected-parent.srcdoc-no-oopif', {
|
|
468
|
+
reason,
|
|
469
|
+
pageId,
|
|
470
|
+
frameTreeHasChild: Boolean(srcdocFrame),
|
|
471
|
+
childUrl: srcdocFrame?.url ?? null,
|
|
472
|
+
});
|
|
473
|
+
notes.push('srcdoc: no own target in this Chrome (incomplete)');
|
|
474
|
+
return {
|
|
475
|
+
live: true,
|
|
476
|
+
incomplete: reason,
|
|
477
|
+
pageId,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const rawIds = new Set(raw.iframes.map((info) => info.targetId));
|
|
481
|
+
const rawUrls = new Set(
|
|
482
|
+
raw.iframes.map((info) => info.url).filter((url) => typeof url === 'string' && url.length > 0)
|
|
483
|
+
);
|
|
484
|
+
const rawParentIds = new Set(
|
|
485
|
+
raw.iframes.map((info) => info.parentId).filter((id) => typeof id === 'string' && id.length > 0)
|
|
486
|
+
);
|
|
487
|
+
rawParentIds.add(pageId);
|
|
488
|
+
note(notes, 'protected-parent.srcdoc-oopif', {
|
|
489
|
+
count: raw.iframes.length,
|
|
490
|
+
iframes: raw.iframes.map((info) => ({
|
|
491
|
+
targetId: info.targetId,
|
|
492
|
+
url: info.url,
|
|
493
|
+
parentId: info.parentId,
|
|
494
|
+
})),
|
|
495
|
+
});
|
|
496
|
+
notes.push('srcdoc: own iframe target, hidden');
|
|
497
|
+
const measured = await measureClient({
|
|
498
|
+
client,
|
|
499
|
+
rawIds,
|
|
500
|
+
rawUrls,
|
|
501
|
+
pageId,
|
|
502
|
+
rawParentIds,
|
|
503
|
+
discovery,
|
|
504
|
+
eventStart,
|
|
505
|
+
budget,
|
|
506
|
+
notes,
|
|
507
|
+
});
|
|
508
|
+
return {
|
|
509
|
+
live: true,
|
|
510
|
+
pageId,
|
|
511
|
+
iframeIds: [...rawIds],
|
|
512
|
+
iframeUrls: [...rawUrls],
|
|
513
|
+
...measured,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export async function protectedParentIframes({
|
|
518
|
+
client,
|
|
519
|
+
reader,
|
|
520
|
+
protectedOrigin,
|
|
521
|
+
cross,
|
|
522
|
+
ordinary,
|
|
523
|
+
mode,
|
|
524
|
+
tag,
|
|
525
|
+
internalSessions = new Set(),
|
|
526
|
+
notes = [],
|
|
527
|
+
budget = 5000,
|
|
528
|
+
discovery = false,
|
|
529
|
+
eventStart = 0,
|
|
530
|
+
}) {
|
|
531
|
+
if (!reader) return { incomplete: 'missing unguarded reader' };
|
|
532
|
+
if (discovery) {
|
|
533
|
+
await outcome(client.send('Target.setDiscoverTargets', { discover: true }));
|
|
534
|
+
await outcome(reader.send('Target.setDiscoverTargets', { discover: true }));
|
|
535
|
+
notes.push('protected-parent.discovery: setDiscoverTargets discover=true');
|
|
536
|
+
}
|
|
537
|
+
const built =
|
|
538
|
+
mode === 'nested-cross'
|
|
539
|
+
? await nestedCross({
|
|
540
|
+
reader,
|
|
541
|
+
client,
|
|
542
|
+
protectedOrigin,
|
|
543
|
+
cross,
|
|
544
|
+
ordinary,
|
|
545
|
+
tag,
|
|
546
|
+
notes,
|
|
547
|
+
budget,
|
|
548
|
+
discovery,
|
|
549
|
+
internalSessions,
|
|
550
|
+
eventStart,
|
|
551
|
+
})
|
|
552
|
+
: mode === 'sandboxed-srcdoc'
|
|
553
|
+
? await sandboxedSrcdoc({
|
|
554
|
+
reader,
|
|
555
|
+
client,
|
|
556
|
+
protectedOrigin,
|
|
557
|
+
tag,
|
|
558
|
+
notes,
|
|
559
|
+
budget,
|
|
560
|
+
discovery,
|
|
561
|
+
internalSessions,
|
|
562
|
+
eventStart,
|
|
563
|
+
})
|
|
564
|
+
: { incomplete: `unknown protectedParentIframes mode ${mode}` };
|
|
565
|
+
if (built.pageId) await outcome(reader.send('Target.closeTarget', { targetId: built.pageId }));
|
|
566
|
+
return built;
|
|
567
|
+
}
|
|
@@ -564,6 +564,22 @@
|
|
|
564
564
|
}
|
|
565
565
|
}
|
|
566
566
|
},
|
|
567
|
+
{
|
|
568
|
+
"type": "object",
|
|
569
|
+
"additionalProperties": false,
|
|
570
|
+
"required": ["protectedParentIframes", "as"],
|
|
571
|
+
"properties": {
|
|
572
|
+
"protectedParentIframes": {
|
|
573
|
+
"enum": ["nested-cross", "sandboxed-srcdoc"]
|
|
574
|
+
},
|
|
575
|
+
"as": {
|
|
576
|
+
"$ref": "#/$defs/name"
|
|
577
|
+
},
|
|
578
|
+
"requires": {
|
|
579
|
+
"$ref": "#/$defs/requires"
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
},
|
|
567
583
|
{
|
|
568
584
|
"type": "object",
|
|
569
585
|
"additionalProperties": false,
|