Electron应用的8种防护方式

Electron 桌面应用的 8 种加密防护方式

Electron 把网页技术(HTML、CSS、JavaScript)和桌面能力拼在一起,让团队能用一套前端技术做出 Windows、macOS、Linux 上都能跑的客户端。方便的代价也很明显:安装包本质是一堆可读文件,主进程、预加载脚本、渲染页、IPC、外链、日志都可能被拆开看、改、重打包再发出去。

下面按「包完整性、数字签名、代码混淆、IPC 白名单、Token 隔离、禁用调试、外链白名单、日志脱敏」八条线,把每一种防护在防什么、怎么理解、对业务和用户有什么好处讲清楚。

大家可以看见自己的Electron项目是否命中了以下哪一个

序号 防护项
1 包完整性校验加固
2 数字签名完整性加固
3 全进程代码混淆压缩
4 IPC 通信安全收束优化
5 Token 安全隔离优化
6 生产环境调试权限封禁
7 H5 外链域名白名单防护
8 日志敏感数据脱敏

写给第一次接触 Electron 安全的人

可以先把 Electron 应用想成三层房子:

  • 主进程(Main):房子的总闸和门锁。负责窗口、文件系统、系统托盘、自动更新、真正的敏感能力。
  • 预加载脚本(Preload):客厅和后厨之间的传菜口。它决定网页能不能、以什么方式跟主进程说话。
  • 渲染进程(Renderer):用户看见的页面。这里最像普通网页,也最容易被前端调试、被 XSS、被错误地塞进 Token。

攻击者通常不会一上来就「破解算法」。更常见的是:改几个文件再重新打包、换掉更新包、用开发者工具把接口和 Token 扒出来、通过任意 IPC 调用本不该暴露的能力、把用户骗到钓鱼页、或者从日志里抄走密码和验证码。

所以这 8 项不是八个互不相关的「加密开关」,而是一层层把门关上:先保证「跑的还是我们发的那份程序」,再保证「程序内部不能被随便调用、随便调试、随便外跳、随便把秘密写进日志」。


1. 包完整性校验加固

它在解决什么问题

用户下载的安装包、解压后的 app.asar、资源文件、主进程脚本,都可能在分发途中或落地后被改掉。盗版渠道常见手法是:拿到正版包 → 改启动逻辑、插广告、换登录页、加木马 → 重新打成「看起来一样」的安装包。用户双击后,界面可能几乎一样,后台已经不是原厂逻辑。

包完整性校验的意思是:程序启动时(以及关键节点再次)对自己依赖的文件做「指纹核对」。指纹通常是哈希(例如 SHA-256):文件任意一个字节变了,指纹就对不上。对不上就拒绝运行,而不是「凑合继续开」。

可以把它理解成快递箱上的封条。封条完好,说明路上没被拆过;封条破了,就算箱子还在、标签还在,也不能当原装货用。

它具体防的是哪一类篡改

  • 替换主进程入口,让应用先加载第三方脚本再进正版界面。
  • 在资源里插入额外页面或脚本,用于弹窗、劫持登录、静默上报。
  • 改配置文件,把更新地址、接口域名、支付回调指到攻击者服务器。
  • 只改一小段文案或图标,让用户以为「只是换皮」,实际已经换核。

完整性校验的价值,恰恰在于它不假定攻击者会大改界面。哪怕只改一个字符,校验失败也应该拦下来。

对业务和用户的好处

第一,把「重打包就能跑」这条最便宜的盗版路径堵死。

没有完整性校验时,盗版成本往往只是会解包、会改 JS、会重新压缩。有了启动校验,改完的包即使用户装上了,也会在启动阶段失败。攻击者必须继续伪造校验逻辑、绕过校验点,成本从「改文件」抬到「对抗整套启动保护」。对大多数想快速捞一波的渠道来说,这就已经不划算。

第二,保护的不只是版权,更是用户账号和设备。

重打包很少只是「去掉授权」。更危险的是顺手加键盘记录、替换登录接口、在支付页插入表单。完整性校验等于在用户打开应用的第一秒问一句:「你手里这份,还是我们仓库里签过名、算过哈希的那份吗?」答不上来就不让进门,用户的密码、短信验证码、本地文件就不会交给一份来路不明的程序。

第三,给客服和安全团队一条清晰的「真伪分界线」。

用户说「我这个客户端被盗号了」,如果没有完整性概念,很难判断是正版被钓鱼,还是地下包被加料。有了校验失败日志和明确拦截,至少能把「非原厂包」从正版事故里剥离出来,减少误判,也减少「正版背锅」。

第四,对自动更新和热修复是底座,不是锦上添花。

后面要做的数字签名、更新包校验,都建立在「本地文件集合是可信的」之上。如果启动时连本地文件都不核,更新下来的文件再签名也只是「新文件可信,旧文件可能已经被换了」。包完整性是整条供应链防护的第一块砖。

第五,对合规和品牌是可对外讲的控制措施。

金融、教育、政务、企业内部桌面端,审计经常会问:如何保证客户端未被篡改?完整性校验是可以用「启动拦截 + 哈希清单 + 失败不可用」回答的措施,比只写一句「我们有加密」更站得住。

使用时要注意的边界

完整性校验防的是「文件被改了还继续跑」,不是防「内存里被调试器改了指令」,也不是防「用户在自己电脑上有最高权限」。它解决的是分发和落地后的静态篡改。和数字签名、反调试搭配,才形成完整故事。对用户体验上,失败提示应明确「安装包不完整或已被修改,请从官网重新下载」,避免用户以为是普通崩溃。


2. 数字签名完整性加固

它和「包完整性校验」差在哪

