桌面助手的权限边界要画在进程、IPC 与授权规则三层,弹窗只是体验。按 Electron 与 Tauri 官方安全文档拆解边界机制,给出一份 11 项可直接执行的验收清单与 4 类一票否决项。
桌面助手的权限边界不该画在“用户点没点同意”上,而要画在进程、IPC 与授权规则三层——弹窗是体验,不是边界。下文机制均取自 Electron 与 Tauri 的官方文档正文,文末 11 项清单可直接抄进测试用例。
“加个登录框”挡不住越权:桌面助手的攻击面在本机进程与模型输出上,而不在账号入口——模型推理、文件读写、命令执行都跑在用户机器里,账号体系管不到进程拿到了什么权限。两套主流桌面框架的官方安全模型都从同一前提出发:渲染层不可信,特权操作必须收敛到一个可信宿主进程。一个常被忽略的事实是,Electron 的进程沙箱文档直接写明:它无法做到与 Chromium 同等的安全等级——Safe Browsing、Certificate Transparency 这类依赖中心化权威与专用服务器的特性,在内置内核中是被禁用的。框架给的是机制,不是保证;缺的那部分要由应用自己补,并在验收文档里写清谁负责。
把边界画在 UI 上还有一个副作用:审计无法复现。弹窗点过几次、点了什么,日志里通常只剩“用户已同意”;而进程有没有真的被限制住,只能靠配置和代码来证明。这也是验收必须落到可测试的代码位置的原因。
Electron 自 12 版起默认开启 contextIsolation、自 20 版起默认启用渲染进程沙箱,而 nodeIntegration: true 会关掉该进程的沙箱——这三个默认值决定基线风险的高度。
window.hello = 'wave',网页侧读到的 window.hello 是 undefined。contextBridge.exposeInMainWorld('myAPI', { send: ipcRenderer.send }) 是把强 API 原样透传,等于让任何页面都能发送任意 IPC 消息。正确姿势是一条 IPC 对应一个方法,例如 loadPreferences: () => ipcRenderer.invoke('load-prefs')。electron 模块中的 contextBridge / crashReporter / ipcRenderer / nativeImage / webFrame / webUtils,以及 events / timers / url 和对应的 node: 形式,并 polyfill 出 Buffer、process、clearImmediate、setImmediate。代价是它是 polyfill:预加载脚本无法再用 CommonJS 拆成多文件,需要打包器。强制手段上,app.enableSandbox() 必须在应用 ready 事件之前调用,此后任何 sandbox: false 都会被覆盖;--no-sandbox 官方明确只建议测试使用、绝不要用于生产。以上出自官方的 Context Isolation 与 Process Sandboxing 两页正文。
边界之外还有一笔工程账要一起算:桌面端运行时在包体、内存占用与首字延迟上的实测口径,可参考我们此前整理的桌面端 AI 架构的工程账本;安全收敛方案越激进,越需要先知道这些数字的代价。
Tauri 把授权拆成命令、permission(命令与范围声明的组合)与 scope(授权范围)三层,其中 deny 永远压过 allow;范围声明需可序列化,一般由插件定义。
$APPLOCALDATA/** 递归访问,同时拒绝 $APPLOCALDATA/EBWebView/**——Windows 上 webview 的数据与配置就存在这里,放行可能造成敏感信息泄露;Linux 另有对应的拒绝项。官方把拒绝项先合并成 deny-default 集合,再与允许项组合成可读的 set,例如 read-files-applocaldata = 目录范围 + allow-read-file。这套结构本身就是给评审者看的。官方把安全拆成 Permissions、Command Scopes、Capabilities、Asset protocol scope、CSP、HTTP Headers、生命周期威胁等多个章节,IPC 侧则提供 Brownfield 与 Isolation 两种模式(见 官方文档索引)。设计意图很清楚:把“前端不可信”写进架构,而不是留在代码规范里靠自觉。
同一个越权动作在两套框架里被拦的位置并不相同:Electron 靠沙箱渲染进程加预加载白名单,Tauri 靠命令授权加 glob 范围,而两者的拒绝项都优先于允许项。
| 越权动作 | Electron 侧 | Tauri 侧 |
|---|---|---|
| 渲染层想直接读本地文件 | 沙箱渲染进程没有 Node 环境,只能把请求经 IPC 交给主进程;预加载脚本仅暴露隔离后的白名单方法 | fs 命令按 glob 路径做允许 / 拒绝,拒绝优先 |
| 页面想发出任意消息 | 不把 ipcRenderer.send 透传出去,一条 IPC 一个方法 | 命令需被 permission 授权,规则由命令实现校验 |
| 想访问任意 URL | 主进程侧出站白名单 + CSP | http 插件按登记域名过滤出站请求 |
| 想读 webview 自身数据目录 | 由应用自行声明拒绝范围 | 官方 deny-webview-data 示例:拒绝 $APPLOCALDATA/EBWebView/** |
模型生成的工具调用参数等同于不可信的 IPC 入参:它必须由宿主进程校验,而写文件、执行命令这类能力必须逐次人工确认——框架的沙箱与 scope 都只挡越权路径,不判断这一次参数是不是恶意的。工具调用是桌面助手里最容易失守的一环:前面那条反面示例(直通 ipcRenderer.send 会让任意页面发出任意 IPC 消息)在 AI 场景会换一种形态复现——一段被注入的文档内容,让模型生成一条删除目录的工具调用。三条硬约束:能力最小化(读、写、执行、出网各自独立的工具与授权范围)、参数校验放在宿主进程而不是提示词、不可逆动作要二次确认且可撤销。在提示词里写“不要删除文件”属于产品文案,不属于安全边界。
工具调用的副作用与重试语义(例如一个“失败”的删除请求其实已经执行)会直接决定二次确认该做在哪一层,可另见AI Agent 落地先过这关:超时、重试与副作用;企业侧把这类 Agent 平台化的落地复盘,见Schneider、monday.com、Vodafone 的企业 AI Agent 生产复盘。
这份清单把 Electron 官方安全页的 20 条建议收敛成 11 项可执行检查与 4 类一票否决项,按“每次发版必跑、版本回归、架构变更必做”三档执行,让安全验收变成可复现的动作而不是一次评审。
--no-sandbox、nodeIntegration: true、sandbox: false。官方对前两者的定性分别是“仅测试”与“关闭沙箱”。exposeInMainWorld 暴露的方法:是否与明确的 IPC channel 一一对应,是否存在把 ipcRenderer 整个对象或 send / invoke 直接透传。false;一旦为 false,预加载脚本的特权 API 可能泄漏给不可信渲染代码。../、符号链接、短路径变体做逃逸测试;未登记的 channel 一律拒绝。这 11 项里有 4 类属于一票否决:生产构建带 --no-sandbox、渲染层可达 Node 模块、入参无校验、AI 的写与执行动作无人工确认。命中任意一条,都不该通过内测准入。
问:开了 contextIsolation 加 contextBridge 就安全了吗?
答:不够。官方在同一页给出的反面示例就是 exposeInMainWorld('myAPI', { send: ipcRenderer.send })——隔离开着,但强 API 被原样透传,页面仍可发出任意 IPC 消息。隔离只解决“能不能碰到”,不解决“碰到了能做什么”。
问:什么情况下才允许 sandbox: false?
答:官方说法是,与沙箱不兼容的场景(例如渲染进程要使用原生 Node 模块)可以对单个进程关闭,但这会带来安全风险,尤其当该进程内存在不可信代码或内容时。能用 app.enableSandbox() 全局强制沙箱的项目,应优先强制。
问:把 Rust 侧的权限声明写全,就一定能防住越权吗?
答:不能只靠声明。规则只是传给命令,校验由命令实现完成;官方 caution 明确要求开发者确认不存在绕过,并审计校验逻辑。所以这类项目的验收重点是“读命令实现”,而不是“读配置文件”。
问:该选 Electron 还是 Tauri?
答:两套官方模型的第一原则一致:渲染层不可信、特权集中到宿主进程。真正的判断点是你能否把上面这份清单跑起来——能否审计预加载脚本或命令实现,能否为每个平台维护允许与拒绝两项。答不上来,换框架并不解决问题。选型阶段的取舍可另见桌面应用定制开发 2026:Electron/Tauri 选型与 AI 原生改造决策。
问:清单要多久跑一遍?
答:分三档即可——构建产物扫描与配置核对属于每次发版必跑;逃逸用例与出站白名单属于版本回归;命令实现与预加载脚本的人工审计属于架构变更时必做。把它固化成三档回归动作,比上线前补一次安全评审有效得多。
蓝曜炬辉在桌面端与 AI Agent 交付里,也按“框架声明 → 宿主校验 → 逐项测试”的顺序推进:边界先落到可测的代码位置,再谈体验优化。