Abusing the Preload Bridge and IPC in Electron Applications

Part 3: contextBridge over-exposure, ipcMain validation gaps, and safe demos using the PocForge intentionally vulnerable Electron lab.

Series Electron application penetration testing (draft series) · Deep dive hub · Desktop VAPT service
← Part 2 · MisconfigurationsPart 4 · ASAR & updates →
Part 3 focus When isolation looks correct on paper but the preload bridge or ipcMain handlers still hand the renderer the keys. Includes safe demos from the PocForge Electron lab.

Disabling nodeIntegration without designing the bridge is how teams get surprised. The renderer no longer calls require directly; instead it calls window.api.run(…), and the main process obliges. From a tester’s perspective that is progress—surfaces are enumerable—but from an attacker’s perspective it is still a privileged API sitting behind XSS or a compromised renderer.

Five-step diagram from renderer XSS through preload and IPC to Node impact
Lab abuse path: renderer input → preload method → ipcMain → Node effect → safe marker/command proof.
IPC attack-path explorerPOCFORGE / INTERACTIVE

Keyboard: ← → · Respects reduced-motion (no autoplay).

1. Preload discipline (and how it fails)

Electron’s security tutorial warns against exposing raw ipcRenderer APIs and against passing callbacks that leak event objects. The healthy pattern is a tiny façade:

// Narrow bridge — production-shaped teaching example
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('appApi', {
  saveNote: (id, text) => ipcRenderer.invoke('notes:save', { id, text }),
});

The lab’s insecure preload deliberately does the opposite: it exposes runCommand, writePath and a raw invoke(channel, …args) passthrough. That mirrors real findings where developers “temporarily” expose debugging helpers and forget to remove them.

// LAB ONLY excerpt — preload-insecure.js (do not ship)
contextBridge.exposeInMainWorld('labApi', {
  runCommand: (cmd) => ipcRenderer.invoke('lab:run-command', cmd),
  writePath: (rel, content) => ipcRenderer.invoke('lab:write-path', rel, content),
  invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
});

2. Treat ipcMain as a private API

For each handler, document: accepted arguments, privileged side effects, authentication/authorisation assumptions, and whether event.senderFrame / origin is checked. Official guidance: validate the sender of all IPC messages. DeepStrike’s methodology likewise recommends schema validation and origin allowlists.

Common handler flaws:

  • Concatenating user paths into fs calls without normalisation and root checks.
  • Passing renderer strings into exec / execFile without allowlists.
  • Fetching attacker-controlled URLs from the main process (SSRF to localhost/metadata).
  • Trusting that “only our UI calls this channel” without enforcing it.
// Teaching shape for safer handlers
ipcMain.handle('notes:save', (event, payload) => {
  if (!isTrustedFrame(event.senderFrame)) return { ok: false };
  const id = schema.parse(payload).id; // validate
  return saveNote(id, payload.text);
});

3. Lab walkthrough (bridgeAbuse mode)

  1. npm run start:bridge in electron-lab/.
  2. Confirm the banner shows mode bridgeAbuse.
  3. Click labApi.runCommand with whoami — expect allowlisted stdout.
  4. Use labApi.writePath to create a file under the lab temp folder.
  5. Switch to npm run start:secure and show the same buttons fail closed.

Document screenshots of the mode banner and JSON output in engagement notes. The lab clamps writes to the OS temp tree and allowlists commands so demos stay non-destructive.

Even with a clean preload, unrestricted will-navigate, missing setWindowOpenHandler, or naïve shell.openExternal can move the user (or the app) onto attacker content or dangerous URL schemes. Public analyses of openExternal misuse show how unexpected schemes become host compromise on some platforms. Part 4 continues with protocols and updates; Part 5 folds these checks into the engagement checklist.

5. Sender validation in practice

Electron documents that frames—including some child frames—may send IPC. A handler that returns secrets or writes files should verify event.senderFrame (origin, not a brittle full URL string) against an allowlist. about:blank and opaque origins deserve denial by default. Log and drop unexpected callers; do not “fail open” for convenience in desktop builds.

function isTrustedFrame(frame) {
  return !!(frame && frame.origin === 'app://ui'); // example custom scheme
}

6. Argument validation beyond strings

IPC payloads are often attacker-controlled objects. Look for:

  • Path traversal via parent-directory segments, absolute paths, UNC shares and symlink games.
  • Prototype pollution when handlers merge options objects.
  • Type confusion ("1" vs 1, arrays where strings are expected).
  • Extra unexpected fields that switch privileged feature flags.

Prefer schema libraries in the main process. Renderers can lie; main must not.

7. WebView and guest content

If webviewTag is enabled, guests can become a second renderer with their own prefs and sometimes their own preload. Historical public bugs abused guest preload attributes and IPC into openExternal. Prefer WebContentsView / controlled BrowserView patterns with centralised creation hooks (will-attach-webview stripping untrusted preloads) as described in Electron’s checklist.

8. Evidence that survives review

For each IPC finding, attach: channel name, handler excerpt, missing check, request payload (redacted), and a safe proof (temp marker path or allowlisted command output). Avoid flashy calculator popups in customer screenshots; markers read cleaner in enterprise reports and match PocForge’s “prove without destruction” standard.

9. Bridge anti-patterns checklist

