Skip to content

网页按钮怎样安全读取本地文件

Renderer 展示的是 Web 页面,它会执行前端依赖、解析用户输入,也可能遭遇脚本注入。让页面直接 require('fs'),等于把本地文件系统权限交给整个页面上下文。安全链路应该是 Renderer 提交明确业务动作,Preload 暴露窄能力,Main 验证来源和参数。

窗口权限基线

js
const win = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
  },
})

Context Isolation 让 Preload 和页面脚本处于不同 JavaScript 世界。Renderer 拿到的是 contextBridge 暴露的方法,不是 ipcRenderer 本体。

Preload 只暴露业务能力

js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('deskLab', Object.freeze({
  openDemoFile: () => ipcRenderer.invoke('desklab:read-demo-file'),
}))

openDemoFile 没有任意 channel,也没有任意路径参数。页面只知道“读取示例文档”,不知道底层通过哪个 Electron 模块完成。

危险的接口通常长这样:

js
contextBridge.exposeInMainWorld('bridge', {
  send: (channel, payload) => ipcRenderer.send(channel, payload),
})

页面一旦被注入,攻击代码就能枚举 channel 和参数,把一项文件能力扩展成更多系统能力。窄接口能被逐项搜索、测试和撤销。

Main 保留最终授权

js
ipcMain.handle('desklab:read-demo-file', async event => {
  const senderUrl = event.senderFrame?.url ?? ''
  if (!senderUrl.startsWith('file://')) {
    throw new Error('unexpected sender')
  }

  return fs.readFile(demoFilePath, 'utf8')
})

真实项目还要验证 event 对应的 webContents、窗口身份、当前导航来源、参数类型和路径范围。路径在拼接后应先规范化,再确认仍处于允许目录;只做字符串前缀比较无法覆盖符号链接、编码和大小写差异。

一次请求的证据链

DeskLab 的 smoke 日志按顺序出现 request accepted、response sent、smoke observed,最终读取固定测试文件的 85 字节内容。请求编号可以把 Renderer 点击、Main 校验、文件读取和响应串成一条链。

审计顺序

  1. 列出 window 上新增的能力。
  2. 定位每个 Preload 方法使用的固定 channel。
  3. 检查 Main handler 的发送方、参数、路径和错误分支。
  4. 确认页面只拿到完成业务动作所需的结果。
  5. 配合 CSP、导航限制、Sandbox 和最小权限形成多层防线。
别急,先让缓存热一下。