Skip to content

fix: remove insecure Electron flags (nodeIntegration, contextIsolation) - #874

Open
raul-oliveira wants to merge 2 commits into
masterfrom
raul-oliveira/fix/electron-insecure-flags
Open

fix: remove insecure Electron flags (nodeIntegration, contextIsolation)#874
raul-oliveira wants to merge 2 commits into
masterfrom
raul-oliveira/fix/electron-insecure-flags

Conversation

@raul-oliveira

@raul-oliveira raul-oliveira commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Acceptance Criteria

  • Remove the insecure Electron flags from the renderer BrowserWindow: nodeIntegration: truefalse and contextIsolation: falsetrue (the warnings Electron itself raises). sandbox is kept false so the preload can still use Node to set up the bridge — enabling full sandboxing is a follow-up.
  • Replace the renderer's window.require('electron') access with a contextBridge API (window.electronAPI) exposed from preload.js, with a whitelisted IPC surface (send / on / removeAllListeners) plus openExternal.
  • Keep the Ledger hardware-wallet integration working unchanged: the USB/HID transport stays entirely in the main process (public/ledger.js) and is reached over the IPC bridge; binary payloads are restored to Buffer when crossing the bridge so public/ledger.js does not change.
  • Move the renderer's Sentry off window.require('@sentry/electron') to the preload (reached via the bridge, outside the LavaMoat-governed bundle); @sentry/browser is kept only as the browser/dev fallback.
  • Fix the Send Tokens screen crash (TypeError: ft.setTimeout is not a function): lodash reads setTimeout off the captured global object and relied on Node's global (which bypassed LavaMoat) — disabling nodeIntegration exposed the missing grant. Fixed with a lavamoat/webpack/policy-override.json grant.

Security Checklist

  • Make sure you do not include new dependencies in the project unless strictly necessary and do not include dev-dependencies as production ones. More dependencies increase the possibility of one of them being hijacked and affecting us.
    • No new dependencies added. @sentry/browser was already a production dependency (previously the browser/dev fallback).

Testing

Validated on a physical Ledger Nano S (Hathor app):

  • ✅ Connect + open app — getVersion, getPublicKeyData (xpub import)
  • ✅ Show/verify address — checkAddress
  • Send transactionsendTx + getSignatures (the binary path through the bridge); transaction broadcast successfully
  • ✅ Send Tokens screen renders correctly (after the lodash policy fix)

The Electron nodeIntegration / contextIsolation security warnings are gone from the console; only the unrelated CSP warning remains.

Summary by CodeRabbit

  • Security

    • Hardened the desktop app’s renderer environment with safer Electron settings and a controlled bridge for app access.
    • Restricted external link opening to trusted web URLs only.
  • New Features

    • Added support for improved error reporting in the desktop app.
    • Enabled the app to communicate with the main process through a more limited, secure API.
  • Bug Fixes

    • Improved compatibility for data passed between app processes.
    • Updated desktop external-link handling to work reliably through the new bridge.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dabfd894-0bd6-443c-8c37-0290fd01032d

📥 Commits

Reviewing files that changed from the base of the PR and between 67e7dc0 and ec185a0.

📒 Files selected for processing (6)
  • lavamoat/webpack/policy-override.json
  • public/electron.js
  • public/preload.js
  • src/constants.js
  • src/utils/helpers.js
  • src/utils/wallet.js
✅ Files skipped from review due to trivial changes (1)
  • lavamoat/webpack/policy-override.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/utils/helpers.js
  • public/electron.js
  • src/constants.js
  • public/preload.js
  • src/utils/wallet.js

📝 Walkthrough

Walkthrough

Electron renderer access is moved behind a preload-exposed window.electronAPI bridge with IPC allowlists, buffer restoration, external URL restrictions, and Sentry helpers. Renderer code is updated to use the bridge, and a LavaMoat policy entry is added for cypress>lodash.

Changes

Electron contextBridge migration

Layer / File(s) Summary
BrowserWindow security and preload bridge foundation
public/electron.js, public/preload.js
BrowserWindow switches to nodeIntegration: false and contextIsolation: true. preload.js adds Sentry setup, IPC allowlists, restoreBuffers(), and contextBridge.exposeInMainWorld('electronAPI', ...) with guarded IPC, external link, and Sentry methods.
Renderer-side migration to window.electronAPI
src/constants.js, src/utils/helpers.js, src/utils/wallet.js
constants.js reads IPC from window.electronAPI. helpers.js uses window.electronAPI.openExternal. wallet.js uses @sentry/browser, bridges Sentry enable/capture calls when available, and uses VERSION for release.
LavaMoat override for cypress lodash globals
lavamoat/webpack/policy-override.json
Adds a resources["cypress>lodash"] entry allowing setTimeout and clearTimeout.

