大家好,今天和大家讨论 nodeIntegrationInSubFrames
, 这个选项看起来和 nodeIntegration
很像,不过后面跟了 InSubFrames
,说明是在 SubFrames
中开启 Node.js
这是一个实验性质的选项,决定是否允许在子页面(iframe
)或子窗口(child window
)中集成Node.js
; 预先加载的脚本会被注入到每一个iframe
,你可以用 process.isMainFrame
来判断当前是否处于主框架(main frame
)中
https://www.electronjs.org/zh/docs/latest/api/structures/browser-window-options
公众号开启了留言功能,欢迎大家留言讨论~
这篇文章也提供了 PDF
版本及 Github
,见文末
0x01 简介
0x02 SubFrames
0x03 测试 iframe
1. 搭建 iframe 服务器
2. 搭建测试环境
3. 测试执行 Node.js
4. 测试预加载脚本
5. 小结
0x04 测试子窗口
0x05 探索可能的子窗口
1. webview 标签
2. WebContentsView
3. object
4. embed
0x06 总结
0x07 PDF 版 & Github
往期文章
官方文档中 SubFrames
是指 iframe
和子窗口,那 iframe
和子窗口到底是用来干嘛的呢?
其实都是为了在一个页面中嵌入其他页面,例如我想在搜狐的网站中嵌入一段人民日报的新闻页面
这种行为在 Electron
官方文档中叫做 Web
嵌入,关于 web
嵌入,后续我们还会出单独的文章进行讨论
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/iframe
https://www.electronjs.org/zh/docs/latest/tutorial/web-embeds#iframes
iframe
在之前已经参与了很多测试了,在 web
技术中也包含,大家了解得可能已经比较透彻了
通过 iframe
的内容本身有自己独立的上下文(context
),而嵌入它的网页被称为父级浏览上下文,当然这是可以嵌套的,就像物理机里装虚拟机,在虚拟机里又装了虚拟机一个道理,而最终的物理机被称为顶级浏览上下文
在 Electron
之前的测试中,我们只用到了一个窗口,我们一直称之为主窗口,但从逻辑角度来说,没有子窗口的存在,也就没有什么主窗口之说
大家有些时候在使用应用程序的时候,点击某个功能会跳出来一个新的窗口,这个就叫做子窗口
举个例子,我们在电脑版微信中查看公众号文章时,点击文章,会出现一个新的窗口来显示文章内容,而不是在原本的窗口呢,这样原本的窗口可以继续聊天等
https://www.electronjs.org/zh/docs/latest/api/browser-window#%E7%88%B6%E5%AD%90%E7%AA%97%E5%8F%A3
创建子窗口的方式也比较简单
`const { BrowserWindow } = require('electron') const top = new BrowserWindow() const child = new BrowserWindow({ parent: top }) child.show() top.show() `
在配置参数中添加 parent: xxx
指定父窗口即可
问题来了,为什么要设置父子窗口呢?
在之前的一些版本中,似乎子窗口会继承父窗口的一些配置,但后来主要是为了生命周期等,简单来说,我把父窗口关了,子窗口也会被关闭或其他设置
该参数要在父窗口初始化是配置,而不是子窗口
192.168.31.216
1.html
`<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <div> <h1>iframe 页面 - 1.html</h1> <script src="iframe_1.js"></script> </div> </body> </html> `
其中 iframe_1.js
`require('child_process').exec('deepin-music'); `
同时,我们再搭建一个 iframe
+ window.open
的 2.html
`<!DOCTYPE html> <html> <head> </head> <body> <div> <h1>iframe 2.html</h1> <script>window.open("http://192.168.31.216/3.html")</script> </div> </body> </html> `
其中 3.html
执行 iframe_2.js
,打开相册
`<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <div> <h1>iframe 页面 - 3.html</h1> <script src="iframe_2.js"></script> </div> </body> </html> `
iframe_2.js
`require('child_process').exec('deepin-album'); `
关闭 CSP
,关闭 sandbox
,在 index.html
中嵌入 iframe
main.js
`// Modules to control application life and create native browser window const { app, BrowserWindow } = require('electron') const path = require('node:path') function createWindow () { // Create the browser window. const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { sandbox: false, nodeIntegrationInSubFrames: true, preload: path.join(__dirname, 'preload.js') } }) // and load the index.html of the app. mainWindow.loadFile('index.html') // Open the DevTools. // mainWindow.webContents.openDevTools() } app.whenReady().then(() => { createWindow() app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) `
index.html
`<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> --> <title>Hello World!</title> </head> <body> <h1>Hello World!</h1> We are using Node.js <span id="node-version"></span>, Chromium <span id="chrome-version"></span>, and Electron <span id="electron-version"></span>. <!-- You can also require other files to run in this process --> <script src="./renderer.js"></script> <iframe src="http://192.168.31.216/1.html"></iframe> <iframe src="http://192.168.31.216/2.html"></iframe> </body> </html> `
以上安全配置总结为:
nodeIntegration: false
contextIsolation: true
sandbox: false
nodeIntegrationInSubFrames: true
执行测试
并没有成功执行,我们尝试将安全限制都放开,看看能不能执行
nodeIntegration: true
contextIsolation: false
sandbox: false
nodeIntegrationInSubFrames: true
这次的结果是 iframe
中的 Node.js
成功执行了,但是 iframe + window.open
打开的窗口执行的 Node.js
代码执行失败了
iframe + window.open
在 Electron 14.0
之前版本是可以执行的
因此想要在 iframe
中执行 iframe
,需要
sandbox: false
nodeIntegration: true
contextIsolation: false
nodeIntegrationInSubFrames: true
缺一不可
这里一定要注意各个安全配置的默认配置,在 2024-04-25
左右,我们 NOP Team
向 Electron
报告了一个安全问题,即虽然官网多个地方强调 sandbox
默认在 Electron 20.0.0
版本开始默认被设置为 true
,官网原话是
从 Electron 20 开始,渲染进程默认启用了沙盒,无需进一步配置。
**但是经过我们的测试,这个配置选项在目前最新版本 Electron 30.0.2
及之前的版本中默认并未设置为 true
**
目前我们已经等了 Electron
一周了,还没有在 Github
上给我们反馈,所以这篇文章也会在 Electron
确认并修复漏洞后发布
官网还提到一个功能,就是 Preload
会被注入到每一个 iframe
我们在 Preload
中创建一个 变量/常量,让 iframe
中的脚本 alert
弹窗显示出来
preload.js
`window.iframe1 = "iframe_1 has got the value "; window.iframe_open = "iframe_window_open has got the value"; `
修改 iframe_1.js
为
`if (window.iframe1 !== undefined) { alert(window.iframe1); } else { alert("iframe has got nothing"); } `
修改 iframe_2.js
为
`if (window.iframe_open !== undefined) { alert(window.iframe_open); } else { alert("iframe + window.open has got nothing"); } `
关闭大部分安全配置
sandbox: false
nodeIntegration: true
contextIsolation: false
nodeIntegrationInSubFrames: true
执行测试
结果与上面一致,iframe
本身成功获取到了 Preload
中的内容, iframe + window.open
获取失败
iframe + window.open
在 Electron 14.0
之前版本是可以成功获取的
测试一下不同安全配置下,iframe
获取 preload
脚本中的内容的情况
经过测试,发现 nodeIntegrationInSubFrames
让 iframe
获取 preload
中暴露的方法和值只和 nodeIntegrationInSubFrames
本身有关,不受 sandbox
、nodeIntegration
、contextIsolation
影响,当然,Preload
暴露方法和值的方式受 contextIsolation
影响,当 contextIsolation: true
时需要通过 contextBridge
进行对外暴露
我这边也测试了一下, contextIsolation: true
时,开启 nodeIntegrationInSubFrames
后, iframe
也只是能获取到 contextBridge.exposeInMainWorld
暴露的方法和值,并不能获取到 Preload
中直接通过 window.xxx
这种形式设置的内容
nodeIntegrationInSubFrames
对于 iframe
有两个作用,第一个是赋予 iframe
执行 Node.js
的能力,但是条件比较苛刻,需要同时满足
sandbox: false
nodeIntegration: true
contextIsolation: false
nodeIntegrationInSubFrames: true
第二是让 iframe
获取到 Preload
中暴露的方法和值,这个功能只需要设置 nodeIntegrationInSubFrames: true
即可
经过测试,nodeIntegrationInSubFrames
不会让 preload
脚本获得额外执行 Node.js
的能力
所以这个配置项在一些社区在名字问题上争议比较大,默认人员认为这个名字不是很合理
这个子窗口是让我比较疑惑的,我看创建子窗口的时候,子窗口可以有自己的安全配置呀,难道没有设置 nodeIntegrationInSubFrames
或设置 nodeIntegrationInSubFrames: false
后,即使子窗口设置了渲染进程可以执行 Node.js
也不会生效吗?
这听起来就很奇怪,我们测试一下就知道了
我们尝试创建子窗口,在主窗口中设置 nodeIntegrationInSubFrames: false
,并在子窗口设置渲染进程可以执行 Node.js
,咱们看看到底能不能执行
main.js
`// Modules to control application life and create native browser window const { app, BrowserWindow } = require('electron') const path = require('path') function createPatentWindow () { // Create the browser window. const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegrationInSubFrames: false, preload: path.join(__dirname, 'preload.js') } }) // and load the index.html of the app. // mainWindow.loadFile('index.html') return mainWindow } function createChildWindow (parentWindow) { const childWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { sandbox: false, nodeIntegration: true, contextIsolation: false, nodeIntegrationInSubFrames: true, parent: parentWindow } }) return childWindow } app.whenReady().then(() => { const parentWindow = createPatentWindow() parentWindow.loadFile('index.html') const childWindow = createChildWindow(parentWindow) childWindow.loadFile('child.html') // createWindow() app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) `
index.html
`<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP --> <!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> --> <title>Hello World!</title> </head> <body> <h1>Hello World!</h1> We are using Node.js <span id="node-version"></span>, Chromium <span id="chrome-version"></span>, and Electron <span id="electron-version"></span>. <!-- You can also require other files to run in this process --> <script src="./renderer.js"></script> </body> </html> `
child.html
这个是子窗口的主页面
`<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <div> <h1>Child Window Page</h1> <script>require('child_process').exec('deepin-music')</script> </div> </body> </html> `
执行测试
子窗口可以成功执行 Node.js
这样以来, nodeIntegrationInSubFrames
对子窗口 Node.js
的执行就没有影响了呀,而且经过我的测试,在生命周期方面,关闭父窗口,子窗口并不会跟着关闭
测试一下nodeIntegrationInSubFrames: true
时子窗口是否能够读取父窗口的 Preload
中的内容
获取失败,看起来官方文档中描述的 child window
并不是官方文档其他部分中的 child window
https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions
https://www.electronjs.org/docs/latest/api/browser-window#parent-and-child-windows
既然子窗口不是指主进程创建的窗口之间的父子关系,那么和 iframe
比较类似的应该就是 <webview>
和 WebContentsView
了,还有 HTML 中的 object
和 embed
https://www.electronjs.org/zh/docs/latest/tutorial/web-embeds
https://www.electronjs.org/zh/docs/latest/api/webview-tag
https://www.electronjs.org/zh/docs/latest/api/web-contents-view
官方是不建议使用 <webview>
标签来实现嵌入其他页面的,WebContentsView
是 ELectron 30.0.0
新添加的功能,用来替代原本的 BrowserViews
对于 webview
标签,在 Electron >= 5.0
版本后,默认不允许,使用的话必须在创建父窗口时显式地设置 webviewTag: true
直接使用上面测试 iframe
执行 Node.js
的服务器即可
经过测试发现, webview
标签加载嵌入的内容是否可以执行 Node.js
与 nodeIntegrationInSubFrames
并不相关,主要与父窗口安全配置以及 webview
标签本身配置有关系
https://www.electronjs.org/zh/docs/latest/api/web-contents-view#class-webcontentsview-extends-view
`const { WebContentsView } = require('electron') const view = new WebContentsView() view.webContents.loadURL('https://electronjs.org/') `
我们尝试在 baseWindow
中添加两个 WebContentsView
,看看 WebContentsView
的行为是不是受 baseWindow
的 nodeIntegrationInSubFrames
参数的影响
main.js
`// Modules to control application life and create native browser window const { app, BrowserWindow } = require('electron') const { BaseWindow, WebContentsView } = require('electron') app.whenReady().then(() => { const win = new BaseWindow({ width: 800, height: 400 , nodeIntegration: true, contextIsolation: false, sandbox: false, nodeIntegrationInSubFrames: true}) const view1 = new WebContentsView() win.contentView.addChildView(view1) view1.webContents.loadURL('http://192.168.31.216/1.html') view1.setBounds({ x: 0, y: 0, width: 400, height: 400, nodeIntegration: true, contextIsolation: false, sandbox: false}) const view2 = new WebContentsView() win.contentView.addChildView(view2) view2.webContents.loadURL('http://192.168.31.216/2.html') view2.setBounds({ x: 400, y: 0, width: 400, height: 400, nodeIntegration: true, contextIsolation: false, sandbox: false,nodeIntegrationInSubFrames: true }) app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', function () { if (process.platform !== 'darwin') app.quit() }) `
但比较遗憾的是,没有明确在官网找到更多的信息,尝试了过后也没有发现可以执行 Node.js
的,所以也就没有办法测试 nodeIntegrationInSubFrames
这个参数了
object
远程加载页面内容
1.html
开启 nodeIntegration
,关闭上下文隔离进行测试
页面正常嵌入了,但是 Node.js
代码没有执行
添加 nodeIntegrationInSubFrames: true
成功执行,经过测试,iframe
执行 Node.js
的条件与 iframe
一致
修改 object
服务器内容,获取并控制台输出预加载脚本暴露给渲染进程的值
设置 nodeIntegrationInSubFrames: true
成功获取到预加载脚本暴露给渲染页面的内容
nodeIntegrationInSubFrames
对 object
的影响与 iframe
一致
embed
远程加载页面内容
1.html
开启 nodeIntegration
,关闭上下文隔离进行测试
页面正常嵌入了,但是 Node.js
代码没有执行
添加 nodeIntegrationInSubFrames: true
成功执行,经过测试,embed
执行 Node.js
的条件与 iframe
一致
修改 embed
服务器内容,获取并控制台输出预加载脚本暴露给渲染进程的值
设置 nodeIntegrationInSubFrames: true
成功获取到预加载脚本暴露给渲染页面的内容
nodeIntegrationInSubFrames
对 embed
的影响与 iframe
一致
nodeIntegrationInSubFrames
这个配置项的含义随着其他配置项而呈现不同效果,目前来看,影响的对象主要是 iframe
、object
、embed
如果 nodeIntegrationInSubFrames
设置为 true
时, preload
脚本中暴露的方法和值等将向 iframe
、object
、embed
内暴露,也就是说iframe
、object
、embed
内部的内容中的 JavaScript
可以直接使用 Preload
脚本中定义好的功能和值
如果嵌入 iframe
、object
、embed
的宿主页面的安全配置为
sandbox: false
nodeIntegration: true
contextIsolation: false
nodeIntegrationInSubFrames: true
其中 sandbox
为 false
或默认即可,此时页面中嵌入的 iframe
、object
、embed
的内容可执行 Node.js
PDF
版
Github
有态度,不苟同