Skip to content

ci(build): flip Electron fuses in packaged builds - #3423

Open
neemlama wants to merge 3 commits into
gitify-app:mainfrom
neemlama:security/flip-electron-fuses
Open

neemlama wants to merge 3 commits into
gitify-app:mainfrom
neemlama:security/flip-electron-fuses

Conversation

@neemlama

Copy link
Copy Markdown

What

Adds Electron fuse flipping to the existing scripts/afterPack.js, so packaged builds ship with the hardening switches closed rather than at their framework defaults.

Why

I unpacked the shipped v7.9.0 Windows build and read the fuse wire out of Gitify.exe. It reports fuse wire version 1, which means @electron/fuses has never been run against the build, so every switch is sitting at its Electron default rather than a deliberate choice:

Fuse Current
run_as_node enabled
enable_node_options_env enabled
enable_node_cli_inspect_args enabled
everything else default

To be clear about what this is and is not: none of these are remotely exploitable, and none of them are a bug introduced by Gitify. They are the Electron framework defaults. The point is that the three which matter were never closed.

Concretely, on a machine where something can influence how the app is launched:

  • ELECTRON_RUN_AS_NODE=1 Gitify.exe runs the binary as a plain Node runtime, dropping the Electron sandbox.
  • NODE_OPTIONS is honoured, so a controlled launch environment can inject --require and load code at startup.
  • A launch argument can attach the Node inspector to the renderer.

How I verified the fix

I applied this exact configuration to a copy of the shipped Gitify.exe and re-read the wire with both @electron/fuses and an independent decoder:

before  {"0":49,"1":48,"2":49,"3":49,"4":48,"5":48,"6":48,"7":49,"8":49}
after   {"0":48,"1":49,"2":48,"3":48,"4":48,"5":49,"6":48,"7":49,"8":49}

As raw bytes, the patch touches 5 bytes of a 245,726,208-byte binary, all inside the 9-entry wire, and the file length is unchanged:

198870250: 49 -> 48   run_as_node
198870251: 48 -> 49   enable_cookie_encryption
198870252: 49 -> 48   enable_node_options_env
198870253: 49 -> 48   enable_node_cli_inspect_args
198870255: 48 -> 49   only_load_app_from_asar

Two deliberate omissions

EnableEmbeddedAsarIntegrityValidation is left off. It requires a matching asarIntegrity option in electron-builder.js, which is not currently set, and enabling the fuse without that prevents the app from launching. I think it is worth enabling, but it belongs in its own change where the build config moves with it.

GrantFileProtocolExtraPrivileges is left at its default, because turning it off breaks file:// handling. OnlyLoadAppFromAsar is set, which is the protection that actually matters for this change.

Why afterPack and not postinstall

Keeps @electron/fuses in devDependencies and avoids an "Electron was rebuilt" notice on every npm install. afterPack also already exists in the repo, so this is one hook covering win/mac/linux rather than three platform scripts.

Note on the API, in case it catches anyone else

@electron/fuses v2 changed shape from v1.x: FuseV2Options no longer exists (all names moved under FuseV1Options), and flipFuses now takes (executablePath, config) rather than (config, executablePath). Both are commented in the file.

Testing

node --check passes, and I confirmed the executable-path resolution returns the right location for each platform (darwin -> Contents/MacOS/<app>, win32 -> <app>.exe, default -> <app>). The hook also no-ops with a log line if the executable is not found, so a packaging change cannot silently skip the flip.

I have not run a full pnpm package:win build, so the three platform paths are verified by inspection rather than by producing all three artifacts.

Happy to split any part of this out, or to drop it entirely if the timing is not right.

Shipped v7.9.0 binaries are on fuse wire version 1, meaning @electron/fuses has
never run against the build. RunAsNode, EnableNodeOptionsEnvironmentVariable
and EnableNodeCliInspectArguments are all left at their permissive framework
defaults.

