nodeIntegration, contextIsolation, sandbox, webSecurity, DevTools exposure and CSP.Modern Electron majors ship safer defaults than early versions—but production apps still regress by copying old snippets, enabling Node for “quick filesystem access,” or disabling isolation for a legacy library. This part explains what to look for during static review and runtime checks, with lab modes that make the differences tangible.

Keyboard: ← → · Respects reduced-motion (no autoplay).
1. The secure baseline (verify, do not assume)
Electron’s published checklist asks developers to keep Node integration off for remote or untrusted content, enable context isolation, enable process sandboxing, keep webSecurity on, define a CSP, validate IPC senders, limit navigation and window creation, and avoid shell.openExternal on untrusted URLs. Testers should treat that list as the expected baseline, then prove what the binary actually enables.
// Teaching example — secure-shaped BrowserWindow prefs
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
},
});
At runtime, dump effective preferences (in a lab or authorised build) via webContents.getLastWebPreferences() and compare every window—including hidden ones and guests.
2. nodeIntegration: the classic XSS→RCE bridge
When nodeIntegration is true, renderer JavaScript can require('fs') or require('child_process'). Any HTML injection sink—markdown preview, note rendering, SVG, help panels—can escalate. Community write-ups (Cobalt Part 1, HackTricks, DeepStrike) still lead with this pattern because it remains common in older or hastily ported apps.
Lab exercise (npm run start:node): confirm Node availability in the renderer, then write a marker file under the lab temp directory. Prefer markers over launching GUI calculators in shared environments.
// LAB ONLY — illustrative check in nodeIntegration mode
typeof require === 'function' && !!require('fs');
3. contextIsolation and sandbox
contextIsolation separates page JavaScript from preload/Electron internal worlds. Disabling it enables prototype pollution-style attacks against preload logic and historical Electron internal gadgets—documented extensively in public Electron security research. Enabling nodeIntegration also disables sandboxing for that renderer; the sandbox document makes that coupling explicit.
Assessment tip: search for contextIsolation:\s*false, sandbox:\s*false, --no-sandbox, and enableRemoteModule / @electron/remote usage. Remote-style bridges expand the attack surface even when they do not yield one-shot RCE.
4. webSecurity, CSP and mixed content
Disabling webSecurity weakens same-origin protections and pairs with insecure content loading. A missing or weak CSP (unsafe-inline, wildcards) makes XSS easier to weaponise. Prefer HTTP header CSP via session.webRequest.onHeadersReceived where possible; file:// pages often need careful custom-protocol designs instead (official recommendation: prefer custom protocols over broad file:// privileges).
5. DevTools and verbose CLI noise
Shipping Toggle Developer Tools, leaving openDevTools() in production, or exposing remote debugging ports rarely equals instant RCE—but they accelerate chaining. Launching the binary from a terminal (as Cobalt and DeepStrike describe) can leak update paths, temp directories and module load details useful for local privilege follow-ons in Part 4.
| Check | How | Evidence |
|---|---|---|
| Prefs in source/ASAR | grep BrowserWindow / webPreferences | Flag values per window |
| Effective prefs | getLastWebPreferences() | Runtime dump |
| DevTools | Menu, F12, Ctrl+Shift+I | Screenshot / note |
| CLI logs | Run binary in terminal | Paths, update URLs |
| CSP | Headers / meta | Policy string |
6. Hardening notes for developers
- Centralise window creation so every window inherits the secure baseline.
- Fail CI if insecure flags appear in production builds.
- Strip DevTools and remote-debugging flags from release artefacts.
- Keep Electron/Chromium within a defined freshness window—engine RCEs turn XSS into RCE even when prefs look perfect (public Discord/ElectroVolt-style cases illustrate the class; cite concrete CVEs only when verified against a named version under test).
7. Why production still ships insecure prefs
Secure defaults only help if window factories use them. Recurring regression patterns observed across public write-ups and open-source Electron apps include:
- Copy-pasting a 2018 Stack Overflow snippet that sets
nodeIntegration: true“so fs works.” - Disabling
contextIsolationbecause an old plugin expected to patch globals. - Turning off
webSecurityto silence CORS errors when the real fix is a proper API design or custom protocol. - Leaving
electron-is-dev/ unpackaged code paths that open DevTools whenever!app.isPackagedis wrong in a staging build that customers receive. - Enabling
webviewTagfor a single feature and forgetting guest prefs inherit weaker settings.
CI grep rules are cheap insurance. Fail the build if production main-process code matches insecure literals, and add a startup self-check that enumerates windows and asserts the baseline—Electron’s own security commentary encourages fail-fast mindset for untrusted content.
8. Remote module and other legacy bridges
The built-in remote module is deprecated/removed; some apps still pull @electron/remote. Even when it does not yield trivial RCE, it widens the renderer’s reach into main-process objects and often coexists with weak isolation. Flag it, document enabled surfaces, and recommend deletion in favour of explicit IPC.
Likewise, nodeIntegrationInWorker and nodeIntegrationInSubFrames deserve explicit review whenever workers or iframes render attacker-influenced content. Defaults are safer today; overrides should have written justifications in architecture docs.
9. CSP that survives Electron packaging
CSP is harder on file:// pages. Prefer serving UI over a privileged custom scheme you control, then attach CSP via response headers. Avoid 'unsafe-eval' unless a named framework forces it—and budget time to remove that need. During tests, attempt a benign inline script probe; collect console CSP errors as evidence that the policy is active, not merely present as a comment in README.
When prefs look perfect but XSS still executes freely, check for executeJavaScript from main, markdown renderers that emit raw HTML, and Electron versions with known renderer escapes. Prefs are necessary; they are not a substitute for input handling.
10. Walking the matrix during an engagement
Print a one-page prefs matrix for every window you discover. Columns: window name, URL/scheme loaded, nodeIntegration, contextIsolation, sandbox, webSecurity, preload path, DevTools, notes. Fill it from ASAR first, then confirm with runtime dumps on at least one packaged build. Discrepancies between source comments and effective prefs are findings in their own right—release pipelines sometimes inject debugging flags only into certain channels.
When you find nodeIntegration: true on a window that also loads remote HTTPS content, treat that as a critical design flaw even before you land XSS: the official Electron security preface calls out remote content plus Node as an unacceptable combination. If the window only loads packaged local UI, severity still depends on whether user-controlled data can influence that UI (almost always yes for notes, chat, email and file preview apps).
Pair prefs review with a quick CSP and permission-handler check. Electron approves many permission prompts by default unless you install session.setPermissionRequestHandler. Camera, microphone and notification abuse is less glamorous than RCE but still belongs on the matrix for products that embed rich web content.
Close Part 2 by running the lab’s three modes and photographing the mode banner beside a prefs excerpt from main.js. That single collage becomes a teaching aid for developers who have never seen isolation fail closed versus open.


