Files
safetynet/docs/electron-security-review.md
T

123 lines
15 KiB
Markdown
Raw Normal View History

# Electron security review — 2026-10-13
This is a read-only review of the current source and unpacked Windows executable. It is not a penetration test or a claim of complete Chromium DLP coverage.
Later entries record implementation and repeated native verification following
this initial review. For current WebTransport per-attempt auditing and its
coverage limits, use [native transport observability](native-transport-observability.md).
Historical missing-event findings below have been superseded by that work;
the separate native WebRTC engine crash remains an open acceptance gate.
## Official references
Electron recommends isolated, sandboxed remote renderers, constrained IPC and navigation, current dependencies, and consideration of custom protocols and packaging fuses. [Security](https://www.electronjs.org/docs/latest/tutorial/security).
A bridge should expose specific actions, rather than raw IPC dispatchers. [Context isolation](https://www.electronjs.org/docs/latest/tutorial/context-isolation), [Preload scripts](https://www.electronjs.org/docs/latest/tutorial/tutorial-preload), [IPC](https://www.electronjs.org/docs/latest/tutorial/ipc).
The subframe option is documented as experimental; sandboxing disables the renderer Node engine. [WebPreferences](https://www.electronjs.org/docs/latest/api/structures/web-preferences).
Fuses can disable alternate startup behavior and enforce ASAR loading/integrity; cookie encryption is another available control. [Fuses](https://www.electronjs.org/docs/latest/tutorial/fuses).
## Implemented boundaries inspected
| Boundary | Current evidence | Assessment |
| --- | --- | --- |
| Remote renderer | main.cjs creates WebContentsView with nodeIntegration:false, contextIsolation:true, sandbox:true, webSecurity:true, allowRunningInsecureContent:false; app.enableSandbox() runs before renderer creation | Correct explicit baseline |
| Website preload | site-preload.cjs asserts sandboxed/contextIsolated, exports no bridge; isolated fixed guards only | No raw IPC exposed to page |
| Trusted UI bridge | preload.cjs exposes fixed channel methods; onState strips event argument and supplies unsubscribe | Matches constrained bridge pattern; trusted chrome still has powerful actions |
| Privileged IPC sender | main.cjs ipc helper requires exact window WebContents, main frame and packaged UI URL | Remote and child frames excluded from trusted UI operations |
| Website reporting IPC | visual-check and page-content bind managed tab and main frame; visual reports validate bounded token/revision/URL | Separate limited channels, not generic dispatch |
| Permissions and certificates | browserSession request/check handlers deny; certificate-error callback denies | Default deny |
| Navigation/windows | trusted chrome cannot navigate/open children; managed sites checked by destination policy and agent authority, popups routed to guarded native tabs | Policy-aware handling |
| UI CSP | index.html self scripts/styles, connect-src none, object/base/form restrictions | Restrictive packaged UI CSP |
| Startup | startup-security.cjs rejects sandbox/security/isolation bypass and external debugger switches; tests/startup-security.test.mjs | Useful after-entry guard, cannot replace binary fuses |
## Experimental subframe setting
nodeIntegrationInSubFrames:true currently ensures the security preload reaches iframes. It is not evidence that page Node access is enabled: scripts/electron-smoke.cjs explicitly checks srcdoc child require/process/safetynet are undefined and sandbox remains true. scripts/native-tabs-smoke.cjs checks require and bridge absence for active, inactive and replacement tabs. site-preload.cjs additionally fails closed if isolation or sandbox changes.
Do not remove this setting solely because its name contains Node. A replacement must retain srcdoc, initial about:blank, cross-origin/OOPIF clipboard protection and the real native negative tests. The setting remains experimental and increases maintenance risk; test it on every Electron upgrade. Current assertions are not a proof against a framework sandbox vulnerability.
## Historical baseline findings
The following findings describe the initial artifact. Later package/startup
follow-ups below supersede the fuse and launch findings; the custom UI scheme,
native RTC stability and production distribution gates remain open.
1. **Historical binary fuse gap, superseded by rebuilt readback below.** An earlier direct read of dist/win-unpacked/SafetyNet.exe found V1 fuse wire 101100011. That older executable enabled RunAsNode, NODE_OPTIONS and CLI inspector and disabled cookie encryption, ASAR integrity and ASAR-only loading. This is historical evidence, not the current fuse status. Final runtime negative probes remain necessary.
2. **Trusted UI uses file://.** main.cjs builds uiUrl with pathToFileURL and calls loadFile. A dedicated standard secure custom scheme with a strict packaged asset allowlist would reduce file-origin privilege. Migration must update exact trusted-sender URL checks and every native smoke harness; do not enable generic file-system routing or bypassCSP.
3. **Subframe test breadth.** The inspected native assertion explicitly proves a same-origin srcdoc frame. Extend capability assertions to real cross-origin/OOPIF, initial blank and nested frames alongside existing clipboard bypass tests. No broad assertion about all frame configurations is justified by one fixture.
4. **Production distribution.** Source pinning and verification do not provide code signing, protected install ACLs or signed updates. External production trust requirements remain open.
## Proposed packaging patch and compatibility
After electron-builder finishes creating win-unpacked, use an explicitly pinned @electron/fuses dependency to flip RunAsNode=false, EnableNodeOptionsEnvironmentVariable=false, EnableNodeCliInspectArguments=false, EnableCookieEncryption=true, OnlyLoadAppFromAsar=true and EnableEmbeddedAsarIntegrityValidation=true on the produced executable. Confirm builder embedded the matching ASAR integrity metadata before enabling integrity; otherwise packaging must fail rather than weaken the setting. Record/read back all fuse values in verify-package.mjs.
Current verify-package.mjs launches SafetyNet.exe with --agent-action and --agent-connection through normal main.cjs and app.whenReady(), not ELECTRON_RUN_AS_NODE. Inspector/inference helpers are external Python/native binaries. No child_process.fork dependency was found in inspected browser/package paths. Thus disabling Node-mode and inspector startup is compatible with the documented current architecture, but only a rebuilt artifact and real packaged verification can prove it.
Add finite negative launch checks for RUN_AS_NODE, NODE_OPTIONS preload injection and inspector flags, plus an isolated copied-ASAR tamper test. A JS source hash comparison alone does not verify runtime archive integrity enforcement. Cookie encryption changes require a migration/reopen fixture for persistent managed sessions.
## Implementation follow-up (verification pending)
scripts/package.mjs now supplies all six hardened fuse settings through electron-builder's supported electronFuses configuration. This runs against the copied executable before signing. scripts/package-fuses.mjs validates the resulting fuse wire and parses the Windows PE INTEGRITY/ELECTRONASAR resource against the exact archive header hash. The installed Electron distribution is not modified.
Two focused unit checks passed. scripts/verify-package-fuses.mjs supplies finite actual-process negative checks and a copied archive tamper fixture; it must pass on the rebuilt artifact before this gap is considered closed. The previously measured executable remains unhardened until packaging runs. The custom-scheme gap remains open.
## Rebuilt artifact verification
The coordinated Windows build completed with all six hardened fuses and a matching embedded ASAR header hash. The standard packaged scanner/runtime/agent verifier passed. Actual negative launches prove RUN_AS_NODE cannot change the normal application entry point and a NODE_OPTIONS require injection does not execute.
The earlier CLI-inspector negative launch exposed an operational defect: Electron's default uncaught-exception handler opened an error box without terminating. The verifier correctly failed on this timeout. Current main.cjs catches startup rejection, emits a fixed message and calls process.exit(1). Final rebuilt negative launch and copied-ASAR tamper probes remain running/pending; absence of an inspector listener alone does not prove terminal behavior.
## Independent final source review
A fresh `npm run lint` passed with zero warnings allowed. `artifacts/mvp-latest-integrated-suite.log` records 748 passed tests and zero failures. These are bounded scenario evidence, not proof of every Chromium transport path. This review changed documentation only.
Current source proves explicit global sandboxing, context isolation and disabled Node integration for remote views; fixed UI bridge methods without raw IPC/event exposure; exact trusted WebContents/main-frame/URL sender checks; denied remote permissions; constrained navigation and popup handling. These match the primary [Security](https://www.electronjs.org/docs/latest/tutorial/security), [Context Isolation](https://www.electronjs.org/docs/latest/tutorial/context-isolation) and [IPC](https://www.electronjs.org/docs/latest/tutorial/ipc) guidance. The experimental subframe option and file-origin UI retain the limitations stated above.
Current rebuilt fuse readback was independently verified with `verifyPackagedFuses`: RunAsNode=false, CookieEncryption=true, NodeOptions=false, NodeCliInspect=false, EmbeddedAsarIntegrity=true and OnlyLoadAppFromAsar=true. The Windows embedded archive-header hash matched. This supersedes the historical fuse wire; final negative launch/tamper probes on the final source-matched package remain pending.
`artifacts/webtransport-native-proof.json` establishes actual page and dedicated-worker delivery to a reachable pinned local HTTP/3 server in the baseline: 132 bytes, including 66 worker bytes, with zero webRequest callbacks. Neither tested disable flag stopped transmission. The authenticated production relay module blocked all receiver packets/bytes while ordinary HTTP remained usable. Main now installs the non-DIRECT relay before tab creation and restores it on policy application. This proves the standalone Session/module boundary; a full enrolled main.cjs page-and-worker receiver test across policy application remains necessary. Chromium's [dedicated WebTransport client](https://raw.githubusercontent.com/chromium/chromium/main/net/quic/dedicated_web_transport_http3_client.cc) rejects a resolved non-DIRECT proxy before socket creation. Revalidate after every Electron upgrade; no claim is made about future HTTP/2 WebTransport implementations.
**Critical unresolved stability gate:** the initial-about:blank native RTC fixture records exit code 3. It must not be presented as completed or stable protection. Source guards and passing unit tests do not close this crash or prove every nested/cross-origin context.
**Startup trust follow-up:** the reviewed denylist accepted certificate-ignore switches and user feature overrides. Root now rejects certificate-ignore/SPKI-ignore, insecure localhost/origin and reduced-security switches. Original application argv rejects user enable/disable-feature and experimental Blink overrides; Electron's internal default disable-features list is preserved. Seven focused startup tests and the updated 749-test suite passed. Actual packaged negative launches for these additional switches are pending a fresh build. The native network gate is `ConnectionAllowlists` (plural), as shown in Chromium's [navigation throttle](https://raw.githubusercontent.com/chromium/chromium/main/content/browser/renderer_host/network_restrictions_navigation_throttle.cc); a singular guessed feature name is not enforcement evidence.
Inspection/inference runs outside the main process, but synchronous audit encryption, writes/fsync and some startup reads remain on it. Traces provide timings for exercised scenarios; no universal responsiveness claim is proven. See Electron's [Performance guidance](https://www.electronjs.org/docs/latest/tutorial/performance).
### Final package follow-up
The subsequent shared clipboard/transport owner implementation also passed
actual Windows package/runtime and fuse negative verifiers. Its enrolled native
fixture now proves six Created and six Closed records, zero Established records,
and twelve correlated lifecycle/audit traces. Clipboard/restart and actual local
Gemma regressions passed with matching source hashes. This supersedes the
transport audit-gap statements below for the specifically tested Window and
dedicated Worker cases; it is not universal transport or payload coverage.
See [native transport observation](native-transport-observability.md).
The new frozen-source Windows build and both package verifiers passed after
the startup correction. Actual baseline, RUN_AS_NODE, NODE_OPTIONS, CLI
inspector, eleven certificate/feature override launches and copied-ASAR tampering
were exercised. All unsafe startup cases terminated with the fixed rejection;
tampering failed before application entry. The original archive remained
unchanged. `artifacts/package-fuses-proof.json` records
`unsafeStartupOverridesRejected=true`. This supersedes the pending package
statements above. Actual executable status remains `NotSigned`.
### Minimal remaining enrolled WebTransport harness
Implementation follow-up: the enrolled main.cjs harness now passed, recorded in
`artifacts/fullmain-webtransport-native-proof.json`. The unguarded Window/Worker
baseline delivered 132 bytes. Actual enrollment, reviewed DIRECT and fetched
HTTPS PAC→DIRECT policies each yielded zero managed receiver packets and bytes,
while three ordinary HTTPS POSTs succeeded. Actual correlated operational
traces exported with zero current/lifetime failures. Exact source hashes are
included; the later change only normalizes Windows startup arguments. No
webRequest callback or individual WebTransport audit event was invented. This
supersedes the proposed-only status below; per-attempt security auditing remains
open.
Extend the Windows DLP smoke parent to start the existing pinned local HTTP/3 receiver and pass its ephemeral URL and certificate hash to the native fixture. First prove baseline marker delivery. After actual S Browser enrollment, attempt a datagram and stream from the managed page and a real dedicated Worker. Assert receiver connection/byte deltas, then repeat after signed policy application, including enterprise DIRECT/PAC configurations. Require ordinary HTTP/HTTPS and benign prompt delivery to keep working. Record source hashes and fixed facts without credentials or prompt contents. This extension is proposed; it is not implemented or passed here.
Upstream handler evidence: [Electron browser initialization](https://github.com/electron/electron/blob/main/lib/browser/init.ts). The verifier now records a fixed stage and bounded diagnostic output before reporting failure, and waits for child termination before removing its own validated temporary directory.