Sequence Diagram

sequenceDiagram
  participant Renderer as Renderer
  participant electronAPI as window.electronAPI
  participant preload as preload.js
  participant Main as Electron Main Process

  Renderer->>electronAPI: send / openExternal / Sentry call
  electronAPI->>preload: validate and normalize input
  preload->>Main: ipcRenderer.send(...) or shell.openExternal(...)
  Main-->>preload: IPC event or external action result
  preload-->>Renderer: bridged listener or return value
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through the bridge with a cheerful thump,
No require in sight, just safe IPC jump!
Buffers restored and channels held tight,
Sentry and links now take the right route at night.
Hooray for the bridge, shiny and bright ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: hardening Electron by removing insecure nodeIntegration/contextIsolation settings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch raul-oliveira/fix/electron-insecure-flags

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
public/preload.js (1)

57-70: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

restoreBuffers only restores Uint8Array; other typed arrays and special objects are silently mangled.

value instanceof Uint8Array correctly catches Buffer and Uint8Array, but any other TypedArray (e.g. Int8Array, Float32Array), Date, Map, or Set falls into the generic object branch, where Object.entries(...) reconstructs an empty/plain object and drops the original type. There is also no cycle guard, so a circular payload would recurse infinitely. This is currently safe for the Buffer-only Ledger payloads, but it’s a latent trap if payload shapes change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/preload.js` around lines 57 - 70, The restoreBuffers function
currently only handles Uint8Array correctly, causing other TypedArray instances
(Int8Array, Float32Array, etc.), Date, Map, and Set objects to be silently
converted to plain objects when they fall into the generic object branch.
Additionally, there is no protection against circular references which could
cause infinite recursion. To fix this, extend the restoreBuffers function to
explicitly check for and properly restore other TypedArray types by converting
them using Buffer.from or their respective constructors, add specific checks for
Date, Map, and Set to preserve their types, and implement cycle detection using
a WeakSet parameter to track visited objects and prevent infinite recursion on
circular payloads.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@public/preload.js`:
- Line 92: The openExternal function exposed at line 92 lacks protocol
validation, creating a security vulnerability where compromised renderers could
execute arbitrary file or custom scheme handlers. Add protocol validation to the
openExternal function to restrict URLs to http: and https: protocols only,
rejecting or throwing an error for any other protocols like file: or custom
schemes before calling shell.openExternal.
- Around line 13-16: The Sentry.init() call at the preload stage initializes
with the live DSN before user consent is obtained, which violates privacy
requirements. Additionally, process.env.npm_package_version is undefined in
packaged Electron builds. Modify the Sentry.init() call to initialize with an
empty string for the dsn parameter instead of constants.SENTRY_DSN, and replace
process.env.npm_package_version with constants.VERSION. The real DSN should be
configured later in the renderer process after the user consent check is
performed via the sentrySetEnabled() mechanism.

---

Nitpick comments:
In `@public/preload.js`:
- Around line 57-70: The restoreBuffers function currently only handles
Uint8Array correctly, causing other TypedArray instances (Int8Array,
Float32Array, etc.), Date, Map, and Set objects to be silently converted to
plain objects when they fall into the generic object branch. Additionally, there
is no protection against circular references which could cause infinite
recursion. To fix this, extend the restoreBuffers function to explicitly check
for and properly restore other TypedArray types by converting them using
Buffer.from or their respective constructors, add specific checks for Date, Map,
and Set to preserve their types, and implement cycle detection using a WeakSet
parameter to track visited objects and prevent infinite recursion on circular
payloads.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b0b5cb3-cd7e-4ce7-8d2f-959307bf628d

📥 Commits

Reviewing files that changed from the base of the PR and between 23f902a and 67e7dc0.

📒 Files selected for processing (6)
  • lavamoat/webpack/policy-override.json
  • public/electron.js
  • public/preload.js
  • src/constants.js
  • src/utils/helpers.js
  • src/utils/wallet.js