上一节像是程序自己拿清单对指纹。数字签名则多了一层:这份清单和这些文件,是不是「我们公司」签过名的。

可以这样记:

  • 哈希:证明「文件现在和当初算指纹时一样」。
  • 签名:证明「当初算指纹的人,是持有我们私钥的那一方」,并且中间人没法悄悄换一份「也对得上」的假清单。

如果只有本地哈希、没有签名,攻击者可以同时改文件和改哈希清单,让两边重新对齐。用户看到的仍是「校验通过」。数字签名把「谁有权宣布这份清单是真的」锁在证书和私钥上。没有私钥,就签不出系统或应用认可的签名。

全局校验程序文件,意思是:不只签安装包外壳,还要对主程序、依赖库、资源、更新包在分发链路上做验签;运行时再抽查关键文件的签名是否仍在、是否被替换成未签名或他人签名的文件。

它重点防范的分发链路风险

用户很少只从官网下一次包。真实路径往往是:官网 → CDN → 网盘镜像 → 内部 IT 分发 → 自动更新服务 → 用户磁盘。任何一环都可能被换包:

  • CDN 或对象存储被入侵,更新文件被替换。
  • 中间人在公司网络里劫持下载。
  • 内部人员误传测试包、或恶意替换共享盘里的安装包。
  • 更新服务只校验「文件能下下来」,不校验「是不是原厂签过的」。

数字签名就是在这条路上每隔一段就问:你是不是拿着我们的证书?文件有没有在签名之后被改过?

对业务和用户的好处

第一,补上「只做本地哈希」的最大漏洞:清单本身被一起伪造。

换个比方:哈希像超市价签,签名像盖了公章的价签。有人可以自己打印一张新价签贴上去;没有公章,收银系统(操作系统或应用验签逻辑)就不会认。这让「改文件 + 改校验表」的组合拳失效。

第二,操作系统级的信任可以借力,降低「用户点了仍中招」的比例。

Windows 的 Authenticode、macOS 的 Developer ID 与公证(Notarization),会让未签名或签名异常的应用弹出更刺眼的警告,甚至直接拦截。用户不一定懂哈希,但懂「系统说这个软件来路不明」。全局验签让正版在系统里是「绿的」,盗版和被换的包是「红的」。这是对普通用户最友好的一层。

第三,自动更新从「能更新」变成「只能更新成我们允许的版本」。

桌面端被打穿,很多不是启动时被改,而是更新时被喂了恶意包。更新通道一旦只看版本号不看签名,攻击者就能做一条假更新。签名校验要求:新包证书链正确、签名未过期、文件哈希与签名匹配,否则拒绝安装、拒绝覆盖。用户不会在一次「常规更新」后变成肉鸡。

第四,出了事后能追责和止损。

签名私钥应放在受限的构建机或硬件里。一旦出现「市场上出现带我们签名的异常包」,说明构建或密钥可能出事,可以立刻吊销证书、停更新、公告重签。如果从来没有签名体系,市场上的假包和真包在证据上几乎无法切开。

第五,对政企采购和安全测评是加分项,不是装饰。

很多甲方清单会写「客户端应具备代码签名与完整性保护」。这一项补的是对外叙事和真实风险之间最大的缺口:如果只有本地哈希、没有签名,你们已经能发现文件被改,但还不能在整条分发链上证明「改完的人假冒不了你们」。

第六,降低「内部误发」造成的事故面。

测试包、调试包、带后门的临时包,如果没有正式签名策略,很容易混进正式渠道。强制「仅生产证书签名的包可被正式环境接受」,等于给发布流程加了一道不会疲劳的门卫。

落地时要注意的几件事

签名不是「点一下签名工具就结束」。私钥保管、证书续期、时间戳(防止证书过期后旧包无法验证)、更新包与安装包同一套策略、签名失败时的用户提示,都要设计。否则会出现「开发机随便签一个」或「证书过期全员无法打开」这两种极端。落地时建议先定:签哪些文件、谁持有密钥、失败是阻断还是告警。对面向外部用户的客户端,阻断通常比静默告警更合适。


3. 全进程代码混淆压缩

它在解决什么问题

Electron 的主进程和预加载脚本,发布后往往仍是 JavaScript。没有混淆时,解包几乎能看到函数名、接口路径、分支条件、授权判断,像在读注释良好的源码。逆向的人要做的不是「破解」,而是「阅读」。

全进程代码混淆压缩,指主进程、预加载(以及有必要的渲染包)在发版时做:

  • 压缩(minify):去掉空格、缩短变量名、合并语句,减小体积,顺便降低可读性。
  • 混淆(obfuscate):把控制流打乱、字符串编码、关键逻辑变形,让「读起来像人写的业务代码」变成「读起来像机器搅过的代码」。

「升级压缩方式」通常意味着:以前可能只对渲染层的前端包做了 webpack/vite 压缩,主进程和 preload 仍是较明文的 Node 脚本;现在把这几段同样纳入更高强度的处理,避免出现「页面看不清、主进程一眼能看懂」的洼地。

为什么必须覆盖主进程和预加载

渲染层再乱,主进程如果明文,攻击者仍能看到:

  • 真正的付费校验、设备绑定、更新地址。
  • ipcMain.handle 注册了哪些通道。
  • 文件系统、剪贴板、系统命令相关的实现。
  • Token 如何读写、Cookie 如何种。

预加载更是「特权清单」。它暴露给页面的 API,就是渲染进程的合法武器库。预加载明文等于把「页面被允许做什么」写成说明书。

所以「全进程」不是口号,是堵住最短的那块板。

对业务和用户的好处

第一,把「只会搜关键词的逆向」挡在门外,让大多数抄袭和破解停在成本账上。

