Repository navigation
Conversation
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.
|
|
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. |



What
Pins Electron's fuses in
electron-builder.js, so packaged builds ship with the hardening switches closed rather than at the framework defaults, and adds a post-packaging check that fails the build when an artifact is not actually hardened.Supersedes #3423, which reaches the same outcome by flipping fuses in
scripts/afterPack.js. This route uses electron-builder's built-inelectronFusesconfiguration instead.Credit to @neemlama, who identified and evidenced this in #3423: the shipped v7.9.0 build sitting on fuse wire version 1, the fuse set applied here, and the reasoning for leaving
grantFileProtocolExtraPrivilegesat its default all come from that analysis.Why
Shipped v7.9.0 binaries are on fuse wire version 1, so
RunAsNode,EnableNodeOptionsEnvironmentVariableandEnableNodeCliInspectArgumentsall sit at their permissive defaults, andapp.asaris never validated at load.None of these are remotely exploitable, and none are a bug introduced by Gitify — they are Electron's defaults. The gap is that the packaging configuration made no deliberate choice about them, and nothing asserted the shipped artifacts were hardened.
The posture
runAsNodeenableCookieEncryptionenableNodeOptionsEnvironmentVariableenableNodeCliInspectArgumentsenableEmbeddedAsarIntegrityValidationonlyLoadAppFromAsargrantFileProtocolExtraPrivilegesis deliberately left at the Electron default: the renderer loadsindex.htmloverfile://, so removing those privileges breaks asset and sound loading.electronFusesalso setsresetAdHocDarwinSignature, which is not a fuse. Flipping invalidates the ad-hoc signature Electron ships with, and Apple Silicon refuses to launch a binary whose signature no longer validates.build.ymlpackages macOS unsigned (-c.mac.identity=null), so without it the arm64 artifact is killed on launch.Why the configuration route over an
afterPackhookelectron-builder flips fuses in
doAddElectronFuses(), deliberately placed betweensanityCheckPackage()anddoSignAfterPack(), and resolves each platform's executable itself — includingLinuxPackager.executableName, which is the lowercased package name (gitify, notGitify). The hook route needs a second copy of@electron/fuses, a hand-rolled per-platform path, and an explicitawait; a fire-and-forget call resolves the hook while the binary is still being rewritten, letting signing race the write.No dependency is added.
@electron/fusesalready resolves transitively through electron-builder, andpnpm-lock.yamlis unchanged.Verification
doAddElectronFuses()is a silent no-op when the configuration key is absent, soscripts/verifyFuses.jsreads 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 fromelectron-builder.js: deriving it from the configuration passed vacuously once the configuration was removed, which is the exact failure this guards against. Both workflows run the check after packaging and before artifact cleanup.Verified locally on a macOS universal build (Electron 44.7.0, electron-builder 26.17.0):
Info.plistcarriesElectronAsarIntegrityforResources/app.asar, injected by@electron/universalduring the architecture mergeverifyFuses.jspasses on the hardened bundle, and fails on one built with the fuse configuration removedcodesign --verifypasses and the unsigned artifact launches on Apple Siliconpnpm checkis clean, andpnpm testpasses 189 files / 1907 testsNot yet verified, and why this is a draft: the checker's Windows and Linux code paths have never been exercised (only macOS binaries have), and Windows launchability needs a Windows runtime. Both are covered by the workflow wiring but need a CI run to confirm.
Notes
gitify-specsstore as theharden-packaged-buildschange, so they are not part of this diff.