@ai-ecoverse/slicc-browsers 1.22.1 → 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 +3 -3
- package/close-protected-shape.mjs +563 -0
- package/fixture-server.mjs +64 -0
- package/guard-runner.mjs +80 -8
- package/guard-vectors.json +606 -1
- package/package.json +6 -4
- package/protected-parent-iframes.mjs +567 -0
- package/schema/guard-vectors.schema.json +32 -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
|
+
}
|
|
@@ -548,6 +548,38 @@
|
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
550
|
},
|
|
551
|
+
{
|
|
552
|
+
"type": "object",
|
|
553
|
+
"additionalProperties": false,
|
|
554
|
+
"required": ["closeProtectedShape", "as"],
|
|
555
|
+
"properties": {
|
|
556
|
+
"closeProtectedShape": {
|
|
557
|
+
"enum": ["view-source", "blob", "userinfo", "navigating", "blank-inherited"]
|
|
558
|
+
},
|
|
559
|
+
"as": {
|
|
560
|
+
"$ref": "#/$defs/name"
|
|
561
|
+
},
|
|
562
|
+
"requires": {
|
|
563
|
+
"$ref": "#/$defs/requires"
|
|
564
|
+
}
|
|
565
|
+
}
|
|
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
|
+
},
|
|
551
583
|
{
|
|
552
584
|
"type": "object",
|
|
553
585
|
"additionalProperties": false,
|