市场上大量盗版、破解、外挂,并不是国家级对抗,而是有人花几小时搜字符串 isVipcheckLicenseipcRenderer.invoke。混淆后,这类搜索会失效或变得极吵。能留下的,是愿意花几天几周做动态调试的人。对商业产品来说,这已经过滤掉最大头的机会主义攻击。

第二,延长「从泄露到被规模化利用」的时间窗。

安全里有一个很实际的指标:攻击者从拿到安装包到做出稳定破解工具,要多久。混淆不保证永远不被破,但能把窗口从「当晚出破解」拉到「一周后才有半成品」。这段时间够你们发版修复、够风控加规则、够法务和渠道下架盗版包。对活动期、新功能期尤其值钱。

第三,降低源码级商业机密被直接抄走的风险。

桌面端里常有:分成逻辑、风控规则、协议拼装、和硬件/驱动打交道的细节。这些一旦明文,竞争对手或灰产可以直接当文档。混淆后,抄袭者即使能跑起来,也很难完整、正确地移植你们的规则。保护的是研发投入,不只是防破解。

第四,和完整性、签名形成「看得见」与「看不懂」的配合。

完整性与签名保证「文件没被换」;混淆保证「文件即使被打开也难看懂」。只做前者,攻击者仍能精读正版逻辑再做外挂;只做后者,攻击者可以改文件去混淆逻辑。两项一起,才是「既不能随便改,改完也不好读」。

第五,对包体积和启动有附带收益,但主价值仍是提高门槛。

压缩能减小主进程脚本体积,对弱网更新、磁盘占用有好处。不过对外讲价值时,应把「提升逆向破解门槛」放在第一位,避免团队误以为这只是构建优化。

需要诚实告诉读者的限度

混淆不是加密保险箱,更不是「算法不可破解」。有经验的人仍可用调试器在运行时看解密后的字符串、看内存中的函数。它的定位是:提高成本,减少规模化、自动化的破解。 真正的密钥、真正的授权,仍应放在服务端。客户端混淆是让「本地被读懂」变难,不是让「本地成为唯一真理」。

另外,混淆过强可能影响堆栈可读性,线上排障要配合 Source Map 的受控管理(Source Map 绝不能跟安装包一起公开分发)。落地后建议确认:生产包无 Source Map、错误上报有内部映射、预加载暴露面没有因为压缩而漏网。


4. IPC 通信安全收束优化

先用生活例子讲清 IPC

Electron 里,页面(渲染进程)不能直接为所欲为地读硬盘、开新窗口乱来、执行系统命令,这些能力在主进程。两边要说话,走的是 IPC(进程间通信)

可以把它想成餐厅点餐:

  • 渲染进程是客人,只能通过菜单点菜。
  • 预加载是服务员,把菜单上的菜传给后厨。
  • 主进程是后厨,真正炒菜(读文件、发请求、写注册表)。

如果没有白名单,就等于后厨宣布:「客人说什么我们就做什么。」客人(或控制了页面的脚本)可以喊「把保险柜打开」「把所有订单发到我邮箱」。这就是「任意接口调用」。

「重构通用代理接口」在很多 Electron 项目里长这样:早期为了开发快,做了一个万能通道,例如 invoke('call', { module, method, args }),主进程反射式执行。功能接得很快,安全模型却是「页面能打到主进程里几乎任意函数」。所谓收束,就是拆掉或锁死这种万能代理,改成:只有名单上的通道、名单上的方法、经过参数校验的调用,才能到达主进程。

任意 IPC 有多危险

假设页面因为一个 XSS、一个被劫持的外链、一个恶意插件,能执行自己的 JavaScript。如果此时存在万能 IPC:

  • 它可以尝试读取本地 Token、通讯录导出文件、下载目录。
  • 它可以让主进程发你们才有权限发的内部请求。
  • 它可以打开隐藏窗口、静默截图、改自动更新地址。
  • 它可以把「本该仅内部工具使用」的调试方法当正式 API 用。

注意:这里不一定需要用户「安装了盗版」。正版应用 + 页面被注入,加上过宽的 IPC,就足以在用户机器上完成很多事。IPC 收束保护的是「页面失守之后,还能不能撬开整台客户端」。

对业务和用户的好处

第一,把爆炸半径从「整个客户端」收到「当前页面能看到的那一点」。

安全设计里这叫最小权限。页面只需要「拉消息列表」,就不该拥有「读任意路径」。白名单让即使渲染层被打穿,攻击者也拿不到菜单上没有的菜。用户丢的是一次会话或一块页面数据,而不是整机文件和主进程能力。

第二,让安全评审和以后的功能迭代变得可审计。

万能代理的可怕之处是「能力是隐式的」:新同事加一个内部方法,页面立刻能调。白名单把能力变成显式清单:通道名、参数类型、谁能调、是否要登录态。审计时可以打印这张表,问「为什么渲染进程能删本地缓存以外的目录」。这项补的是长期治理,不只是修一个洞。

第三,阻断一类非常典型的「Electron 特有」攻击链。

传统网页 XSS 的上限,往往是当前站点的 Cookie 和 DOM。Electron 里 XSS 的上限,取决于你暴露了多少 Node/主进程能力。收束 IPC,就是把 Electron 重新拉回「更像浏览器、而不像把 Node 交给网页」的位置。这对防止「网页漏洞升级为本地漏洞」几乎是决定性的。

第四,减少误用和内部人员失误。

很多事故不是黑客,是业务页为了省事直接调了危险方法,或把内部工具通道留在了生产包。白名单默认拒绝,生产环境不会因为某次「临时 debug 接口忘了删」而敞开。对小团队尤其重要:人会忘,名单不会累。

第五,为下一节 Token 隔离铺路。

