← back to writeups
SECURITY RESEARCH / ELECTRON

Electron IPC Abuse: An Auditor's Checklist

August 2026

Electron ships two worlds in one process boundary: a Chromium renderer that may load untrusted or semi-trusted content, and a Node-capable main process that can touch the filesystem, spawn processes, and talk to the OS. IPC is the bridge between them. When it's built carelessly, that bridge is the whole vulnerability.

Since Electron 20, contextIsolation, sandbox, and nodeIntegration: false are defaults - so most apps you'll audit today aren't broken by the classic "nodeIntegration: true" mistake. The real bugs live in what the preload script chooses to expose, and how the main process trusts what comes back over IPC. Below is what to check, in the order I check it.

1. Confirm the baseline isn't disabled

Grep every BrowserWindow and webPreferences block for regressions:

// vulnerable - full Node access in a renderer loading remote content
new BrowserWindow({
  webPreferences: {
    nodeIntegration: true,
    contextIsolation: false,
  }
})
// fixed
new BrowserWindow({
  webPreferences: {
    nodeIntegration: false,
    contextIsolation: true,
    sandbox: true,
  }
})

If contextIsolation is off anywhere, stop and flag it - every other check below is moot, since the renderer can reach the preload scope directly.

2. Read the preload script like an API surface

contextBridge.exposeInMainWorld is the actual attack surface, not the IPC channels themselves. A common failure is exposing raw ipcRenderer or a generic invoke wrapper:

// vulnerable — renderer can call *any* main-process handler
contextBridge.exposeInMainWorld('electron', {
  invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),
});
// fixed — explicit allowlist, one function per capability
contextBridge.exposeInMainWorld('api', {
  readConfig: () => ipcRenderer.invoke('config:read'),
  exportReport: (id) => ipcRenderer.invoke('report:export', id),
});

Anything the renderer can invoke, an XSS payload in that renderer can invoke too. Treat every exposed function as a public API endpoint reachable by attacker-controlled JS.

3. Check that main-process handlers validate, not just receive

ipcMain.handle arguments come from the renderer and should be treated as untrusted input, even in a "trusted" app:

// vulnerable — path traversal via unvalidated argument
ipcMain.handle('file:read', (event, filePath) => {
  return fs.readFileSync(filePath, 'utf-8');
});
// fixed — constrain to an allowed directory, validate type/shape
ipcMain.handle('file:read', (event, fileName) => {
  if (typeof fileName !== 'string' || fileName.includes('..')) {
    throw new Error('invalid file name');
  }
  const safePath = path.join(SAFE_DIR, path.basename(fileName));
  return fs.readFileSync(safePath, 'utf-8');
});

Look especially for handlers wrapping shell.openExternal, shell.openPath, child_process, or dynamic require() - these turn an IPC bug into RCE.

4. Verify the sender, not just the channel

A handler that's safe for the main window can be dangerous if a malicious or compromised iframe/webview can also reach it. Check for sender validation:

ipcMain.handle('privileged:action', (event) => {
  if (event.senderFrame.url !== MAIN_WINDOW_URL) {
    throw new Error('unauthorized sender');
  }
  // ...
});

Any app loading third-party content in a <webview> or iframe without this check is exposed to that content reaching privileged handlers.

5. Check return values for serialization abuse

APIs that return live objects across the context bridge (rather than plain data) have been a real bug class - a Promise-returning function combined with prototype manipulation in the main world can, in some configurations, be used to reach back into the isolated context. Keep exposed functions returning plain, structured-clone-safe data (strings, numbers, plain objects/arrays), not class instances, functions, or DOM nodes.

6. Confirm sandbox and CSP aren't quietly disabled elsewhere

Check session.defaultSession.webRequest overrides, webSecurity: false, and missing or overly permissive Content-Security-Policy headers - these don't create the IPC bug but they determine whether an attacker can get script execution in the renderer in the first place, which is the precondition for exploiting everything above.

Quick reference

CheckRed flag
webPreferencesnodeIntegration: true, contextIsolation: false, sandbox: false
preload exposureGeneric invoke/send passthrough instead of named functions
IPC handlersNo type/shape validation on arguments
Sender checksNo event.senderFrame verification on privileged channels
Return valuesNon-serializable objects, class instances, functions
CSP/webSecurityMissing CSP, webSecurity: false, allowRunningInsecureContent: true

None of this requires exotic tooling - a preload script and a grep -r ipcMain are usually enough to map the whole attack surface in an unfamiliar codebase.