During code review or ASAR greps, escalate immediately when you see:

  • contextBridge.exposeInMainWorld(..., {{ invoke: ipcRenderer.invoke }}) or exposing on/send wholesale.
  • Methods named exec, run, eval, require, openPath, writeFile, readFile without narrow parameters.
  • Handlers that accept a shell command string or a URL and forward it unchanged.
  • “Temporary” debug bridges gated only by a renderer-supplied boolean.
  • Duplicate IPC channels registered in multiple packages with inconsistent validation.

Healthy bridges speak in domain language: notes.create, vault.lock, export.start. They do not speak in OS primitives. If product requirements truly need a privileged operation, implement it entirely in main with server-side (main-side) authorisation, then expose a single boolean success/failure to the renderer.

For dynamic confirmation in the lab’s bridgeAbuse mode, call each exported method once with benign arguments and archive the JSON responses. Then attempt a rejected command (for example ls when only whoami is allowlisted) to show that allowlists fail closed. Contrast with secure mode where the methods are absent. That contrast is often more persuasive to engineering managers than a theoretical slide about contextBridge.

Remember navigation and openExternal are also bridges—just ones that exit the process into the OS handler. Include them on the same IPC spreadsheet so nothing privileged hides in event listeners outside ipcMain.

IPC findings rarely stand alone. A weak bridge is more severe when prefs already disable isolation, when DevTools ship in production, or when update staging is writable. Cross-link evidence: the same report section that shows labApi.runCommand should reference the prefs matrix from Part 2 and the install ACL notes from Part 4. Attackers chain whatever is cheapest; reports should too.

Use the lab as a controlled place to practise that narrative. Record a short internal Loom or markdown note that walks secure → nodeIntegration → bridgeAbuse, then reuse the structure on customer binaries without reusing lab payloads against systems outside authorisation.

Lab demo: bridgeAbuse IPC exploit path with whoami stdout and writePath marker
Lab demo: exploit over-exposed preload — labApi.runCommand(whoami) returns stdout, writePath creates temp marker, raw invoke reaches ipcMain; ls denied by allowlist.
Lab demo strip comparing secure, nodeIntegration and bridgeAbuse outcomes
Lab demo: find issues across modes — secure fails closed; nodeIntegration yields Node in renderer; bridgeAbuse yields IPC command/path effects despite isolation.

11. Lab screenshots and exact console steps

Illustrative PocForge Electron lab window in bridgeAbuse mode showing whoami IPC result
bridgeAbuse mode: isolation enabled, but labApi.runCommand returns allowlisted whoami output.

Exact sequence for engagement rehearsal notes:

cd electron-lab
npm install
npm run start:bridge
# In the UI: click labApi.runCommand with whoami
# Then in DevTools:
// LAB ONLY — bridgeAbuse DevTools
await window.labApi.runCommand('whoami')
await window.labApi.runCommand('ls')          // expect deny
await window.labApi.writePath('nested/a.txt', 'pocforge-lab-write\\n')
await window.labApi.writePath('../escape.txt', 'x')  // discuss incomplete checks + temp clamp
await window.labApi.invoke('lab:write-marker', 'via-raw-invoke')
Object.keys(window.labApi)
Illustrative PocForge Electron lab window in secure mode with narrow bridge
secure mode contrast: dangerous bridge methods absent; writeMarker still works for benign telemetry-style proofs.

Archive Object.keys(window.labApi) for both modes. The delta list is your finding template: methods that exist only in the insecure preload should not ship. Part 6 extends this into systematic fuzzing with wordlists under electron-lab/fuzz/.

12. Finding the bridge in DevTools (lab)

After npm run start:bridge, open DevTools and treat the console as your API client:

// LAB ONLY — discovery then exploitation
console.log(window.labApi)
Object.keys(window.labApi)
// FIND: runCommand / writePath / invoke should appear in bridgeAbuse
// EXPLOIT:
await window.labApi.runCommand('whoami')
await window.labApi.writePath('findings/ipc-1.txt', 'pocforge-lab-write\n')

Archive a screenshot of the JSON panel (as in the lab demos above) plus the mode banner. That pair answers the two questions reviewers ask first: what was exposed? and what privileged effect followed?

If you only have a packaged customer build, the same console workflow applies once you confirm DevTools are reachable under RoE—or use a customer-supplied debug build. Shipping DevTools in production is itself a Part 2 finding; using them during an authorised test is normal.

FAQ

If contextIsolation is true, are we safe?

Only if the exposed bridge and IPC handlers are minimal and validated. Isolation stops prototype tricks against preload internals; it does not stop calling an intentionally exported runCommand.

Should we fuzz IPC blindly?

Prefer an inventory-first approach, then targeted malformed inputs under authorisation. Blind fuzzing of production mains can be disruptive; use lab builds.

Sources (public)

  • Electron — Security items 17 & 20 (IPC sender validation; do not expose Electron APIs)
  • DeepStrike — IPC / preload testing sections
  • HackTricks — preload and IPC RCE patterns
  • Doyensec — Electronegativity / Electron security research (public papers & tooling)

Lab demos are intentionally vulnerable and for authorised training only. PocForge does not claim CERT-In empanelment in this article.

Series Electron application penetration testing (draft series) · Deep dive hub · Desktop VAPT service
← Part 2 · MisconfigurationsPart 4 · ASAR & updates →