Token 要从渲染层拿走、只通过受控 IPC 领取,前提是 IPC 本身不是万能的。否则「受控获取 Token」会变成「任意调用获取 Token」。两项是一套:先收口子,再让口子里只传必要的短时令牌。

第六,对性能和稳定性也有间接好处。

通用反射代理往往缺少统一的超时、并发、参数大小限制。收束后可以给每个接口设配额:谁能刷文件、谁能打网络、payload 最大多少。恶意或失控的页面就不容易通过 IPC 把主进程拖死。

实施时建议怎么向产品解释「为什么变慢了」

收束会让「前端直接调任意 Node 模块」这种爽快写法消失,有些功能要改成「先提需求、再开白名单」。这是在用一点研发摩擦,换用户机器不被一锅端。对外可以写:我们宁可功能接口变少、变严,也不让一个页面漏洞等于本地权限。


5. Token 安全隔离优化

Token 为什么不能放在渲染层

Token 是应用证明「你已经登录」的钥匙,常见于:Access Token、Refresh Token、会话 Cookie、设备令牌。渲染层是网页,具备网页的所有弱点:

  • 开发者工具里 localStoragesessionStorage 一眼能看见。
  • XSS 可以读页面能读到的存储和内存变量。
  • 前端日志、错误上报、第三方脚本都可能把存储内容带出去。
  • 用户或插件也能较容易地从渲染进程里掏数据。

「剥离渲染层 Token 存储,仅主进程留存」,意思是:页面不再持久化这把钥匙;钥匙放在主进程的内存或受操作系统保护的存储里;页面需要带身份访问接口时,通过受控 IPC 向主进程申请「使用令牌发一次请求」或「领取一个短寿命、窄权限的令牌」。

最准确的比喻是:银行卡不要放在餐厅桌子上(渲染层),要放在后厨保险柜(主进程),服务员(IPC)只允许按菜单代为结账,不允许把卡号抄给客人。

「通过受控 IPC 获取令牌」的两种常见形态

读者不必写成代码,但要理解产品形态:

  1. 页面不拿完整 Token:主进程代发需要鉴权的请求,页面只拿业务数据。页面被 XSS,也抄不走可用于任意 API 的钥匙。
  2. 页面只能拿短暂、受限的 Token:例如五分钟、仅某个接口、用完即废。即便泄露,窗口也小。

无论哪一种,前提都是第四节的白名单,以及「渲染层磁盘上不再出现完整长期 Token」。

对业务和用户的好处

第一,XSS 和「打开控制台复制 Token」不再等于「账号被完整盗走」。

这是对用户最直接的好处。现在很多桌面端登录态被盗,路径非常土:教用户开调试、或诱导安装扩展、或在页面插一段脚本,从 localStorage 把 Token 贴走,换一台设备继续用。Token 不在渲染层后,这条土路断了。攻击者要拿到和主进程同级的能力,成本高一个数量级。

第二,降低「前端任意依赖」带来的供应链风险。

渲染层往往会接统计、客服气泡、富文本、播放器。这些脚本一旦被投毒,传统架构下它们和你们的业务页共享存储。Token 在主进程,第三方脚本默认摸不到。这对「前端包很大、依赖很多」的 Electron 应用特别重要。

第三,让退出登录、强制下线、密钥轮转真正有效。

Token 散落在多个 webview、多个存储 key 里时,退出登录经常漏清。集中在主进程,登出就是一处销毁;服务端吊销后,主进程也可以立刻停发、停代请求。用户在网吧、共享电脑上的残留风险下降。

第四,方便做更细的使用策略,而页面无需知道策略细节。

例如:Token 仅内存存放、锁屏后作废、检测调试器后作废、按设备绑定。这些策略放主进程,页面只感知「需要重新登录」。安全策略升级不必改遍所有前端存储逻辑。

第五,减少日志、截图、客服远程协助时的意外泄露。

用户把「网络面板」「Application 面板」截图发给客服,是常见事故。渲染层没有 Token,这类截图的含金量会低很多。配合第八节日志脱敏,身份凭证从「到处都是」变成「只有主进程和受控通道知道」。

第六,对监管和等保类检查更好回答「密钥如何保管」。

「前端本地存储 JWT」在很多检查里是明确减分项。改为主进程保管、最小暴露,叙述上从「存在浏览器存储」升级为「隔离存储 + 受控使用」,更接近桌面端应有的模型。

落地时要同时设计的用户体验

隔离不是让用户频繁重新登录。应设计:主进程在操作系统钥匙串/凭据管理器中安全恢复、页面无感续期、过期时统一跳转登录。否则业务会为了体验把 Token 又塞回渲染层,防护回潮。安全隔离和「记住登录」可以共存,只是记住的位置换到了更安全的一层。


6. 生产环境调试权限封禁

它在关哪些门

开发时,开发者工具(DevTools)是生产力:看控制台、看网络、改 CSS、下断点。生产环境里,同一套能力会变成:

  • 查看前端源码、接口、隐藏路由。
  • 看本地存储、Cookie、(若未隔离)Token。
  • 在控制台里直接调用你们暴露给页面的 API。
  • 用远程调试(例如 Chrome DevTools Protocol、--inspectopenDevTools)从另一台机器连进来。

「全面禁用生产环境开发者工具与远程调试,关闭调试入口」,包括但不限于:快捷键打不开控制台、菜单没有「检查元素」、窗口对象不能被远程 attach、主进程不以调试端口监听、打包参数去掉 inspect、对常见「打开 DevTools」的调用做防护或直接无效。

可以这样理解:工厂的设备出厂时要把维修后门焊上。维修(开发)在车间里进行;卖到客户家里的机器,不应留一个谁都能拧开的检修盖。

为什么「我们又没把密码写在前端」仍然要关

