长亭百川云 - 文章详情

nodeIntegrationInSubFrames | Electron 安全

NOP Team

90

2024-07-13

0x01 简介

大家好,今天和大家讨论 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

  • 往期文章


0x02 SubFrames

官方文档中 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 指定父窗口即可

问题来了,为什么要设置父子窗口呢?

在之前的一些版本中,似乎子窗口会继承父窗口的一些配置,但后来主要是为了生命周期等,简单来说,我把父窗口关了,子窗口也会被关闭或其他设置

该参数要在父窗口初始化是配置,而不是子窗口

0x03 测试 iframe

1. 搭建 iframe 服务器

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.open2.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');   `

2. 搭建测试环境

关闭 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

3. 测试执行 Node.js

执行测试

并没有成功执行,我们尝试将安全限制都放开,看看能不能执行

  • nodeIntegration: true

  • contextIsolation: false

  • sandbox: false

  • nodeIntegrationInSubFrames: true

这次的结果是 iframe 中的 Node.js 成功执行了,但是 iframe + window.open 打开的窗口执行的 Node.js 代码执行失败了

iframe + window.openElectron 14.0 之前版本是可以执行的

因此想要在 iframe 中执行 iframe ,需要

  • sandbox: false

  • nodeIntegration: true

  • contextIsolation: false

  • nodeIntegrationInSubFrames: true

缺一不可

这里一定要注意各个安全配置的默认配置,在 2024-04-25左右,我们 NOP TeamElectron 报告了一个安全问题,即虽然官网多个地方强调 sandbox 默认在 Electron 20.0.0 版本开始默认被设置为 true ,官网原话是

从 Electron 20 开始,渲染进程默认启用了沙盒,无需进一步配置。

https://www.electronjs.org/zh/docs/latest/tutorial/sandbox

**但是经过我们的测试,这个配置选项在目前最新版本 Electron 30.0.2 及之前的版本中默认并未设置为 true **

目前我们已经等了 Electron 一周了,还没有在 Github 上给我们反馈,所以这篇文章也会在 Electron 确认并修复漏洞后发布

4. 测试预加载脚本

官网还提到一个功能,就是 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.openElectron 14.0 之前版本是可以成功获取的

测试一下不同安全配置下,iframe 获取 preload 脚本中的内容的情况

经过测试,发现 nodeIntegrationInSubFramesiframe 获取 preload 中暴露的方法和值只和 nodeIntegrationInSubFrames 本身有关,不受 sandboxnodeIntegrationcontextIsolation 影响,当然,Preload 暴露方法和值的方式受 contextIsolation 影响,当 contextIsolation: true 时需要通过 contextBridge 进行对外暴露

我这边也测试了一下, contextIsolation: true 时,开启 nodeIntegrationInSubFrames 后, iframe 也只是能获取到 contextBridge.exposeInMainWorld 暴露的方法和值,并不能获取到 Preload 中直接通过 window.xxx  这种形式设置的内容

5. 小结

nodeIntegrationInSubFrames 对于 iframe 有两个作用,第一个是赋予 iframe 执行 Node.js 的能力,但是条件比较苛刻,需要同时满足

  • sandbox: false

  • nodeIntegration: true

  • contextIsolation: false

  • nodeIntegrationInSubFrames: true

第二是让 iframe 获取到 Preload 中暴露的方法和值,这个功能只需要设置 nodeIntegrationInSubFrames: true 即可

经过测试,nodeIntegrationInSubFrames 不会让 preload 脚本获得额外执行 Node.js 的能力

所以这个配置项在一些社区在名字问题上争议比较大,默认人员认为这个名字不是很合理

0x04 测试子窗口

这个子窗口是让我比较疑惑的,我看创建子窗口的时候,子窗口可以有自己的安全配置呀,难道没有设置 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

0x05 探索可能的子窗口

既然子窗口不是指主进程创建的窗口之间的父子关系,那么和 iframe 比较类似的应该就是 <webview>WebContentsView 了,还有 HTML 中的 objectembed

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> 标签来实现嵌入其他页面的,WebContentsViewELectron 30.0.0 新添加的功能,用来替代原本的 BrowserViews

1. webview 标签

对于 webview 标签,在 Electron >= 5.0 版本后,默认不允许,使用的话必须在创建父窗口时显式地设置 webviewTag: true

直接使用上面测试 iframe 执行 Node.js 的服务器即可

经过测试发现, webview 标签加载嵌入的内容是否可以执行 Node.jsnodeIntegrationInSubFrames 并不相关,主要与父窗口安全配置以及 webview 标签本身配置有关系

2. WebContentsView

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 的行为是不是受 baseWindownodeIntegrationInSubFrames 参数的影响

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 这个参数了

3. object

1) embed 服务器

object 远程加载页面内容

1.html

2) 测试执行 Node.js

开启 nodeIntegration,关闭上下文隔离进行测试

页面正常嵌入了,但是 Node.js 代码没有执行

添加 nodeIntegrationInSubFrames: true

成功执行,经过测试,iframe 执行 Node.js 的条件与 iframe 一致

3) 测试预加载脚本

修改 object 服务器内容,获取并控制台输出预加载脚本暴露给渲染进程的值

设置 nodeIntegrationInSubFrames: true

成功获取到预加载脚本暴露给渲染页面的内容

4) 小结

nodeIntegrationInSubFramesobject 的影响与 iframe 一致

4. embed

1) embed 服务器

embed 远程加载页面内容

1.html

2) 测试执行 Node.js

开启 nodeIntegration,关闭上下文隔离进行测试

页面正常嵌入了,但是 Node.js 代码没有执行

添加 nodeIntegrationInSubFrames: true

成功执行,经过测试,embed 执行 Node.js 的条件与 iframe 一致

3) 测试预加载脚本

修改 embed 服务器内容,获取并控制台输出预加载脚本暴露给渲染进程的值

设置 nodeIntegrationInSubFrames: true

成功获取到预加载脚本暴露给渲染页面的内容

4) 小结

nodeIntegrationInSubFramesembed 的影响与 iframe 一致

0x06 总结

nodeIntegrationInSubFrames 这个配置项的含义随着其他配置项而呈现不同效果,目前来看,影响的对象主要是 iframeobjectembed

  • 如果 nodeIntegrationInSubFrames 设置为 true 时, preload 脚本中暴露的方法和值等将向 iframeobjectembed内暴露,也就是说iframeobjectembed 内部的内容中的 JavaScript 可以直接使用 Preload 脚本中定义好的功能和值

  • 如果嵌入 iframeobjectembed 的宿主页面的安全配置为

  • sandbox: false

  • nodeIntegration: true

  • contextIsolation: false

  • nodeIntegrationInSubFrames: true

其中 sandboxfalse 或默认即可,此时页面中嵌入的 iframeobjectembed 的内容可执行 Node.js

0x07 PDF 版 & Github

PDF

https://pan.baidu.com/s/1PJzvwu4YvBpwiBOsjuu0UA?pwd=14y2

Github

https://github.com/Just-Hack-For-Fun/Electron-Security

往期文章

有态度,不苟同

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2