Skip to content

Electron security: disable nodeIntegration, enable contextIsolation with contextBridge preloads#11

Closed
Copilot wants to merge 7 commits into
masterfrom
copilot/sub-pr-5-one-more-time
Closed

Electron security: disable nodeIntegration, enable contextIsolation with contextBridge preloads#11
Copilot wants to merge 7 commits into
masterfrom
copilot/sub-pr-5-one-more-time

Conversation

Copilot AI commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

This PR hardens Electron renderer security by disabling nodeIntegration and enabling contextIsolation with contextBridge preload scripts, and resolves all merge conflicts with the feature/add-tests base branch.

Changes Made

Security hardening

  • Main window: nodeIntegration: false, contextIsolation: true, preload via app/preload.js
  • Settings window: nodeIntegration: false, contextIsolation: true, preload via app/js/settings-preload.js
  • About window: nodeIntegration: false, contextIsolation: true, preload via app/js/about-preload.js
  • Replaced inline require() calls in HTML files with static <script src> tags
  • Moved file I/O (open/save/export) and Dropbox OAuth callback server from renderer to main process via IPC

Preload / contextBridge API

  • app/preload.js: exposes electronAPI, storeAPI, i18n, and version APIs for the main window
  • app/js/about-preload.js: exposes unified aboutAPI (i18n, version, onProgress)
  • app/js/settings-preload.js: exposes unified settingsAPI with nested store (get/set/useDropbox/storeCloudData/deleteHistory), log, ipcSend, onReadyForExportData, and closeWindow

Bug fixes (from code review)

  • deleteHistory() exposed in settingsAPI.store so Settings.js can clear history without throwing
  • Fixed implicit global argconst arg in for...of loop (main.js)
  • Fixed if (filename && content)if (filename && content != null) to allow saving empty-string content (main.js)
  • Fixed open-dialog cancel: if (files)if (files && files.length > 0) to prevent readFileSync(undefined) (main.js)
  • Closed Dropbox auth HTTP server after token is received to prevent port 17234 leak (main.js)
  • Defaulted storeAPI.get("sender") to [] to prevent TypeError on fresh install (LetterStructure.js)
  • Fixed about-preload.js version path: ../../package.json../package.json

Merge with feature/add-tests

  • Adopted unified settingsAPI/aboutAPI contextBridge structure from base branch
  • Added app/js/letterStructureHelpers.js with extracted colorize and buildAddress pure utility functions
  • Updated LetterStructure.js to use buildAddress from helpers
  • Export-all-dialog uses pendingExportPath two-step pattern (main stores path → notifies renderer → renderer sends data → main writes file)

Testing

  • ✅ All 48 tests passing (including new test/letterStructure.test.js for helper functions)
  • npm ci succeeds
  • ✅ CodeQL: no alerts
  • ✅ No merge conflict markers remaining

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…on with preload scripts

Co-authored-by: Samuel3 <15921086+Samuel3@users.noreply.github.com>
@Samuel3 Samuel3 requested a review from Copilot March 2, 2026 19:43
Copilot AI changed the title [WIP] Address feedback on feature add tests Electron security: disable nodeIntegration, enable contextIsolation with contextBridge preloads Mar 2, 2026
@Samuel3

Samuel3 commented Mar 2, 2026

Copy link
Copy Markdown
Owner

@copilot please fix merge conflict and broken build

Copilot AI 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.

Pull request overview

This PR continues the security hardening/refactor of the Electron app by disabling renderer Node.js access (nodeIntegration off + contextIsolation on) and moving privileged operations behind preload/contextBridge + IPC.

Changes:

  • Replaced inline require() usage in HTML with static <script src> tags.
  • Added preload scripts to expose i18n/store/version and window-specific IPC APIs via contextBridge.
  • Moved file I/O (open/save/export) and Dropbox OAuth callback handling into the main process via IPC.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
app/sites/index.html Switches from inline require() to script tags for jQuery/UI and main renderer script
app/sites/about.html Switches from inline require() to script tags for jQuery/UI and About renderer script
app/sites/settings.html Switches from inline require() to script tags for jQuery/UI and Settings renderer script
app/preload.js New main-window preload exposing i18n, version, store, and IPC bridges
app/main.js Hardens main window webPreferences; moves open/save/export and Dropbox auth callback server into main process
app/js/settings-preload.js New settings-window preload exposing i18n, store, and settings IPC
app/js/about-preload.js New about-window preload exposing i18n, version, and progress IPC
app/js/Settings.js Updates settings renderer to use bridged APIs instead of remote/ipcRenderer/filesystem
app/js/MenuTemplate.js Updates settings/about window creation to use secure webPreferences + preload
app/js/LetterStructure.js Updates main renderer to use bridged APIs for store, IPC, file open/save/export, Dropbox auth
app/js/About.js Updates about renderer to use bridged APIs for version + progress updates
Comments suppressed due to low confidence (1)