很多团队觉得:反正有混淆、反正接口有鉴权,开着 DevTools 也无妨。实际上生产调试是攻击者成本最低的观察窗

  • 不必解包,不必懂 Node,会按 F12 就行。
  • 可以看到混淆后运行时的真实网络请求、真实业务字段。
  • 可以当场试调用 IPC、改前端状态,做「现场破解」。
  • 远程调试一旦开启,等于在用户电脑上留了一扇不用改文件就能进屋的门。

关调试不是羞辱用户「不许看」,而是承认:生产包的目标用户不是开发者,观察窗对灰产的价值远大于对普通用户的价值。

对业务和用户的好处

第一,大幅抬高「会一点前端就能现场拆应用」的门槛。

关闭 DevTools 后,机会主义者少了最顺手的工具。他们必须回到解包、hook、自写调试环境,其中一部分会直接放弃。和混淆是同一类收益:不追求绝对,追求让廉价攻击消失。

第二,保护还没迁走的敏感信息,并为 Token 隔离争取时间。

即便 Token 仍在渲染层、IPC 还不够收束,禁用调试也是一层「先止血」。普通用户和普通盗号脚本不再那么容易在官方包里一键复制。这是防御纵深:前面的门先锁上,后面的隔离和收束才更从容。

第三,减少远程调试被当成木马通道的风险。

inspect 端口、未鉴权的调试协议,在同一局域网或恶意软件配合下,可以让攻击者看到页面、执行脚本,甚至驱动主进程行为。生产环境关掉这些入口,等于关掉一类「不需要改安装包」的控制面。对在企业内网大规模部署的客户端尤其重要。

第四,降低内部误用生产包做调试,从而把环境信息带出的概率。

有人会用生产包连生产环境,再打开 DevTools 看真实用户数据、真实订单。封禁后,这类操作必须走受控的调试包或专用环境,减少「生产数据出现在个人电脑控制台」的合规问题。

第五,让完整性、混淆真正发挥作用。

如果用户能在运行时随意打开调试器改脚本、改内存中的前端逻辑,静态混淆的意义会被削弱。关调试是在运行时减少「边看边改」。它和完整性校验互补:一个防文件被换,一个防运行时被当实验场。

具体怎么关:不要只关一处

生产环境要关的不是「某一个按钮」,而是一整组入口。只改一处,别的口子还在:

要关的入口 不关会发生什么 推荐关法
窗口配置里的 DevTools F12、菜单「切换开发者工具」仍可能打开 webPreferences.devTools = false
代码里主动 openDevTools() 发版时忘了删,生产包自己弹控制台 app.isPackaged 包一层,生产永不调用
快捷键 F12 / Ctrl+Shift+I 用户或脚本模拟按键打开 主进程 before-input-event 拦截
右键「检查」 系统/默认菜单自带检查项 生产禁用默认菜单,或自绘菜单且不放该项
运行后又被打开 有人通过其他 API 强行打开 监听 devtools-opened,立刻 closeDevTools()
启动参数 --inspect--remote-debugging-port 别人用命令行给正式包开远程调试口 生产包检测到危险参数就退出
后续弹出的子窗口、<webview> 只关了主窗口,新窗口还能调试 web-contents-created 里对所有内容统一处理

判断「是不是生产」时,优先用 Electron 自带的 app.isPackaged:打成安装包后为 true,本地 electron . 跑源码时为 false。不要只看 NODE_ENV,有人会忘了在打包脚本里赋值。

下面按「一个能直接对照的主进程文件」来写。开发环境该开的调试全部保留;一旦打成安装包,这些门一起焊上。

案例:主进程一次性封禁

javascript 复制代码
// main.js
const { app, BrowserWindow, Menu } = require('electron');
const path = require('path');

function isProd() {
  return app.isPackaged;
}

// 生产包若被带上远程调试/检查参数,直接退出,不给端口机会
const DANGEROUS_ARGV = [
  '--inspect',
  '--inspect-brk',
  '--inspect-port',
  '--remote-debugging-port',
  '--remote-debugging-address',
];

function hasDangerousArgv(argv) {
  return argv.some((arg) =>
    DANGEROUS_ARGV.some((flag) => arg === flag || arg.startsWith(`${flag}=`))
  );
}

if (isProd() && hasDangerousArgv(process.argv)) {
  app.exit(1);
}

function applyDevtoolsLock(contents) {
  if (!isProd()) {
    return;
  }

  // 配置层已关时,多数快捷键会失效;这里再兜一层
  contents.on('devtools-opened', () => {
    contents.closeDevTools();
  });

  contents.on('before-input-event', (event, input) => {
    if (isDevtoolsShortcut(input)) {
      event.preventDefault();
    }
  });
}

function isDevtoolsShortcut(input) {
  const key = String(input.key || '').toLowerCase();
  if (key === 'f12') {
    return true;
  }
  // Windows / Linux: Ctrl+Shift+I / J / C
  if (input.control && input.shift && ['i', 'j', 'c'].includes(key)) {
    return true;
  }
  // macOS: Command+Option+I
  if (input.meta && input.alt && key === 'i') {
    return true;
  }
  return false;
}

function createWindow() {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      // 生产关闭开发者工具;开发环境保持可调试
      devTools: !isProd(),
      nodeIntegration: false,
      contextIsolation: true,
      sandbox: true,
      preload: path.join(__dirname, 'preload.js'),
    },
  });

  applyDevtoolsLock(win.webContents);

  // 生产环境不要写 win.webContents.openDevTools()
  if (!isProd()) {
    win.webContents.openDevTools({ mode: 'detach' });
  }

  win.loadFile('index.html');
}

