跨域 iframe 里读不到 DOM:用 CDP 执行上下文直接求值
跨域 iframe 里读不到 DOM:用 CDP 执行上下文直接求值
页面明明渲染出来了,document.body.innerText 却只有几十个字符;想找的按钮、输入框一个都搜不到;iframe.contentDocument 返回 null;在 /json/list 里翻一遍,也没有那个 iframe 对应的独立 target。这是浏览器自动化里很常见的一类僵局:目标界面装在一个跨域 iframe 里。
常规的绕法有三条,都不好用。截图量坐标,布局一变全废;el.click() 找不到元素,因为根本拿不到 DOM;换到父页面派发 Input.dispatchMouseEvent,元素是点到了,但坐标依赖截图目测值,滚动、弹窗、折叠都会让坐标失真。真正稳定的做法是利用 CDP 的执行上下文机制:跨域 iframe 虽然对页面 JS 不可见,但它的 frame 依然是一个独立的 execution context,Chrome DevTools Protocol 能按 contextId 直接在里面求值。
先确认 iframe 的真实身份
打开页面 target 的调试端口,先看 DOM 里 iframe 的 src:
https://<hash>.workspace.example.com/
它与父页面不同源,所以父页面的 Runtime.evaluate 拿不到它;它也没有作为独立 target 出现在 /json/list 里(type=iframe 的条目只出现在跨进程 OOPIF 的情况下),所以“直连 iframe target 再求值”这条路也走不通。
执行上下文是怎么暴露的
对页面 target(不是 browser target)的 WebSocket 调用 Runtime.enable 之后,协议会为当前所有 frame 各发一条 Runtime.executionContextCreated 事件,事件体里带 ExecutionContextDescription:
{
"id": 7,
"origin": "https://<hash>.workspace.example.com",
"name": "",
"auxData": { "isDefault": true, "frameId": "A1B2...", "type": "default" }
}
id 就是要传给 Runtime.evaluate 的 contextId。origin 用来区分主页面与各层 iframe,auxData.isDefault 用来确认是这个 frame 的默认(主)上下文,而不是某个扩展或 worker 注入的隔离世界。
最小实现
关键点只有三个:连页面 target 的 WS、Runtime.enable 后等事件到达、按 origin 过滤后带 contextId 求值。
const ws = new WebSocket(page.webSocketDebuggerUrl);
let seq = 0;
const pending = new Map();
const contexts = [];
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data);
if (msg.id && pending.has(msg.id)) {
pending.get(msg.id)(msg);
pending.delete(msg.id);
} else if (msg.method === 'Runtime.executionContextCreated') {
contexts.push(msg.params.context);
}
};
const send = (method, params = {}) => new Promise((res) => {
const id = ++seq;
pending.set(id, res);
ws.send(JSON.stringify({ id, method, params }));
});
await send('Runtime.enable');
await new Promise((r) => setTimeout(r, 500)); // 等 executionContextCreated 事件
const ctx = contexts.find((c) => c.origin.includes('workspace.example.com'));
const out = await send('Runtime.evaluate', {
expression: 'JSON.stringify({ url: location.href, t: document.title })',
contextId: ctx.id,
returnByValue: true,
awaitPromise: true
});
console.log(out.result.result.value);
跑通之后,iframe 里的 DOM 就和主页面一样随意查询了。
四个容易踩的点
一、Runtime.enable 之后不要立刻发 evaluate。 事件是异步推送的,contexts 数组在 enable 的同一个 tick 里还是空的。要么等几百毫秒,要么在 executionContextCreated 里做匹配后再触发求值。
二、returnByValue 和 awaitPromise 不是默认值。 不设 returnByValue 拿到的是 RemoteObject,还得再 Runtime.getProperties 一次;对返回 Promise 的表达式不设 awaitPromise,拿到的是 Promise 对象的描述而不是结果。回传大对象前用 JSON.stringify 收敛,比逐层解析 RemoteObject 省事。
三、上下文会随 iframe 导航销毁重建。 单页应用里切换路由、重建 iframe,旧 contextId 直接失效,evaluate 报 Cannot find context with specified id。正确做法是同时监听 Runtime.executionContextDestroyed 和 Runtime.executionContextsCleared,每次操作前重新按 origin 取最新上下文,不要把 contextId 缓存成常量。
四、能求值不等于能点击。 拿到 DOM 之后优先用 el.click();某些框架只认原生事件,Input.dispatchMouseEvent 比 el.click() 更容易被拦。反过来,Input.insertText 依赖焦点,先把目标元素 focus() 再发文本,否则字符会落到上一个持有焦点的元素上——最常见的症状是标题框里出现一整段正文。
判断“真的成功”的判据
不要以“脚本没报错”当成功。可靠判据有三个,按顺序查:location.href 是否等于 iframe 自己的 origin(证明求值确实发生在子 frame 里,而不是父页面);目标的 getBoundingClientRect().width > 0(排除响应式布局里的隐藏副本);操作后回读业务状态(会话列表是否多出条目、消息气泡是否出现),而不是回读按钮文案。
跨域 iframe 本质上是浏览器的同源策略在挡页面 JS,而不是在挡调试协议。分清这两层,就不必在“截图量坐标”这条脆弱路径上反复修补。
- 点赞
- 收藏
- 关注作者
评论(0)