app/js/LetterStructure.js:16

  • Several for ... of loops here use undeclared iteration variables (addressEntry, addressEntryField) which creates implicit globals in the renderer context. Declare them with const/let to avoid accidental global mutation and hard-to-debug side effects.
        var addressData = window.storeAPI.get("address")
        var _body = $(document).find("tbody")
        _body.empty()
        if (typeof addressData !== "undefined") {
            for (addressEntry of addressData) {
                var tr = $("<tr>");
                for (addressEntryField of addressEntry) {
                    createTableData(tr, addressEntryField);
                }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/js/settings-preload.js Outdated
Comment on lines +38 to +65
contextBridge.exposeInMainWorld('storeAPI', {
get: function (key) {
return dataStore.get(key);
},
set: function (key, value) {
dataStore.set(key, value);
},
useDropbox: function () {
var settings = dataStore.get('settings') || {};
return (typeof settings.useDropbox === 'undefined') ? false : (settings.useDropbox === true);
},
storeCloudData: function () {
var settings = dataStore.get('settings') || {};
var dropboxKey = settings.dropboxKey;
if (typeof dropboxKey !== 'undefined' && dropboxKey && DropboxClass) {
try {
var box = new DropboxClass();
box.setClientId('av5lekkcrbbfbgn');
box.setAccessToken(dropboxKey);
box.filesUpload({ path: '/config.json', contents: JSON.stringify(dataStore.data), mode: 'overwrite' })
.then(function (response) { log.info(response); })
.catch(function (err) { log.error(err); });
} catch (e) {
log.error(e);
}
}
}
});

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

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

Settings.js calls window.storeAPI.deleteHistory(), but the storeAPI exposed by settings-preload.js doesn’t define deleteHistory. This will throw at runtime when the user confirms deleting history. Expose deleteHistory in settings-preload.js (or update Settings to use an API that exists).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot please fix

Comment thread app/js/Settings.js Outdated
store.deleteHistory()
console.warn("Deleting all letters.");
showMessage(i18n("message.historydeleted"));
window.storeAPI.deleteHistory()

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

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

This click handler calls window.storeAPI.deleteHistory(), but settings-preload.js’s storeAPI currently exposes only get/set/useDropbox/storeCloudData. As-is, deleting history in the settings window will raise deleteHistory is not a function. Align the preload API and renderer usage (either add deleteHistory to the preload bridge or change the call).

Suggested change
window.storeAPI.deleteHistory()
window.storeAPI.set("history", [])

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot please implement feature to deleteHistory()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Implemented in commit 64ae365 via window.settingsAPI.store.deleteHistory() in Settings.js, backed by deleteHistory: () => store.deleteHistory() in settings-preload.js. This correctly clears the history array through the Store class.

Comment thread app/main.js Outdated
@@ -72,7 +79,12 @@ function createWindow () {
mainWindow.webContents.on('did-finish-load', function () {
for (arg of process.argv) {

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

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

arg is used without let/const in this for...of, which creates/uses an implicit global variable in the main process. Declare it (e.g., for (const arg of process.argv)) to avoid leaking globals and potential side effects.

Suggested change
for (arg of process.argv) {
for (const arg of process.argv) {

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot please fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 0f3a014 — changed for (arg of process.argv) to for (const arg of process.argv).

Comment thread app/main.js
Comment on lines 155 to +160
dialog.showSaveDialog(options, (filename) => {
mainWindow.send('saved-file', filename)
if (filename && content) {
fs.writeFileSync(filename, content);
} else if (filename) {
mainWindow.webContents.send('save-requested');
}

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

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

if (filename && content) treats an empty string as “no content” and will trigger the save-requested fallback even when the renderer intentionally sends "". Also, there’s at least one caller (MenuTemplate’s File→Save) that triggers save without content, and the current fallback re-opens a new save dialog without preserving the chosen filename. Consider changing this flow to (1) treat content === "" as valid, and (2) either require content for save-dialog or send the chosen filename back so the renderer can provide content for that specific path.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot please fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 0f3a014 — changed if (filename && content) to if (filename && content != null) so empty-string content is treated as valid and written to disk.

Comment thread app/main.js
Comment on lines +343 to +345
});
}
}).listen(17234);

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

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

The Dropbox auth HTTP server is never closed after a token is received. This leaves a listener bound to port 17234 for the rest of the app session and can break subsequent logins or conflict with other processes. Close dropboxAuthServer (and set it back to null) after successfully parsing/sending the token (and possibly also on error/timeouts).

Suggested change
});
}
}).listen(17234);
if (dropboxAuthServer) {
try { dropboxAuthServer.close(); } catch (e) {}
dropboxAuthServer = null;
}
});
}
});
dropboxAuthServer.listen(17234);
// Ensure the temporary auth server does not live indefinitely if the callback is never hit
dropboxAuthServer.setTimeout(5 * 60 * 1000, () => {
if (dropboxAuthServer) {
try { dropboxAuthServer.close(); } catch (e) {}
dropboxAuthServer = null;
}
});

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot please fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 0f3a014 — the Dropbox auth server is now closed (and set to null) immediately after the token is successfully parsed and forwarded to the renderer.