app.whenReady().then(() => {
  if (isProd()) {
    // Electron 默认菜单里带「切换开发者工具」,生产换成不含该项的菜单,或直接去掉菜单
    Menu.setApplicationMenu(null);
  }

  createWindow();
});

// 子窗口、新 tab、<webview> 也走同一套锁,避免只关了主窗口
app.on('web-contents-created', (_event, contents) => {
  applyDevtoolsLock(contents);

  contents.setWindowOpenHandler(() => {
    // 如需允许开窗,新窗口同样不要带调试能力;这里按业务改
    return { action: 'deny' };
  });

  contents.on('will-attach-webview', (event, webPreferences) => {
    webPreferences.devTools = !isProd();
    webPreferences.nodeIntegration = false;
    webPreferences.contextIsolation = true;
  });
});

上面这段做了四件事,和前面那张表一一对应:

  1. 创建窗口时关掉 devTools 这是官方开关,生产包里窗口默认不具备开发者工具。
  2. 只有未打包时才 openDevTools() 避免有人把开发期的「自动弹出控制台」原样带进安装包。
  3. 快捷键和「打开后立刻关掉」做双保险。 devTools: false 之后,正常路径已经打不开;若仍有人调到了 openDevTools()devtools-opened 会马上关上。
  4. 启动参数里出现 inspect / remote-debugging-port 就退出。 正式包不应接受「我在命令行给你开一个调试端口」。

案例:右键菜单不要留「检查」

如果产品还需要右键菜单(复制、粘贴),不要用系统默认菜单,自己拼一份,且生产环境不要加 inspect 角色:

javascript 复制代码
const { Menu } = require('electron');

function showSafeContextMenu(contents, params) {
  const template = [
    { role: 'copy', label: '复制' },
    { role: 'paste', label: '粘贴' },
    { type: 'separator' },
    { role: 'selectAll', label: '全选' },
  ];

  // 仅开发环境提供「检查」,生产不要加这一项
  if (!app.isPackaged) {
    template.push(
      { type: 'separator' },
      {
        label: '检查元素',
        click: () => contents.inspectElement(params.x, params.y),
      }
    );
  }

  Menu.buildFromTemplate(template).popup();
}

// 在创建窗口后绑定
// win.webContents.on('context-menu', (_event, params) => {
//   showSafeContextMenu(win.webContents, params);
// });

案例:打包配置也不要给调试留口

主进程关了还不够,构建脚本里常见两类漏网:

javascript 复制代码
// 错误:安装包启动时仍带检查端口
// app.commandLine.appendSwitch('remote-debugging-port', '9222');
// app.commandLine.appendSwitch('inspect', '5858');

这两行只应出现在本地开发,或单独的「内部诊断包」里,不能进默认生产包。electron-builder 的启动参数同样不要写它们:

json 复制代码
{
  "build": {
    "productName": "YourApp",
    "asar": true,
    "nsis": {
      "oneClick": false
    }
  },
  "scripts": {
    "dev": "electron .",
    "start:debug": "electron --inspect=5858 .",
    "dist": "electron-builder --publish never"
  }
}

dev / start:debug 给研发用;dist 打出来的安装包走 main.js 里的 isProd() 逻辑,不再附加 inspect 参数。生产构建也请不要把 Source Map 打进安装包。

关完之后怎么自测

打一个和用户相同的安装包,不要用 electron . 代替:

  1. 打开应用,按 F12Ctrl+Shift+I(macOS 再试 Command+Option+I),控制台不应出现。
  2. 右键页面,不应出现「检查」或「Inspect」。
  3. 顶部菜单不应有「切换开发者工具」。
  4. 用命令行给安装包加 --remote-debugging-port=9222--inspect,进程应直接退出,本机用浏览器访问 http://127.0.0.1:9222 也不应连上。
  5. 应用内再打开一个新窗口或内嵌页面,重复上面几步,确认不是只关了首页。

开发机上继续用未打包方式调试即可:isProd() 为假时,devTools 仍是开的,openDevTools() 也会走。

对「高级用户想自己排查」的说明

封禁生产调试后,排障应走:日志(已脱敏)、错误上报、官方诊断模式(需额外授权或一次性口令)。不要为了少数技术用户永久留 DevTools。需要时可以做「仅调试包」「需服务端下发临时许可」的旁路,且旁路不得出现在默认生产包的菜单里。


7. H5 外链域名白名单防护

Electron 里的「打开一个网页」为什么更危险

桌面端经常要打开帮助中心、支付页、活动页、第三方登录、客服。实现上可能是:应用内 BrowserView / webview 打开 URL,或调系统浏览器,或 window.open

网页世界里,链接跳转的风险是钓鱼和 XSS;Electron 里还多一层:这个窗口往往还带着你们预加载脚本、还可能共享部分会话、还可能再通过 IPC 回主进程。一个「看起来像官方活动」的外链,如果落到攻击者域名上,页面里的脚本就在你们的壳子里执行。

「域名校验、拦截非法外链」,就是:应用内允许打开的地址,必须落在白名单域名(以及约定好的协议、路径规则)上。不在名单里的,一律不打开,或只在系统浏览器中打开且剥离预加载与登录态。

可以这样理解:公司工牌只能刷公司楼的门。别人发来一张「看起来像工牌」的链接,门禁(白名单)不认,门就不开。

它如何防范 XSS 和钓鱼

钓鱼: 用户以为在官方支付页输卡号,实际在假域名。白名单让应用内根本打不开那个假域名,或打开时失去官方壳和官方会话,钓鱼页少了「套着你们 UI 框架」的伪装。

