domain 废弃带来的安全限制问题

579 阅读8分钟

1、背景和喜报

插播一条喜报

096fcc53e8cc3419bc811ca4f74b455.jpg

如果你觉得八天假期很短,那么你马上就会知道七天有多长了;那些鼓励调休的专家,就应该给他先饿上三天,再给他喂 9 碗米饭,不仅量大管饱,还能节省时间 image.png

闲话少扯,进入正题;国庆回来,业务上反馈登录弹窗 iframe 跨域报错。

最终定位到罪魁祸首:document.domain 即将变为只读属性。

新版本谷歌浏览器,逐步废弃,原来使用此方案解决跨域的场景都将失效(灰度发布,也就意味着部分用户的新版浏览器已经受到这个安全策略的影响)。

后续其他浏览器也将跟随,包括 Edge 也已经发布了相关文档: Edge 禁用修改 document.domain 文档

2、domain 的原理

  • 如何利用 domain 实现跨域?
  • 使用时需要注意些什么?
  • 为什么要废弃?

2.1 同源策略

同源策略是一种浏览器安全机制,它限制了不同源(协议、域名、端口号)的页面之间的交互。也就是说,如果两个页面的域名不同,则它们之间无法进行通信。

然而,在某些情况下,我们希望不同子域名下的页面能够进行通信,早期经常使用 document.domain 属性来解决跨子域问题

2.2 具体原理

假设页面的域名为 a.example.com,而我们嵌入的 iframe 则部署在 b.example.com 上,此时设置 document.domain = 'example.com' 后,这样一来,由于它们具有相同的基础域,就可以绕过同源策略限制,进行跨子域通信。

  • 子页面操作父页面: 通过 window.parent 来获取父页面的元素、数据,并操作 DOM
  • 父页面操作 iframe:通过 document.querySelector('#iframeId').contentWindow 来获取 iframe 的窗口对象,操作 iframe 的 DOM

2.3 注意事项

  • 设置 domain 必须拥有相同的二级域名,a.example.comb.example.com 是允许的,因为它们的二级域名都是 example.com;但 a.example.coma.test.com 是不允许的。
  • 连带着 node:domain 模块也在等待弃用,以后关于 domain 的设置功能算是基本不能用了。

2.4 安全问题

页面可以尝试与不同域名的文档进行交互,包括读取和修改文档内容、调用文档中的函数等。这可能会被恶意网站滥用,从而进行跨站点脚本攻击(XSS)等攻击。

拥有共同相同二级域名,这都是属于同一业务线啊?

no no no,最典型的如 github 部署博客,每个人都拥有https://xxx.github.io 的域名,公用一级域名。如果你在你自己的站点设置 document.domain = 'github,io',那么任意其他 github 用户,都将可以毫无忌惮的攻击你的站点。

这时就存在跨域攻击的风险,这也是为什么禁用 domain setter 的原因。

2.5 如何确定我的浏览器是否受到影响

由于 domain setter 的废弃采用的是灰度发布方案,这也就意味着,即使多个人的 Chrome 版本一致,也可能只有少部分个人受到影响。

那么如何确定我的浏览器是否受到影响呢?

打开控制台,设置 document.domain 为你当前网页的二级域名,如果收到警告,则代表你的浏览器已经受到安全策略的影响

image.png

3、解决方案

业务中通过更改 document.domain 进行跨域的场景,芭比Q了,很多网页可能已经出现了部分用户无法正常访问的问题了。

3.1 临时解决方案

  • 推荐用户更换浏览器(如使用 Edge,目前还没有限制)或者使用低版本的浏览器
  • 让用户修改浏览器设置
    • 浏览器地址栏输入 chrome://flags/#origin-agent-cluster-default
    • 然后修改 Origin-keyed Agent Clusters by default 选项值为 Disabled

3.2 过渡方案

根据 Edge 官方文档中所说,如果有充分的理由继续设置 document.domain,则可以在目标文档上发送 Origin-Agent-Cluster: ?0 响应标头。即由服务端设置 response.setHeader('Origin-Agent-Cluster', '?0');

但由于 Origin-Agent-Cluster 没有纳入 html 标准,将来可能失效;不过依据 w3c 发布废弃标准的流程,几年内甚至更久,此方案应该都是有效的,说不定还会纳入标准呢 image.png

3.3 彻底解决之 postMessage

普通页面中使用 window.postMessage

Web Worker 中使用 Channel Messaging API,MDN 在 github 上发布了使用演示

3.3.1 postMessage 使用

语法: otherWindow.postMessage(message, targetOrigin, [transfer]);

  • 调用 postMessage 的 otherWindow 是一个窗口对象,父页面给 iframe 发射消息时 为 window;iframe 给父页面发射消息时为 iframe 的 contentWindow
  • message 是携带的参数,所有可以被 结构化克隆算法 (en-US)序列化的对象都可以携带;同时也意味着 DOM 元素对象是无法携带的(绑定的事件函数无法被克隆)
  • targetOrigin 接收消息目标域名,为保证安全,属于必须参数(可以使用通配符 * 来让所有接收方接收消息,但不建议);实际使用中,需要确保目标窗口与发送方和接收方之间建立了正确的跨域通信机制,如设置正确的 CORS 头信息或使用相同二级域名
  • transfer 可选参数,传递特殊类型参数 ArrayBuffer、MessagePort 和 ImageBitma,但还是不支持 DOM。

父页面发射&接收消息

// 发射消息给 iframe
iframe.contentWindow.postMessage({
    id: 1,
    desc: '我是父页面,有内鬼,交易取消'
}, 'https://static.cdn.com')

// 接收 iframe 的消息
window.addEventListener('message', function(event) {
    // 通过 ID 标识过滤,也可以通过 origin 过滤
    if (event.data && event.data.id) {
        // 处理逻辑
    }
})

子页面发射&接收消息

// 发射消息给父页面
window.parent.postMessage({
    id: 1,
    desc: '我是 iframe,我已经就位了,动手~'
}, 'https://a.test.com')

// 接收父页面的消息
window.addEventListener('message', function(event) {
    // 通过 ID 标识过滤,也可以通过 origin 过滤
    if (event.data && event.data.id) {
        // 处理逻辑
    }
})

总结:

  • 接收消息的方式基本一致,注意过滤掉杂七杂八的消息,一般是通过 origin 或自定义 ID
  • 发射消息时,iframe 需要通过 window.parent 调用;父页面则需要拿到 iframe.contentWindow 调用。
  • 为了保证安全性,明确 targetOrigin 的域名地址,而不是采用通配符

3.3.2 如何利用 postMessage 解决登录 iframe 跨域问题

  • 目前 domain 实现方式
document.domain = "51cto.com"
// 判断是否在绑定页面点击的关闭 是则直接刷新
if (document.getElementsByClassName("login-iframe")[0].contentWindow.document.getElementsByClassName("bind-box").length > 0) {
    window.location.reload();
}
this.props.parent.closeIframe();
  • postMessage 如何改造

调用登录弹窗 iframe 的页面中

// 给 iframe 发射关闭消息,需要指定域名
iframe.postMessage('close', 'https://b.xxx.com');

window.addEventListener('message', function(event) {
    console.log(`收到 iframe 发射的消息);
    // 过滤
    if (event.origin !== 'https://a.xxx.com') return;
    }
})

3.3.3 关于携带参数为 DOM 元素问题

既然抛开 postMessage 而选择 domain 实现 iframe 与主页面通讯,那么业务中千奇百怪的使用都可能出现,一种常见的操作就是在 iframe 中,通过 window.parent 来操作父页面的各种 DOM。一旦 domain 失效,所有这类代码都将不能正常工作,且改造为 postMessage 方式工作量大增。

  • 一种思路是改造父页面和 iframe

将原本在 iframe 处理父页面 DOM 相关的逻辑抽离到父页面中,利用 postMessage 来触发。改造成本较大,但实现上没有技术难点。

  • 另一种思路则是尝试解决携带 DOM 作为的问题

前面已经说过了,postMessage 默认是不支持携带 DOM 作为参数的,原因是 DOM 内部存在许多函数属性;剔除函数属性,则可以携带。

那么子 iframe 中如果使用 DOM 实例的函数怎么办呢?最好的做法是在父页面处理,把结果作为参数告知 iframe;如果处理函数内部实现没有关联 DOM 的纯粹的解耦逻辑,可以将函数 toString() 转化,然后使用 eval 解析,亲测可行。

那么问题来了,富文本编辑器 UEditor 的 toolbar 使用 iframe 实现,子 iframe 中大量使用了 UE 实例的函数属性,而这些函数内部大多调用了 UE 的封装工具类、常量;简直是...无解

3.3.4 关于 UEditor toolbar 使用 domain 的改造

  • 升级富文本编辑器(业务上有需求,是否优先?)

周边场景影响最大则是 vue2~vue3 大版本 sdk 升级,同时编辑器更换,需要覆盖测试的影响范围大。

  • 将 toolbar 对应的 iframe 迁移到与项目下(推荐)

将 iframe 部署到项目,那么就处于同一域名下,配合 UEditor all.js 配置 iframeUrlMap 的指向项目工程下的绝对地址,部署后就不会产生跨域问题。

开发成本几乎没有,可以做到快速切换;缺点是造成大量的资源冗余,每个项目下都需要部署一份 toolbar 的 iframe 文件。

  • 富文本编辑器的所有 toolbar 对应的 iframe 改造为组件封装的形式

开发成本非常大,将近 20 个 iframe 页面,且内部操逻辑非常复杂,劝退。

4、结束语

最近被 PV 的有点严重,提醒自己不必陷入自证陷阱。

6e0eed9e7689ffaff6080d2e48eb2f1.jpg

说是这么说,可是没法一下子想出“狗咬你你不急啊”这种杀伤力爆强的句子唉~ image.png