With RunAsNode enabled, ELECTRON_RUN_AS_NODE=1 <app> starts the binary as a
plain Node runtime, dropping the Electron sandbox entirely.
EnableNodeOptionsEnvironmentVariable means NODE_OPTIONS is honoured, so a
controlled launch environment can inject --require and load code at startup.
EnableNodeCliInspectArguments lets a launch argument attach the Node inspector
to the renderer process.

None of these are remotely exploitable and none need an embargo. They are the
Electron framework defaults, so this is not a bug introduced here. It is the
set of switches that were never closed.

Adds fuse flipping to the existing afterPack hook so it applies to all three
packaged platforms (win/mac/linux).

Two deliberate omissions:

- EnableEmbeddedAsarIntegrityValidation is left off. It requires the matching
  `asarIntegrity` option in electron-builder.js, and enabling the fuse without
  that prevents the app from launching. Worth a separate change.
- GrantFileProtocolExtraPrivileges is left at its default, since turning it off
  breaks file:// handling. OnlyLoadAppFromAsar is set, which is the protection
  that actually matters here.

This runs in afterPack rather than postinstall, so @electron/fuses stays a
devDependency and there is no Electron rebuild notice on every npm install.
@neemlama neemlama changed the title security: flip Electron fuses in packaged builds fix(build): flip Electron fuses in packaged builds Oct 14, 2026
@github-actions github-actions Bot added the bug Something isn't working label Oct 14, 2026
@setchy setchy added the platform:windows Related to windows build label Oct 14, 2026
Comment thread package.json Outdated
@setchy setchy added enhancement New feature or enhancement to existing functionality and removed bug Something isn't working platform:windows Related to windows build labels Oct 15, 2026
@setchy setchy changed the title fix(build): flip Electron fuses in packaged builds ci(build): flip Electron fuses in packaged builds Oct 15, 2026
@github-actions github-actions Bot added build Build, action or package manager changes and removed enhancement New feature or enhancement to existing functionality labels Oct 15, 2026
setchy and others added 2 commits October 14, 2026 21:51
`pnpm install --frozen-lockfile` failed because pnpm-lock.yaml did not know
about the new devDependency, which would have broken CI.

Also applies the formatting oxfmt wants for scripts/afterPack.js.

Verified locally with the project's own tooling:

  $ pnpm check
  pass: All 491 files are correctly formatted
  pass: Found no warnings or lint errors in 440 files
@github-actions github-actions Bot added the dependency Dependency updates label Oct 15, 2026
@neemlama

Copy link
Copy Markdown
Author

Pushing an update while this is under review, since I found a problem I should have caught before opening.

I had added @electron/fuses to package.json without updating pnpm-lock.yaml. That means pnpm install --frozen-lockfile fails:

specifiers in the lockfile don't match specifiers in package.json:
  in importers["."]:
  1 dependency was added: @electron/fuses@2.1.3

So CI would have failed on the first run. 014b3052 adds the lockfile entry and applies the formatting oxfmt wants for afterPack.js.

What I have now verified locally, rather than assumed:

$ pnpm install --frozen-lockfile
Lockfile is up to date

$ pnpm check
pass: All 491 files are correctly formatted
pass: Found no warnings or lint errors in 440 files

The pre-commit hook also runs tsc --noEmit and vp test --changed, and both pass on this branch.

On the format change specifically: oxfmt only collapsed two multi-line calls onto single lines. I re-read the file afterwards to confirm flipFuses(executablePath, config) is still in v2 argument order and all five fuse settings are unchanged.

Two caveats I want to be explicit about, since neither is covered by the checks above:

  • I have not run a full pnpm package:win / package:mac / package:linux. The per-platform executable path resolution is verified by reading the code, not by producing all three artifacts.
  • afterPack runs only at package time, so it has no unit test coverage in this repo. That is the part of this change I would most want a second reviewer's eyes on.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Build, action or package manager changes dependency Dependency updates

Development

Successfully merging this pull request may close these issues.

2 participants