XSS / 开放重定向: 业务里如果有 ?redirect=、运营配置的 banner 链接、聊天消息里的 URL,攻击者可能塞入 javascript:、恶意站点、或你们 CDN 上被插入的页面。域名与协议校验把「任意字符串当链接打开」改成「仅 https + 名单内主机」。javascript:data:、未知自定义协议应直接拒绝。

对业务和用户的好处

第一,切断「从官方客户端一跳进入钓鱼站」这条信任滥用。

用户对桌面客户端的信任,高于对随机短信链接的信任。攻击者最想要的就是:在你们窗口标题、图标都还在的情况下,打开一个外域页面。白名单保住的是「官方窗口里出现的页面,域名仍是官方承认的」。这是品牌信任,也是资金和账号信任。

第二,限制 XSS 的「第二跳」。

有的漏洞不直接偷 Token,而是先让页面跳到攻击者控制的 URL,再在新窗口里诱导授权、下载木马、复用未隔离的会话。外链拦截让漏洞利用少一跳,很多脚本化攻击会因此写不下去。

第三,给运营和增长一个必须遵守的安全边界,避免「临时加一个活动域」变成永久漏洞。

业务会不断加新域名:新落地页、新支付渠道、新客服。没有机制时,这些会以字符串形式散落在代码和配置里,谁都能加。白名单迫使「加域名 = 走变更和评审」。短期会觉得烦,长期能避免活动结束了恶意域名还留在包里。

第四,降低 webview 里第三方页面滥用预加载 API 的风险。

即使 IPC 最终会收束,外域页面一旦加载了带预加载的 webview,就可能摸到本该只给自家页面的能力。域名白名单可以从源头减少「外域页 + 特权预加载」的组合。即便 IPC 和 Token 隔离尚未做到位,这一层也是非常值钱的补偿控制。

第五,对邮件、IM、二维码里的「打开客户端并跳转」深链接同样有效。

很多客户端支持 myapp://open?url=。若不去校验 url,任何人做一张二维码就能让已安装用户在官方壳里打开任意站点。白名单应覆盖:应用内超链接、通知点击、协议唤起、主进程 shell.openExternal 的目标。漏掉深链接,防护会像只锁了大门、没锁车库。

第六,出了钓鱼事件时更好解释「客户端侧已拦截」。

安全事件沟通里,能说「非白名单域名在应用内不可打开」,比「请用户仔细看网址」更有力量。用户确实常常不看地址栏;客户端替他们看,是更符合人性的设计。

实施建议

白名单不要只写根域名就算完:要明确是否允许任意子域、是否允许端口、是否只允许 https、路径是否收敛。支付类建议精确到主机名。拦截时给用户一句人话:「该链接不在官方安全列表中,已阻止打开,请从官网重新进入。」不要只失败无提示,否则会被当成故障。


8. 日志敏感数据脱敏

日志为什么会变成「第二份密码本」

客户端要排障,就会打日志:登录失败原因、接口出入参、设备信息、页面埋点。开发阶段图省事,常把整个请求体、整个响应、甚至表单对象 JSON.stringify 打出来。生产环境若沿用同一套,日志里就会出现:

  • 密码、支付密码
  • Token、Refresh Token、Cookie
  • 短信验证码、邮箱验证码
  • 身份证、银行卡、手机号
  • 内部密钥、签名串

这些日志可能在:用户本地文件、上传到日志平台、卡在客服工具、躺在研发同学的下载目录。攻击者不必破解应用,只要拿到一份日志包。

「对密码、令牌、验证码等敏感字段自动脱敏」,是在日志离开业务函数之前,按字段名和模式把明文换成 ***138****0000、或只保留后四位。自动,是为了不依赖每个开发都记得「这里不能打」。

「自动」为什么比「写规范别打密码」更重要

规范会被截止日期打败。新接口加了 verifyCode,有人复制旧日志代码,规范看不住。自动脱敏靠的是统一拦截:日志库的包装层扫描 key(password、token、authorization、otp、smsCode)和明显的模式,再输出。人可以忘,管道不忘。

对业务和用户的好处

第一,杜绝一类最冤枉的泄露:自己把秘密写给自己,再写给平台。

很多泄露不是黑客入侵生产库,而是日志平台账号权限过宽、或一份 zip 发到了群里。脱敏后,即便日志被转发、被误存、被供应商看到,可读内容也不再是可登录的凭证。这是用很低成本,关掉很高频的事故类型。

第二,让「禁用 DevTools + 混淆」不会功亏一篑。

前面把门关得再好,日志若打印了 Authorization: Bearer ...,等于把钥匙复印在门卫登记簿上。做好脱敏,是让其他防护的成果能保住。安全是木桶,日志常常是最短的那块,而且最短得毫不戏剧性。

第三,保护的不只是攻击面,还有合规和用户隐私义务。

密码和验证码进日志,在很多合规口径下属于「过度采集或未加密存储个人信息」。手机号、证件号同样。自动脱敏让默认行为接近「最小必要」:排障仍能看到「验证码校验失败、错误码 10021」,看不到「验证码 834921」。用户知情同意里承诺的「保护账号安全」,在工程上有了对应实现。

第四,降低内部威胁和「好奇的眼睛」。

能看日志的人,通常多于能看生产数据库的人。运维、外包、值班、新同事都可能碰到完整请求体。脱敏把「看日志」和「持有用户凭证」分开,符合权限分离。对人员流动频繁的团队,这一点比防外部黑客更日常。

第五,提升事故响应时的可用性。

未脱敏时,一旦发现日志含密码,整段日志都可能被当「事故证据」封存,研发反而不敢继续用它排障。脱敏后,日志可以更放心地留存、检索、给多方协同,真正服务于稳定性。安全与效率在这里是同向的。

第六,反向倒逼接口设计更干净。

