Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lavamoat/webpack/policy-override.json
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@
"window": true,
"self": true
}
},
"cypress>lodash": {
"globals": {
"setTimeout": true,
"clearTimeout": true
}
}
}
}
9 changes: 7 additions & 2 deletions public/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ function createWindow () {
height: 768,
icon: path.join(__dirname, iconOS),
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
// Security: no Node in the renderer; it reaches main only through the
// contextBridge API in preload.js (window.electronAPI).
nodeIntegration: false,
contextIsolation: true,
// sandbox stays off so the preload can still use Node (require/process)
// for the bridge and Sentry — enabling it is a follow-up.
sandbox: false,
preload: path.join(__dirname, 'preload.js')
}
})
Expand Down
105 changes: 104 additions & 1 deletion public/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,118 @@
*/

// This file is executed right before electron start loading the index
const { contextBridge, ipcRenderer, shell } = require('electron')
const Sentry = require('@sentry/electron')
const constants = require('./constants');
const { ipcRenderer } = require('electron')

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

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.


const VALID_SEND_CHANNELS = [
'ledger:getVersion',
'ledger:getPublicKeyData',
'ledger:checkAddress',
'ledger:sendTx',
'ledger:getSignatures',
'ledger:signToken',
'ledger:sendTokens',
'ledger:verifyTokenSignature',
'ledger:verifyManyTokenSignatures',
'ledger:resetTokenSignatures',
'app:clear_storage_success',
];

const VALID_RECEIVE_CHANNELS = [
'ledger:version',
'ledger:publicKeyData',
'ledger:address',
'ledger:txSent',
'ledger:signatures',
'ledger:tokenSignature',
'ledger:tokenDataSent',
'ledger:tokenSignatureValid',
'ledger:manyTokenSignatureValid',
'ledger:tokenSignatureReset',
'ledger:closed',
'app:clear_storage',
];

/**
* Restore Buffers that the contextBridge downgraded to Uint8Array, before the
* IPC send: the main process and ledgerjs expect Buffers, so public/ledger.js
* stays unchanged. Only plain objects and arrays are traversed; anything else
* (Date, Map, non-Uint8Array typed arrays, ...) is passed through untouched, and
* the WeakSet guards against circular payloads.
*/
function restoreBuffers(value, seen = new WeakSet()) {
if (value instanceof Uint8Array) {
return Buffer.from(value);
}
if (value === null || typeof value !== 'object') {
return value;
}
if (seen.has(value)) {
return value;
}
seen.add(value);
if (Array.isArray(value)) {
return value.map((item) => restoreBuffers(item, seen));
}
const proto = Object.getPrototypeOf(value);
if (proto !== Object.prototype && proto !== null) {
return value;
}
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, restoreBuffers(item, seen)])
);
}

// Replaces the old `window.require('electron')` access that required nodeIntegration.
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, ...args) => {
if (VALID_SEND_CHANNELS.includes(channel)) {
ipcRenderer.send(channel, ...args.map(restoreBuffers));
}
},
on: (channel, listener) => {
if (VALID_RECEIVE_CHANNELS.includes(channel)) {
// Don't forward the Electron event across the bridge; pass undefined to
// keep the legacy (event, ...args) listener signature.
ipcRenderer.on(channel, (_event, ...args) => listener(undefined, ...args));
}
},
removeAllListeners: (channel) => {
if (VALID_RECEIVE_CHANNELS.includes(channel)) {
ipcRenderer.removeAllListeners(channel);
}
},
// Only http/https may be opened externally; block file: and custom schemes,
// which could trigger host-side execution if the renderer is compromised.
openExternal: (url) => {
try {
const { protocol } = new URL(url);
if (protocol === 'http:' || protocol === 'https:') {
return shell.openExternal(url);
}
} catch (_e) { /* invalid URL */ }
return undefined;
},
// Sentry runs in the preload (@sentry/electron), outside the LavaMoat-governed
// renderer bundle, on the renderer's behalf. Empty dsn disables it (consent toggle).
sentrySetEnabled: (dsn) => Sentry.init({ dsn, release: process.env.npm_package_version }),
sentryCapture: ({ name, message, stack, extra }) => {
const error = new Error(message);
if (name) error.name = name;
if (stack) error.stack = stack;
Sentry.withScope(scope => {
Object.entries(extra || {}).forEach(([key, item]) => scope.setExtra(key, item));
Sentry.captureException(error);
});
},
});

