Common Electron Misconfigurations That Turn XSS into RCE

Part 2: nodeIntegration, contextIsolation, sandbox, webSecurity, DevTools and CSP—how preference regressions turn renderer bugs into desktop code execution.

Series Electron application penetration testing (draft series) · Deep dive hub · Desktop VAPT service
← Part 1 · Threat modelPart 3 · IPC & preload →
Part 2 focus The flags that decide whether a renderer bug stays a UI issue or becomes desktop code execution: 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.

Table-style diagram of Electron webPreferences secure defaults versus risky settings
Teaching matrix: secure defaults versus common risky settings. Always verify effective prefs—defaults changed across major versions.
Misconfiguration explorerPOCFORGE / INTERACTIVE

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

  1. Centralise window creation so every window inherits the secure baseline.
  2. Fail CI if insecure flags appear in production builds.
  3. Strip DevTools and remote-debugging flags from release artefacts.
  4. 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 contextIsolation because an old plugin expected to patch globals.
  • Turning off webSecurity to 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.isPackaged is wrong in a staging build that customers receive.
  • Enabling webviewTag for 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.

Lab demo: nodeIntegration mode proving typeof require is function and hasFs true
Lab demo: find nodeIntegration — DevTools/UI proof shows typeofRequire=function, hasFs=true, marker under OS temp, prefs isolation off.
Lab demo: secure mode fails closed without runCommand or writePath
Lab demo: secure contrast — only getMode/writeMarker exposed; runCommandExposed=false.

11. Lab: proving nodeIntegration without theatrics

In ELECTRON_LAB_MODE=nodeIntegration (npm run start:node):

  1. Open DevTools (lab builds may allow it; production must not).
  2. Confirm typeof require === 'function' and that require('fs') loads.
  3. Write a marker with Node fs into 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.

Series Electron application penetration testing (draft series) · Deep dive hub · Desktop VAPT service
← Part 1 · Threat modelPart 3 · IPC & preload →