当日志系统统一遮盖敏感字段,团队会更早把字段命名规范起来(例如统一 password 而不是 pwd1),也会减少「把整个 user 对象打出来」。长期看,这是代码卫生,不只是安全项。

落地之后仍建议知道的细节

脱敏要覆盖:控制台、文件日志、崩溃堆栈里的自定义字段、上报到服务端的面包屑、IPC 调试日志。只遮 password 不够,还要遮 accessTokenidTokenset-cookieprivateKey。测试环境可以保留更细日志,但生产包必须走同一套脱敏管道,避免「生产以为关了、其实某个模块自己打了 console」。

验证码和 Token 的部分匹配也要处理:有人会打 msg: 您的验证码是 123456。除了字段名,还需要对常见中文模板做规则。对外可以强调「字段级 + 模式级」双层,而不是只做了个 replace。


八项如何叠在一起

把八项看成从外到内的四圈,会更好记。

最外圈:你跑的是不是我们的程序

  1. 包完整性:文件被改就别运行。
  2. 数字签名:文件和清单都必须是原厂签过的,分发路上换包无效。

第二圈:你就算打开了包,也别那么好读、那么好调

  1. 混淆压缩:主进程和预加载不再是说明书。
  2. 禁用生产调试:运行时也不给现成的观察窗和远程检修盖。

第三圈:页面即使失守,也别拿到整栋楼的钥匙

  1. IPC 白名单:菜单上没有的菜,后厨不做。
  2. Token 只在主进程:桌子上不再放银行卡。
  3. 外链白名单:官方壳子里不给陌生人开门。

最内圈:我们自己说话时也不把秘密说出去

  1. 日志脱敏:排障记录里没有密码本。

前四圈合在一起,才能同时挡住「改包就跑、明文乱读、F12 现场拆、日志里抄密码」,以及「分发链假冒、万能 IPC、钥匙放在网页里、外链钓鱼」。

对决策者可以这样概括:完整性、混淆、禁调试、日志脱敏,让正版包更不像「开箱即读的源码工程」;签名、IPC 收束、Token 隔离、外链白名单,则让正版包在「被打开、被注入、被诱导跳转、被换更新包」之后,仍然不把用户身份和本机能力交出去。


每项好处对照

  • 完整性校验:把盗版重打包从「改完就能用」变成「改完启动即死」。保护用户不被加料包收割,也保护品牌不被假客户端背锅。
  • 数字签名:让「同时伪造文件和校验表」以及「更新通道喂毒」变得必须先偷到私钥。它服务的是整条下载和更新链路,而不只是用户硬盘上的一次核对。
  • 混淆压缩:让主进程和预加载不再充当攻击教程,把破解从「阅读」变成「真正的逆向」,从而减少规模化破解工具的出现速度。
  • IPC 收束:承认渲染层会失守,并提前规定失守之后攻击者仍然买不到的能力。这是 Electron 区别于普通网站时,最应该补的那一课。
  • Token 隔离:让页面、第三方脚本、控制台、截图,都不再天然持有长期登录钥匙。账号被盗的最常见土路会被挖断。
  • 禁用调试:立刻拿掉最低成本的观察和远程进入方式,并在其他重构完成前先给生产环境止血。
  • 外链白名单:保住「官方窗口里的页面仍然是官方的」,让钓鱼和开放跳转无法借用你们的图标和预加载。
  • 日志脱敏:让排障体系不再生产第二份密码库,同时让更多人可以安全地看日志,而不扩大内部知情面。

写在最后

这八项里,真正像「加密」的其实不多。更多是完整性、身份(签名)、最小权限、隔离、减少观察面、减少泄露面。标题里的「加密防护」可以理解为:让未授权的人更难改、更难读、更难调、更难盗用身份。

请带着三个预期读完:

  1. 没有一项能单独包打天下。 只混淆不验签,改包仍可能跑;只验签不收 IPC,页面漏洞仍可能提权;只禁调试但日志打 Token,钥匙仍在文本里。
  2. 客户端防护提高的是成本,权威仍应在服务端。 授权、支付、验证码校验,最终以服务器结果为准。客户端负责不把路铺平。
  3. 八项里,签名、IPC、Token、外链这四项,往往是「已经能防小打小闹」之后,防「正经攻击链」最容易缺的环节。 签名对分发、IPC 和 Token 对运行时失守、外链对钓鱼,建议按风险而不是按开发爽感排期。

按这个顺序对外讲,看文章的人即使不懂 Electron,也能明白:这不是在堆术语,而是在一层层回答------用户打开的,还是不是我们发的那份程序;这份程序就算被盯上,会不会把用户的钥匙和电脑能力一起交出去。

相关推荐
郭邯1 小时前
用 Canvas 实现纯前端图片格式互转:PNG/JPG/WebP 一键切换,还能控制质量与尺寸
前端
shmily麻瓜小菜鸡1 小时前
JavaScript / TypeScript 易踩坑知识点完全指南 -假值(Falsy Values)完全指南
开发语言·javascript·typescript
小鱼咕噜咕噜1 小时前
Element UI的el-upload文件上传与ZIP文件解压上传
javascript
labixiong1 小时前
z-index: 99999 输给 z-index: 2?原生弹窗的 Top Layer 排位战:后进者居上、看得见却点不着
前端·html
deli0071 小时前
CSS 渐变生成器:拖两个颜色直接给代码,做按钮背景不用手写
前端
陈拉斯1 小时前
上线一个多人扫码网页娱乐工具,我在并发、SEO、PWA 和打包脚本上踩的 8 个坑
javascript
芳心粽伙饭1 小时前
HTML第八章 表单标签
前端·html
Darling噜啦啦2 小时前
从 SSE 到 LLM 流式输出:搞懂前端实时通信的两种姿势
前端·后端·llm