Comment thread public/preload.js
Comment on lines 13 to 16
Sentry.init({
dsn: constants.SENTRY_DSN,
release: process.env.npm_package_version
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '\b(isSentryAllowed|updateSentryState|initSentry|sentrySetEnabled)\b' src public
rg -nP 'SENTRY_DSN|export const VERSION' src/constants.js
rg -nP 'npm_package_version' public src

Repository: HathorNetwork/hathor-wallet

Length of output: 1617


🏁 Script executed:

cat -n public/preload.js | head -100

Repository: HathorNetwork/hathor-wallet

Length of output: 4256


Sentry initializes with the live DSN at preload startup, before any user consent gate.

The top-level Sentry.init at lines 13–16 runs immediately when preload loads, capturing the real constants.SENTRY_DSN. While the renderer can later disable it via sentrySetEnabled(), this only affects future errors. Any errors during preload execution—including early renderer initialization before consent is checked—are captured without user consent.

Additionally, process.env.npm_package_version is only populated during npm script execution. In packaged Electron builds, it's undefined. Use constants.VERSION instead, which is already imported and properly sourced from package.json.

Initialize with the DSN disabled ('') and let the renderer enable it only after consent:

Suggested change
-Sentry.init({
-  dsn: constants.SENTRY_DSN,
-  release: process.env.npm_package_version
-})
+// Start disabled; the renderer enables it via sentrySetEnabled(dsn) only
+// after the user consents (see wallet.updateSentryState).
+Sentry.init({
+  dsn: '',
+  release: constants.VERSION
+})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@public/preload.js` around lines 13 - 16, The Sentry.init() call at the
preload stage initializes with the live DSN before user consent is obtained,
which violates privacy requirements. Additionally,
process.env.npm_package_version is undefined in packaged Electron builds. Modify
the Sentry.init() call to initialize with an empty string for the dsn parameter
instead of constants.SENTRY_DSN, and replace process.env.npm_package_version
with constants.VERSION. The real DSN should be configured later in the renderer
process after the user consent check is performed via the sentrySetEnabled()
mechanism.

Comment thread public/preload.js Outdated
@raul-oliveira
raul-oliveira force-pushed the raul-oliveira/fix/electron-insecure-flags branch from c432d6c to 9292f39 Compare June 24, 2026 01:27
@raul-oliveira raul-oliveira self-assigned this Jun 24, 2026
@raul-oliveira raul-oliveira moved this from Todo to In Progress (WIP) in Hathor Network Jun 24, 2026
The renderer ran with nodeIntegration enabled and contextIsolation
disabled so it could reach the main process via window.require. Replace
that with a contextBridge API exposed from preload.js, which lets us turn
the two insecure flags off.

- electron.js: nodeIntegration:false, contextIsolation:true (sandbox kept
  false so the preload can still use Node to set up the bridge)
- preload.js: expose `window.electronAPI` with a whitelisted IPC surface
  (send/on/removeAllListeners), openExternal, and Sentry. Buffers are
  restored when crossing the bridge so the Ledger main-process code in
  public/ledger.js stays unchanged
- constants/helpers/wallet: use window.electronAPI instead of window.require
- Sentry now runs in the preload (@sentry/electron, outside LavaMoat) and
  is reached through the bridge; @sentry/browser is kept only as the
  browser/dev fallback
- lavamoat policy-override: grant setTimeout/clearTimeout to lodash. Lodash
  reads setTimeout off the captured global object; with nodeIntegration on
  it used Node's global (which bypassed LavaMoat), and disabling it exposed
  the missing grant

Validated on a physical Ledger Nano S: getVersion, getPublicKeyData,
checkAddress, and a full sendTx + getSignatures flow (transaction sent
successfully) all work through the bridge.
@raul-oliveira
raul-oliveira force-pushed the raul-oliveira/fix/electron-insecure-flags branch from 9292f39 to ec185a0 Compare June 24, 2026 13:50
@raul-oliveira raul-oliveira moved this from In Progress (WIP) to In Progress (Done) in Hathor Network Jul 3, 2026

@pedroferreira1 pedroferreira1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by claudin:

P1 — send() throws for every non-Buffer payload; Ledger send hangs forever

public/preload.js:81 — ipcRenderer.send(channel, ...args.map(restoreBuffers)). Array.prototype.map passes (element, index, array), so the array index lands in restoreBuffers's seen parameter, replacing the WeakSet default. Any top-level argument that's a plain object or array hits seen.has(value) → TypeError: seen.has is not a function. I ran the exact function from the diff: ledger:getSignatures (number array, src/utils/ledger.js:184), ledger:sendTokens and ledger:verifyManyTokenSignatures (token-map objects) all throw; only Buffer/primitive/no-arg channels survive.

User-visible impact, traced through the screens:
- HTR send: sendTx (a single Buffer) crosses fine, the Ledger prompts, the user approves on device — then handleTxSent calls getSignatures (src/screens/SendTokens.js:170), which is async and called without await/.catch (SendTokens.js:293), so the TypeError becomes a swallowed rejection and the app hangs forever on the footer-less, data-backdrop="static" "Validate outputs on Ledger" modal. Restart required; transaction never broadcast.
- Custom-token send: ledger.sendTokens throws synchronously before the flow starts → generic "Unhandled error" modal.

This contradicts the PR's "getSignatures ✅ transaction broadcast successfully" testing claim — no code path in commit ec185a0 can complete that flow, so the device testing must predate the seen parameter. Ask for a re-test on hardware after the fix.

The right fix is to delete restoreBuffers entirely, not repair it. The function can't achieve its stated purpose: ipcRenderer.send re-serializes with the Structured Clone Algorithm (Electron 27 docs), so Buffers created in the preload arrive in the main process as Uint8Array anyway — exactly what main received before this PR. Verifiers confirmed public/ledger.js is already Uint8Array-safe (data.length/data.slice at ledger.js:339-340; @ledgerhq/hw-transport's Buffer.concat accepts Uint8Array). ipcRenderer.send(channel, ...args) is the whole fix. While in there, make the SendTokens.js:293 wrapper .catch → handleSendError so a future rejection surfaces instead of hanging that modal.

P1 — Sentry consent is broken both ways in Electron

Two sides of the same migration, both verified against the pinned @sentry/electron 3.0.7 sources:

1. Opt-out is a silent no-op. sentrySetEnabled: (dsn) => Sentry.init({ dsn, ... }) (preload.js:109) relies on re-init, but the renderer SDK has a one-shot guard (window.__SENTRY__RENDERER_INIT__ in renderer/sdk.js) — after the unconditional startup init with the real DSN (preload.js:13), every later Sentry.init returns early. So updateSentryState → initSentry('') never disables anything; users who decline error reporting keep an active SDK whose events forward via IPC to the main process (also unconditionally init'd, electron.js:17). The "Empty dsn disables it" comment codifies a contract the SDK doesn't honor. (Partially pre-existing at base, but this PR makes sentrySetEnabled the only Electron path, so this is the place to fix it.)
2. Opt-in users lose all reporting. With contextIsolation: true, the preload SDK's global handlers live in the isolated world and never see main-world uncaught errors (at base, the shared window meant auto-capture worked). The browser SDK is never initialized in Electron (wallet.js:461 returns early via the bridge), and ModalUnhandledError.js:87 shows the manual "Send error report" button only when !isSentryAllowed() — so consented users have no path at all for app errors to reach Sentry. This is a real regression introduced by the contextIsolation flip.

Suggested shape: make the bridge API a boolean (sentrySetEnabled(enabled)) with the DSN kept inside the preload (also removes the renderer-supplied-DSN surface); gate captures by flipping the live client (getClient().getOptions().enabled = enabled — checked before the EventToMain processor runs; beforeSend would be too late), ideally starting disabled until consent is read; and capture main-world errors explicitly (e.g. ErrorWrapper's handlers → sentryCapture when consented). Note the related pre-existing bug that becomes load-bearing: ErrorWrapper.js:46 does setError(error) instead of setError(newError) and never passes the error to the modal, so the one surviving report path sends new Error(undefined) with no stack — worth fixing in passing.

P2 � LavaMoat grant keyed to cypress>lodash is a production time bomb

policy-override.json:324 keys the Send-Tokens crash fix to "cypress>lodash". lodash is a phantom dependency � imported by ~36 src files but declared nowhere in package.json; @lavamoat/aa canonicalizes it through cypress (a devDependency) only because it prefers the shortest direct-dep name. The policy regenerates on every production build, so if cypress is removed/upgraded or hoisting shifts, the key silently stops matching (no build error, only a verbosity-gated debug log), the setTimeout grant vanishes � the generated policy can only detect define for lodash � and the ft.setTimeout is not a function crash returns in packaged builds only, invisible to dev/e2e. Fix: add lodash@4.17.21 to dependencies, re-key the override as "lodash", regenerate policy.json.

Minor

- Release tag: both Sentry.init calls in preload use process.env.npm_package_version, which is undefined in packaged builds. Use constants.versionNumber (already required; matches electron.js). Note for the CodeRabbit thread: its suggested constants.VERSION does not exist in public/constants.js, and its "init with dsn: '' then enable later" patch would break enabling due to the same one-shot init guard � so don't apply it as written.

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

Labels

None yet

Projects

Status: In Progress (Done)

Development

Successfully merging this pull request may close these issues.

2 participants