11. Lab: proving nodeIntegration without theatrics
In ELECTRON_LAB_MODE=nodeIntegration (npm run start:node):
- Open DevTools (lab builds may allow it; production must not).
- Confirm
typeof require === 'function'and thatrequire('fs')loads. - Write a marker with Node
fsinto the lab temp directory rather than spawning GUI calculators:
// LAB ONLY — nodeIntegration mode console
const fs = require('fs');
const os = require('os');
const path = require('path');
const dir = path.join(os.tmpdir(), 'pocforge-electron-lab');
fs.mkdirSync(dir, { recursive: true });
const dest = path.join(dir, 'node-integration-proof.txt');
fs.writeFileSync(dest, 'pocforge-lab-marker\\nvia=require(fs)\\n');
dest;
Then flip to npm run start:secure and show typeof require === 'undefined' (or that require is not a Node require). Pair both screenshots with the prefs excerpt from main.js that toggles nodeIntegration, contextIsolation and sandbox. That is the entire Part 2 lesson in three artefacts: insecure prefs, privileged proof, secure contrast.
For bridgeAbuse, keep isolation on and demonstrate that XSS can still call over-exposed bridge methods—preview of Part 3—so engineers do not stop hardening at “we turned nodeIntegration off.”
FAQ
Defaults changed—how do we avoid outdated advice?
Read the current Electron security tutorial for the version you ship, and verify runtime prefs. Do not rely on blog posts alone for default values.
Is sandbox alone sufficient?
No. Sandbox limits OS access for the renderer; dangerous preload/IPC designs can still mediate privileged main-process actions.
Sources (public)
- Electron — Security checklist & sandbox docs: Security, Sandbox
- Cobalt — Common Electron misconfigurations Part 1
- DeepStrike — Electron penetration testing guide (webPreferences section)
- HackTricks — Electron Desktop Apps (prefs & RCE patterns)
PocForge publishes offensive-security research to explain attack paths and defensive controls. This article is not a claim of CERT-In empanelment and does not describe a named client engagement.
