
Electron Fuses 完全指南用打包期魔法比特加固 Electron 应用的 9 个安全开关【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electronElectron 的很多底层能力如把整个应用当作 Node.js 运行的ELECTRON_RUN_AS_NODE、支持任意NODE_OPTIONS注入功能强大但生产环境很少用到反而会扩大攻击面。Fuses 就是 Electron 为此设计的一套打包期特性开关它是一段内嵌在 Electron 二进制中的比特位fuse wire在应用打包阶段、代码签名之前被翻转之后由操作系统级别的代码签名校验机制保证其不可被回改。读完本文你将完整掌握这 9 个 Fuse 各自的功能、默认状态与推荐策略并能用electron/fuses工具或手动改二进制的方式在发布流水线中精确地关闭不需要的特性。Fuses 的设计动机为什么需要二进制里的开关而不是源码或配置从安全角度看禁用那些强大但用不到的 Electron 特性能显著收紧应用的安全姿态。最典型的例子是ELECTRON_RUN_AS_NODE任何不使用该环境变量的应用都应该把它关掉以阻断一类常被恶意脚本利用的 living off the landLOtL攻击——攻击者借助系统里本就存在的可信程序完成提权、横向移动等行为。一个更朴素的诉求是别让我去 fork Electron为了让应用不用某个特性而修改 Chromium/Node 源码并长期维护一个自建分支技术难度和成本都高得离谱。Fuses 正是在这一背景下诞生的解决方案——从高层视角看它们是 Electron 二进制中的magic bits魔法比特可以在打包你的 Electron 应用时翻转用来启用或禁用某些特性/限制。Fuse 的可靠性建立在一个关键前提上它在代码签名之前被翻转因此签名之后操作系统负责保证这些比特无法被偷偷翻转回去——在 macOS 上由 Gatekeeper 之类的机制校验在 Windows 上则依赖 AppLocker 这类应用控制策略。也就是说Fuse 的安全承诺与应用的代码签名流程是绑定的。底层原理Sentinel、Fuse Schema 与 Fuse Wire 的二进制布局要理解 Fuses需要先弄清三个术语原文档将它们定义为词汇表Fuse Wire保险丝导线Electron 二进制中用来控制各 Fuse 的一段字节序列Sentinel哨兵一段静态的、已知的字节序列用来在二进制中定位 fuse wire 的位置Fuse SchemaFuse 模式fuse wire 的格式及每个比特位允许的取值。整个机制在仓库中由两个文件驱动。默认值与模式定义在 build/fuses/fuses.json5{ _comment: 任何以破坏性方式修改 fuse schema 都应使 _version 递增。永远不要移除某个 fuse 或改变其含义应将其标记为 rremoved, _schema: 0 off, 1 on, r removed fuse, _version: 1, run_as_node: 1, cookie_encryption: 0, node_options: 1, node_cli_inspect: 1, embedded_asar_integrity_validation: 0, only_load_app_from_asar: 0, load_browser_process_specific_v8_snapshot: 0, grant_file_protocol_extra_privileges: 1, wasm_trap_handlers: 1 }构建期脚本 build/fuses/build.py 负责读取这份 json5把它编译成一个 C 源文件。脚本里可以看到硬编码的哨兵字符串、版本号与 fuse 线段的生成逻辑SENTINEL dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX生成的 C 代码中维护了一个const volatile char kFuseWire[]数组其结构在原文档中被描述为| ...binary | sentinel_bytes | fuse_version | fuse_wire_length | fuse_wire | ...binary |四个组成部分的含义如下sentinel_bytes固定是这段精确字符串dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX作用是在二进制文件里可靠地定位后续字段fuse_version1 个字节其无符号整数值表示 fuse schema 的版本当前版本为 1fuse_wire_length1 个字节表示其后 fuse wire 中 Fuse 的数量fuse_wireN 个字节的序列每个字节代表一个 Fuse 及其状态取值仅有三类00x30Fuse 被禁用10x31Fuse 被启用r0x72该 Fuse 已被移除此时无论把字节改成 1 还是 0 都不会再有任何效果。模式文件_comment中的约定也印证了这一点schema 的任何破坏性改动都应递增_version且永远不要删除某个 Fuse 或改变其含义——只能标记为r这样才能让旧二进制的校验逻辑保持兼容。构建脚本还会为每个 Fuse 生成一个形如IsXxxEnabled()的 C gettergetter 读取kFuseWire[index]并判断是否为1。值得留意的是getter 中有一段仅在DCHECK_IS_ON()即 debug 构建下生效的逻辑它会先检查命令行里是否带有--set-fuse-name开关若存在则以开关值为准。这意味着开发与测试阶段可以不用重新打包二进制直接在命令行临时指定某个 Fuse 的状态详见下文测试章节。当前全部 9 个 Fuse 逐项详解下表先给出全景各小节再结合仓库源码逐项深入Fuseschema 键名默认状态electron/fuses选项名官方倾向建议run_as_node启用RunAsNode大多数应用可禁用cookie_encryption禁用EnableCookieEncryption大多数应用可安全启用node_options启用EnableNodeOptionsEnvironmentVariable大多数应用可安全禁用node_cli_inspect启用EnableNodeCliInspectArguments大多数应用可安全禁用embedded_asar_integrity_validation禁用EnableEmbeddedAsarIntegrityValidation大多数应用可安全启用only_load_app_from_asar禁用OnlyLoadAppFromAsar配合 asar 完整性校验使用load_browser_process_specific_v8_snapshot禁用LoadBrowserProcessSpecificV8Snapshot有安全收益需自行评估grant_file_protocol_extra_privileges启用GrantFileProtocolExtraPrivileges不使用file://时应禁用wasm_trap_handlers启用WasmTrapHandlers默认启用即可说明上表状态与 build/fuses/fuses.json5 中的 schema 默认值完全一致Fuse 名称列即原文档的小节命名。runAsNode是否响应ELECTRON_RUN_AS_NODE默认启用选项名FuseV1Options.RunAsNode该 Fuse 决定ELECTRON_RUN_AS_NODE环境变量是否被尊重。当它被禁用后主进程中的child_process.fork将无法按预期工作——因为 fork 依赖这个环境变量来把 Electron 当作 Node.js 启动。官方建议改用 Utility Process它在很多需要一个独立 Node.js 进程的场景例如 SQLite 服务器进程都能胜任。实现层面判断逻辑散落在各平台的入口代码中Linux 的 shell/app/electron_main_linux.cc 在IsRunAsNodeEnabled() IsEnvSet(electron::kRunAsNode)时才进入作为 Node 运行的启动路径macOSshell/app/electron_main_mac.cc、Windowsshell/app/electron_main_win.cc以及 macOS 的库入口shell/app/electron_library_main.mm都有同样的守卫。cookieEncryptioncookie 落盘是否加密默认禁用选项名FuseV1Options.EnableCookieEncryptionChromium 用 SQLite 数据库存放 cookie默认情况下值以明文写入磁盘。启用此 Fuse 后cookie 会像 Chromium 自身一样使用操作系统级加密密钥落盘。这是单向迁移务必谨慎启用后已有的未加密 cookie 会在写入时被加密但之后再禁用该 Fuse会导致 cookie 存储损坏且无法使用。[!IMPORTANT] 在 macOS 上此 Fuse 依赖与safeStorage相同的 Keychain 访问权限因此你的应用必须经过 代码签名对应 macOS 上需要代码签名才能调用的 API才能正常工作。在源码中cookie 加密通过 network service 的CookieEncryptionProvider注入实现。shell/browser/net/network_context_service.cc 读取IsCookieEncryptionEnabled()为真时创建CookieEncryptionProviderImpl并将其绑定到network_context_params-cookie_encryption_provider上。仓库的测试spec/fuses-spec.ts覆盖了启用后 cookie 的写入、读取、secure/httpOnly 属性以及跨会话持久化场景。nodeOptions是否尊重 Node 运行时环境变量默认启用选项名FuseV1Options.EnableNodeOptionsEnvironmentVariable该 Fuse 决定NODE_OPTIONS与NODE_EXTRA_CA_CERTS环境变量是否生效。NODE_OPTIONS可以把各种自定义选项传给 Node.js 运行时而应用在生产环境中通常用不到它。大多数应用可以放心禁用。运行时对NODE_OPTIONS的解析和清洗集中在 shell/common/node_bindings.cc主进程解析 Node 参数时会在 node_bindings.cc#L481-L488 依据IsNodeOptionsEnabled()决定是否把环境变量里的选项并入启动参数node_bindings.cc#L736 处也做了同样的守卫。禁用后即使环境变量被恶意或意外设置其内容也不会进入 Node.js 启动参数。nodeCliInspect是否接受 inspector 启动参数默认启用选项名FuseV1Options.EnableNodeCliInspectArguments该 Fuse 决定--inspect、--inspect-brk等参数是否被接受。被禁用时它还保证SIGUSR1信号不会触发主进程 inspector 初始化——也就是说外部进程无法通过发信号的方式悄悄为你的主进程打开调试端口。大多数应用可以放心禁用。实现证据集中在 shell/common/node_bindings.cc决定Node 是否允许被调试的回调在 node_bindings.cc#L454 直接返回IsNodeCliInspectEnabled()node_bindings.cc#L951 附近则用同一判断拦截SIGUSR1对 inspector 的拉起。仓库测试spec/fuses-spec.ts专门验证了当node_cli_inspect为 0 时即便传入--inspect也不会出现 Debugger listening 输出。embeddedAsarIntegrityValidation加载app.asar时校验内容完整性默认禁用选项名FuseV1Options.EnableEmbeddedAsarIntegrityValidation该 Fuse 在 macOS 和 Windows 上启用加载app.asar时校验其内容的特性。它被设计为把性能影响降到最低但仍可能轻微拖慢从app.asar内部读取文件的速度。大多数应用可以放心启用。更完整的用法请参阅 Asar 完整性校验文档。在主进程启动早期shell/app/electron_main_delegate.cc#L127 会读取IsEmbeddedAsarIntegrityValidationEnabled()来配置相关的完整性校验能力。onlyLoadAppFromAsar收紧应用代码的搜索路径默认禁用选项名FuseV1Options.OnlyLoadAppFromAsar默认情况下Electron 定位应用代码的搜索顺序是app.asarappdefault_app.asar启用该 Fuse 后Electron只会搜索app.asar。把它与上面的embeddedAsarIntegrityValidation组合使用时就保证了不可能加载到未经校验的代码——这是防止攻击者把未签名/被篡改的代码放进行政目录、从而劫持应用启动的关键防线。加载路径的选择逻辑位于 shell/common/node_bindings.cc#L892-L901会依据IsOnlyLoadAppFromAsarEnabled()决定是否启用仅 app.asar的搜索模式。loadBrowserProcessSpecificV8Snapshot主进程使用专用 V8 快照默认禁用选项名FuseV1Options.LoadBrowserProcessSpecificV8SnapshotV8 快照可以改善应用启动性能V8 允许先对初始化好的堆拍快照之后直接加载快照省去重新初始化堆的开销。这个 Fuse 决定浏览器主进程使用哪个 V8 快照文件。默认情况下 Electron 各进程共用同一个 V8 快照文件启用后主进程改用名为browser_v8_context_snapshot.bin的快照其他进程仍使用各自原来的快照。为什么这样更安全为渲染进程和主进程使用不同快照能提升安全隔离——尤其是确保渲染进程不会加载带有nodeIntegration的快照。shell/app/electron_main_delegate.cc#L398 处正是依据IsLoadBrowserProcessSpecificV8SnapshotEnabled()决定是否使用主进程专用快照文件。原文档还提示了一个重要的性能权衡当主进程运行在自定义 V8 快照上无论是本 Fuse 生效还是v8_context_snapshot.bin被electron-mksnapshot生成的快照替换Electron 会改为从源码启动主进程的 Node.js 环境而不是使用内嵌的 Node.js 启动快照——这样才能让自定义快照中的对象对主进程可见。这自然会抵消一部分内嵌快照带来的主进程启动时间收益。grantFileProtocolExtraPrivilegesfile://页面的额外权限默认启用选项名FuseV1Options.GrantFileProtocolExtraPrivileges该 Fuse 控制从file://协议加载的页面是否被授予超出传统浏览器的额外权限。这种特权行为是早期 Electron 的核心设计但如今应用应当改用自定义协议来服务本地文件参见安全指南第 18 条中对 file 协议的回避建议因此如果你不从file://提供页面就应禁用此 Fuse。该 Fuse 授予的额外权限在原文档中被不完整地列出如下file://页面可以用fetch跨file://加载其他资源file://页面可以使用 Service Workerfile://页面对其同样运行在file://协议下的子框架拥有 universal access不受 sandbox 设置限制。实现上shell/app/electron_content_client.cc#L151 根据IsGrantFileProtocolExtraPrivilegesEnabled()决定是否把file://视为可授予特权的方案。仓库测试spec/fuses-spec.ts验证了当该 Fuse 为 0 时file://页面里通过fetch去读另一个file://资源示例中甚至直接尝试读取/etc/passwd会被以 Failed to fetch 拒绝。wasmTrapHandlersWebAssembly 越界访问的信号处理默认启用选项名FuseV1Options.WasmTrapHandlers该 Fuse 控制 V8 是否使用信号处理器来捕获 WebAssembly 的越界Out of Bounds内存访问。开启时的工作方式是在 WebAssembly 内存四周布置巨大的 guard 区域再安装信号处理器来拦截对 guard 区域的访问尝试。其内存布局如下| Guard Pages | WASM heap | Guard Pages | |-----8GB-----| |-----8GB-----|该特性仅支持以下 64 位系统Linux、macOS、Windows 的 x86_64 架构Linux、macOS 的 aarch64 架构。当 Fuse 被禁用时V8 会在生成的 WebAssembly 代码中插入显式边界检查来保证内存安全但这带来三个缺点每条内存引用都会多生成额外的编译节点节点处理耗时增加导致编译时间变长这些额外节点进一步产生大量额外代码使 WebAssembly 模块体积超出理想大小尤其是每次内存访问前的比较与分支指令会带来可观的运行时开销。因此除非目标平台不支持或确有兼容性问题一般建议保持默认启用。该 Fuse 的检查同时存在于浏览器进程shell/browser/electron_browser_main_parts.cc#L300、渲染进程shell/renderer/electron_renderer_client.cc#L290以及 Node 子进程服务shell/services/node/node_service.cc#L146的启动路径中。开发与测试用--set-fuse-*命令行开关临时翻转 Fuse正常发布的 Electron 二进制中Fuse 状态在打包时被固化。但仓库为开发测试预留了一个便捷通道构建脚本生成的每个 getter 在DCHECK_IS_ON()的构建里会优先读取形如--set-fuse-name的命令行开关值见 build/fuses/build.py 中HasSwitch({switch_name})与GetSwitchValueASCII的逻辑。也就是说在 debug/带 DCHECK 的测试二进制中你可以直接以命令行参数覆盖 Fuse 状态而不必重新打包。spec/fuses-spec.ts 恰好就是利用这一机制编写的运行时测试例如用--set-fuse-node_options0启动后即使设置了NODE_OPTIONS: -e 0也不会被 Node 执行未禁用时会以错误码 9 退出用--set-fuse-node_cli_inspect0启动并传入--inspectstderr 中不会出现调试器监听输出用--set-fuse-grant_file_protocol_extra_privileges0启动后file://页面的跨 file 资源 fetch 会被拒绝用--set-fuse-cookie_encryption1启动后cookie 的写入、读取与跨会话持久化都正常工作。这也意味着你可以在本地验证某个 Fuse 关闭后对我的应用有没有影响再决定是否把该状态固化到发布构建中。如何翻转 Fuses简单方式使用electron/fuseselectron/fuses是专为翻转这些 Fuse 设计的 JavaScript 工具库。下面的示例把runAsNode关闭const { flipFuses, FuseVersion, FuseV1Options } require(electron/fuses) flipFuses( // Path to electron require(electron), // Fuses to flip { version: FuseVersion.V1, [FuseV1Options.RunAsNode]: false } )其中version: FuseVersion.V1对应 schema 的_version: 1见 build/fuses/fuses.json5。翻转动作应该发生在打包之后、代码签名之前这样签名才能锁定这些比特位。工具还提供了 CLI可以校验已翻转的 Fuse或查看任意 Electron 应用的 Fuse 状态npx electron/fuses read --app /Applications/Foo.app[!NOTE] 如果你用 Electron Forge 分发应用可以通过electron-forge/plugin-fuses在构建流程中翻转 Fuse——该插件已预装在 Forge 的所有模板中。困难方式手动编辑二进制[!IMPORTANT] 词汇回顾Fuse Wire——Electron 二进制中控制各 Fuse 的字节序列Sentinel——用于定位 fuse wire 的静态已知字节序列Fuse Schema——fuse wire 的格式与合法取值。手动翻转意味着直接修改 Electron 二进制中的 fuse wire把它改成目标 Fuse 状态所对应的字节序列。二进制中总有一段字节具有如下形态| ...binary | sentinel_bytes | fuse_version | fuse_wire_length | fuse_wire | ...binary |操作步骤要点如下用二进制编辑器或脚本在整个文件中搜索哨兵字符串dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX来定位 fuse wire哨兵之后第一个字节是fuse_version无符号整数表示 schema 版本接着一个字节是fuse_wire_length表示其后 Fuse 的数量再往后按顺序排列fuse_wire的 N 个字节每个字节代表一个 Fuse00x30表示禁用、10x31表示启用、r0x72表示该 Fuse 已被移除改它无效找到目标 Fuse 在 fuse wire 中的位置把对应字节改成0或1即可。由于各 Fuse 在 schema 中的顺序对应 build/fuses/fuses.json5 的键顺序逐一对照即可确定每个字节归属哪个 Fuse。当前 schema 的权威视图也以该文件为准。落地建议一份可直接套用的发布前检查清单把原文档的逐条建议汇总成清单方便接入你的发布流程RunAsNode置为禁用——不使用ELECTRON_RUN_AS_NODE的应用都应关闭消除一类 LOtL 攻击面需要独立 Node 进程时改用 Utility ProcessEnableCookieEncryption置为启用——注意单向性启用前确认 macOS 代码签名与 Keychain 可用EnableNodeOptionsEnvironmentVariable置为禁用——生产环境基本用不到NODE_OPTIONS/NODE_EXTRA_CA_CERTSEnableNodeCliInspectArguments置为禁用——顺带阻止SIGUSR1拉起 inspector防止调试端口被外部触发EnableEmbeddedAsarIntegrityValidation置为启用——用极小的读文件开销换取app.asar内容完整性保障OnlyLoadAppFromAsar结合第 5 条启用——让只从app.asar加载且内容经校验成为唯一路径杜绝加载未经校验的代码GrantFileProtocolExtraPrivileges置为禁用——前提是应用已改为用自定义协议服务本地页面仍依赖file://页面互访的应用需自行权衡LoadBrowserProcessSpecificV8Snapshot与WasmTrapHandlers——前者按需启用并接受主进程启动时的快照回退代价后者一般保持默认启用即可。最后提醒一个所有方案共有的执行顺序先翻转 Fuse再对应用做代码签名。Fuse 之所以安全正是因为它把状态固化在签名内容里由操作系统在后续每次运行时替你监督这些比特没有被偷偷翻回去。【免费下载链接】electron:electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS项目地址: https://gitcode.com/GitHub_Trending/el/electron创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考