Comment thread app/main.js
Comment on lines 261 to +266
}, (files) => {
if (files) {
mainWindow.send('selected-directory', files)
try {
var content = fs.readFileSync(files[0] + '');
mainWindow.webContents.send('file-content', content.toString());
} catch (e) {

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

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

showOpenDialog can return an empty array when the user cancels; if (files) will still be truthy for [], and files[0] will be undefined, causing readFileSync to throw. Check files && files.length > 0 (or equivalent) before reading files[0].

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot please fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 0f3a014 — changed if (files) to if (files && files.length > 0) so a cancelled dialog (empty array) no longer triggers readFileSync(undefined).

Comment thread app/js/LetterStructure.js
Comment on lines 22 to 27
var _select = $("#sender").empty()

for (data of store.get("sender")) {
for (data of window.storeAPI.get("sender")) {
var _option = $("<option>").html(data);
_select.append(_option);
}

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

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

window.storeAPI.get("sender") can be undefined on a fresh install, and for (data of undefined) will throw a TypeError before the later code that initializes sender to [] runs. Fetch the value once and default it to an empty array (and declare the loop variable) before iterating.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot please fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in commit 0f3a014 — changed for (data of window.storeAPI.get("sender")) to for (data of (window.storeAPI.get("sender") || [])) to prevent a TypeError on fresh installs.

…ilure

Co-authored-by: Samuel3 <15921086+Samuel3@users.noreply.github.com>

Copilot AI commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please fix merge conflict and broken build

Fixed in commit 8e92a99. The build was failing because a previous npm install had upgraded package-lock.json from lockfileVersion 1 to lockfileVersion 3, causing npm ci to fail with missing transitive dependencies. Restored the lock file to its original lockfileVersion 1 format — npm ci and all 47 tests now pass.

@Samuel3

Samuel3 commented Mar 2, 2026

Copy link
Copy Markdown
Owner

@copilot please fix merge conflict on LetterStructure.js

…ring, open dialog cancel, Dropbox server leak, undefined sender default

Co-authored-by: Samuel3 <15921086+Samuel3@users.noreply.github.com>
@Samuel3

Samuel3 commented Mar 2, 2026

Copy link
Copy Markdown
Owner

@copilot please fix merge conflict

1 similar comment
@Samuel3

Samuel3 commented Mar 30, 2026

Copy link
Copy Markdown
Owner

@copilot please fix merge conflict

…/aboutAPI preload structure

Agent-Logs-Url: https://github.com/Samuel3/LetterCreator/sessions/3e3ccf99-6a0c-4830-b7a2-d3779df023c4

Co-authored-by: Samuel3 <15921086+Samuel3@users.noreply.github.com>

Copilot AI commented Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please fix merge conflict on LetterStructure.js

Fixed in commit 64ae365. Merged feature/add-tests into the branch, resolving all conflicts in LetterStructure.js, Settings.js, About.js, about-preload.js, settings-preload.js, and main.js. All 48 tests pass.

@copilot please fix merge conflict

Fixed in commit 64ae365. All merge conflicts resolved across 6 files — adopted the unified settingsAPI/aboutAPI contextBridge structure from the base branch, brought in letterStructureHelpers.js and the new tests, and used the pendingExportPath pattern for export-all-dialog. 48 tests passing.

Copilot AI requested a review from Samuel3 March 30, 2026 18:00
@Samuel3

Samuel3 commented Apr 25, 2026

Copy link
Copy Markdown
Owner

@copilot please fix merge conflicts

…ch, fix about-preload path, use webContents.send)

Agent-Logs-Url: https://github.com/Samuel3/LetterCreator/sessions/ad31b0b0-57ad-479d-9fba-c8d97c07ac07

Co-authored-by: Samuel3 <15921086+Samuel3@users.noreply.github.com>

Copilot AI commented Apr 25, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please fix merge conflicts

Fixed in commit 8d3a6c6. Merged the latest feature/add-tests changes (Store.js callback guard + expanded store tests), resolving conflicts in LetterStructure.js, about-preload.js, and main.js. All 50 tests pass.

Base automatically changed from feature/add-tests to master June 1, 2026 10:14
@Samuel3

Samuel3 commented Jun 1, 2026

Copy link
Copy Markdown
Owner

@copilot please check wether changes are still required

Copilot AI commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

@copilot please check wether changes are still required

The feature/add-tests base branch was merged into master today, which introduced new merge conflicts. Resolved them in commit 338d6cb:

  • LetterStructure.js: kept our contextBridge approach (no renderer-side require())
  • about-preload.js: kept our ../package.json path (correct for app/js/ location), and adopted the improved onProgress that stores the listener and returns a cleanup function to avoid memory leaks
  • main.js: kept settingsWindow.webContents.send() (correct Electron API for a BrowserWindow instance)

All 50 tests pass.

@Samuel3 Samuel3 closed this Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants