问题描述
公司有个内部项目是用 Electron 来开发的,有个功能需要像浏览器一样加载第三方站点。
本来一切安好,但是某天打开某个站点的链接,导致 整个客户端直接变成了该站点的页面。
这一看就是该站点做了特殊的处理,经排查网页源码后,果然发现了有这么一句代码。
if (window.top !== window.self) { window.top.location = window.location; }
翻译一下就是:如果当前窗口不是顶级窗口的话,将当前窗口设置为顶级窗口。
奇怪的是两者不是 跨域 了吗,为什么 iframe 还可以影响顶级窗口。
先说一下我当时的一些解决办法:
- 用 webview 替换 iframe
- 给 iframe 添加 sandbox 属性
后续内容就是一点复盘工作。
场景复现(Web端)
一开始怀疑是客户端的问题,所以我用在纯 Web 上进行了一次对比验证。
这里我们新建两个文件:1.html 和 2.html,我们称之为 页面A 和 页面B。
然后起了两个本地服务器来模拟同源与跨域的情况。
- 页面A:http://127.0.0.1:5500/1.html
- 页面B:http://127.0.0.1:5500/2.html 和 http://localhost:3000/2.html
符合同源策略
<body> <h1>这是页面A</h1> <!-- 这是同源的情况 --> <iframe id="iframe" src="http://127.0.0.1:5500/2.html" /> <script> iframe.onload = () => { console.log('iframe loaded..') console.log('子窗口路径', iframe.contentWindow.location.href) } </script> </body>
<body> <h2>这是页面B</h2> <script> console.log('page2...') console.log(window === window.top) console.log('顶部窗口路径', window.top.location.href) </script> </body>
我们打开控制台可以看到 页面A 和 页面B 是可以 互相访问 到对方窗口的路径。
如果这个时候在 页面B 加上文章开头提到的 代码片段,那么显然页面将会发生变化。
跨域的情况
这时候我们修改 页面A 加载 页面B 的地址,使其不符合同源策略。
理所应当的是,两个页面不能够相互访问了,这才是正常的,否则内嵌第三方页面可以互相修改,那就太不安全了。
场景复现(客户端)
既然 Web 端是符合预期的,那是不是 Electron 自己的问题呢?
我们通过 electron-vite 快速搭建了一个 React模板的electron应用,版本为:electron@22.3.27,并且在 App 中也嵌入了刚才的 页面B。
function App(): JSX.Element { return ( <> <h1>这是Electron页面</h1> <iframe id="iframe" src="http://localhost:3000/2.html"/> </> ) } export default App
对不起,干干净净的 Electron 根本不背这个锅,在它身上的表现如同 Web端 一样,也受同源策略的限制。
那么肯定是我的项目里有什么特殊的配置,通过对比主进程的代码,答案终于揭晓。
new BrowserWindow({ ..., webPreferences: { ..., webSecurity: false // 就是因为它 } })
Electron 官方文档 里是这么描述 webSecurity 这个配置的。
webSecurity boolean (可选) - 当设置为 false, 它将禁用同源策略 (通常用来测试网站), 如果此选项不是由开发者设置的,还会把 allowRunningInsecureContent设置为 true. 默认值为 true。
也就是说,Electron本身是有一层屏障的,但当该属性设置为 false 的时候,我们的客户端将会绕过同源策略的限制,这层屏障也就消失了,因此 iframe 的行为表现得像是嵌套了同源的站点一样。
解决方案
把这个配置去掉,确实是可以解决这个问题,但考虑到可能对其他功能造成的影响,只能采取其他方案。
如文章开头提到的,用 webview 替换 iframe。
webview 是 Electron的一个自定义元素(标签),可用于在应用程序中嵌入第三方网页,它默认开启安全策略,直接实现了主应用与嵌入页面的隔离。
因为目前这个需求是仅作展示,不需要与嵌套页面进行交互以及复杂的通信,因此在一开始的开发过程中,并没有使用它,而是直接采用了 iframe。
而 iframe 也能够实现类似的效果,只需要添加一个 sandbox 属性可以解决。
MDN 中提到,sandbox 控制应用于嵌入在 <iframe> 中的内容的限制。该属性的值可以为空以应用所有限制,也可以为空格分隔的标记以解除特定的限制。
如此一来,就算是同源的,两者也不会互相干扰。
总结
这不是一个复杂的问题,发现后及时修复了,并没有造成很大的影响(还好是自己人用的平台)。
写这篇文章的主要目的是为了记录这次事件,让我意识到在平时开发过程中,把注意力过多的放在了 业务、样式、性能等这些看得见的问题上,可能很少关注甚至忽略了 安全 这一要素,以为前端框架能够防御像 XSS 这样的攻击就能安枕无忧。
谨记,永远不要相信第三方,距离产生美。
如有纰漏,欢迎在评论区指出。