process.once('loaded', () => {
const oldAccessDataRaw = localStorage.getItem('wallet:accessData');
if (oldAccessDataRaw) {
Expand Down
9 changes: 4 additions & 5 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,10 @@ export const MIN_JOB_ESTIMATION = 1;

let ipcRenderer = null;

if (window.require) {
// Requiring electron outside main thread must be done like that
// https://github.com/electron/electron/issues/7300
const electron = window.require('electron');
ipcRenderer = electron.ipcRenderer;
if (window.electronAPI) {
// window.electronAPI (from preload.js) mirrors the ipcRenderer methods the
// renderer uses, without requiring nodeIntegration.
ipcRenderer = window.electronAPI;
}

/**
Expand Down
11 changes: 6 additions & 5 deletions src/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import { networkUpdate, networkSettingsUpdate, networkSettingsUpdateSuccess } fr
import { NETWORK_SETTINGS } from '../constants';
import LOCAL_STORE from '../storage';

let shell = null;
if (window.require) {
shell = window.require('electron').shell;
let openExternal = null;
if (window.electronAPI) {
// Exposed by preload.js via contextBridge; wraps electron's shell.openExternal.
openExternal = window.electronAPI.openExternal;
}

const helpers = {
Expand All @@ -31,8 +32,8 @@ const helpers = {
openExternalURL(url) {
// We use electron shell to open the user external default browser
// otherwise it would open another electron window and the user wouldn't be able to copy the URL
if (shell !== null) {
shell.openExternal(url);
if (openExternal !== null) {
openExternal(url);
} else {
// In case it's running on the browser it won't have electron shell
// This should be used only when testing
Expand Down
36 changes: 24 additions & 12 deletions src/utils/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import {
SENTRY_DSN,
VERSION,
WALLET_HISTORY_COUNT,
METADATA_CONCURRENT_DOWNLOAD,
ADDRESS_MODE,
Expand All @@ -30,14 +31,12 @@ import {
import { chunk, get } from 'lodash';
import helpers from '../utils/helpers';
import LOCAL_STORE from '../storage';
// In Electron, Sentry runs in the preload (@sentry/electron) via the bridge,
// keeping it outside the LavaMoat-governed bundle. In a plain browser (dev/tests)
// there's no bridge, so fall back to the bundled browser SDK.
import * as SentryBrowser from '@sentry/browser';

let Sentry = null;
// Need to import with window.require in electron (https://github.com/electron/electron/issues/7300)
if (window.require) {
Sentry = window.require('@sentry/electron');
} else {
Sentry = require('@sentry/browser');
}
const sentryBridge = (typeof window !== 'undefined' && window.electronAPI) || null;

/**
* Key string constants for manipulating the storage
Expand Down Expand Up @@ -459,9 +458,13 @@ const wallet = {
* @inner
*/
initSentry(dsn) {
Sentry.init({
if (sentryBridge) {
sentryBridge.sentrySetEnabled(dsn);
return;
}
SentryBrowser.init({
dsn: dsn,
release: process.env.npm_package_version
release: VERSION
});
},

Expand Down Expand Up @@ -491,12 +494,21 @@ const wallet = {
* @inner
*/
sentryWithScope(error, info) {
Sentry.withScope(scope => {
Object.entries(info).forEach(
if (sentryBridge) {
sentryBridge.sentryCapture({
name: error?.name,
message: error?.message,
stack: error?.stack,
extra: info,
});
return;
}
SentryBrowser.withScope(scope => {
Object.entries(info || {}).forEach(
([key, item]) => scope.setExtra(key, item)
);
// TODO: Add storage snapshot to sentry
Sentry.captureException(error);
SentryBrowser.captureException(error);
});
},

Expand Down
Loading