跨域 iframe 里读不到 DOM:用 CDP 执行上下文直接求值

举报
yd_228692151 发表于 2026/10/31 06:35:57 2026/10/31
【摘要】 跨域 iframe 里读不到 DOM:用 CDP 执行上下文直接求值页面明明渲染出来了,document.body.innerText 却只有几十个字符;想找的按钮、输入框一个都搜不到;iframe.contentDocument 返回 null;在 /json/list 里翻一遍,也没有那个 iframe 对应的独立 target。这是浏览器自动化里很常见的一类僵局:目标界面装在一个跨域...

跨域 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,而不是在挡调试协议。分清这两层,就不必在“截图量坐标”这条脆弱路径上反复修补。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。