Repository navigation
build: harden packaged builds with Electron fuses - #3442
Conversation
|
Thanks for superseding #3423, and for the credit - that means a lot coming from the maintainer. Worth recording the two mistakes in it, since they are the reason your route is the correct one and not just a stylistic preference:
Both were invisible to One thing I would still flag in your PR if useful: Closing #3423 now. |
Pin the hardening fuse set in electron-builder.js so packaged builds refuse ELECTRON_RUN_AS_NODE, NODE_OPTIONS and the Node inspector, encrypt the cookie store, load only from app.asar, and validate that archive before loading it. Because electron-builder leaves the Electron defaults in place when electronFuses is absent, scripts/verifyFuses.js reads the fuse wire back out of each built artifact and fails when it does not match the expected posture. The expected set is a constant in that script rather than read from the configuration it polices, which would otherwise pass vacuously once the configuration is removed. Both workflows run it after packaging. resetAdHocDarwinSignature is also set: flipping the fuses invalidates the ad-hoc signature Electron ships with, and build.yml packages macOS unsigned, so the arm64 artifact is killed on launch without it. Supersedes #3423, which reaches the same outcome by flipping fuses in scripts/afterPack.js. The configuration route needs no new dependency and resolves each platform's executable itself.
f39d874 to
c607bee
Compare
|



What
Pins Electron's fuses in
electron-builder.js, so packaged builds ship with the security switches turned off instead of Electron's defaults. Also adds a check after packaging that fails the build if an artifact isn't actually hardened.Supersedes #3423, which does the same thing with an
afterPackhook. This uses electron-builder's built-inelectronFusesconfig instead. Credit to @neemlama for finding and documenting this in #3423.Why
Shipped v7.9.0 builds use fuse wire version 1, so
RunAsNode,EnableNodeOptionsEnvironmentVariableandEnableNodeCliInspectArgumentsall sit at their permissive defaults, andapp.asaris never checked on load.None of this is remotely exploitable, and none of it is a bug in Gitify — these are Electron's defaults. The gap is that packaging never made a deliberate choice about them, and nothing confirmed that shipped builds were hardened.
The fuses
runAsNodeenableNodeOptionsEnvironmentVariableenableNodeCliInspectArgumentsenableEmbeddedAsarIntegrityValidationonlyLoadAppFromAsarTwo are left at their defaults on purpose:
enableCookieEncryption: tokens are already encrypted withsafeStorage, Gitify keeps no cookies worth protecting, and Electron documents the fuse as one-way.grantFileProtocolExtraPrivileges: the renderer loadsindex.htmloverfile://, so removing these privileges breaks asset and sound loading.electronFusesalso setsresetAdHocDarwinSignature, which isn't a fuse. Flipping fuses breaks the ad-hoc signature Electron ships with, and Apple Silicon won't launch a binary with an invalid signature. CI builds macOS unsigned (-c.mac.identity=null), so without this the arm64 build won't start.Why config instead of an
afterPackhookelectron-builder flips fuses in
doAddElectronFuses(), which runs betweensanityCheckPackage()anddoSignAfterPack()and works out each platform's executable path itself — including the lowercased Linux name,gitify. The hook route needs a second copy of@electron/fuses, its own per-platform path logic, and an explicitawait; without it, signing can race the fuse write.No dependency is added:
@electron/fusesalready comes in through electron-builder, andpnpm-lock.yamlis unchanged.Verification
doAddElectronFuses()silently does nothing if the config key is missing. Soscripts/verifyFuses.jsreads the fuses back from each built artifact and fails if they don't match the expected set. That set is hardcoded rather than read fromelectron-builder.js, because reading it from the config would pass trivially once the config was removed. Both workflows run the check after packaging.Verified locally on macOS (universal build, Electron 44.7.0, electron-builder 26.17.0):
Info.plisthasElectronAsarIntegrityforResources/app.asarverifyFuses.jspasses on the hardened bundle and fails when the fuse config is removedcodesign --verifypasses and the unsigned build launches on Apple Siliconpnpm checkis clean andpnpm testpasses (189 files, 1907 tests)Verified on Linux with a packaged build (Electron 44, electron-builder 26.18.0):
verifyFuses.jspasses ondist/linux-unpacked/gitify, and fails on stock Electron and on a missing pathELECTRON_RUN_AS_NODE=1, where stock Electron runs it as Nodeapp.asar, loads sounds and icons overfile://, decrypts an existing profile's tokens, and fetches notificationsCI runs
verifyFuses.jsafter packaging on all three platforms.Not yet verified: launching the Windows artifact, which needs a Windows runtime.
Notes
gitify-specsstore as theharden-packaged-buildschange, so they're not part of this diff.