| 1 | --- |
| 2 | archetype: electron-desktop |
| 3 | primary_gate_families: [1, 7] |
| 4 | --- |
| 5 | |
| 6 | # Archetype - Electron Desktop Apps |
| 7 | |
| 8 | Targets: Obsidian, Logseq, Cursor, Discord, VS Code-likes, Slack, Notion, and the long tail of |
| 9 | Electron apps. The thesis: an Electron app is a **browser shipped with Node.js wired to the OS**. If |
| 10 | attacker-controlled content reaches a renderer that can touch Node - directly via `nodeIntegration`, |
| 11 | or indirectly via a leaked IPC/`preload` bridge - a content-level XSS becomes **local code execution** |
| 12 | on the user's machine. That jump (web-bug → OS-RCE) is what makes this a [[../00-doctrine/SEVERITY-GATE#family-1-remote-code-execution-the-crown|Family 1]] archetype, not a web-XSS footnote. |
| 13 | |
| 14 | See: [[ai-agent-mcp]] (AI Electron apps overlap), [[cicd-supplychain]] (plugin/extension registries). |
| 15 | |
| 16 | --- |
| 17 | |
| 18 | ## Principals & trust boundaries |
| 19 | |
| 20 | - **Principals:** remote content authors (a markdown note, a webpage rendered in-app, a chat message, |
| 21 | a repo opened in the editor, a plugin author), the **renderer** process (web context), the |
| 22 | **preload** bridge, the **main** process (full Node + OS), the host user. |
| 23 | - **Core boundaries:** |
| 24 | 1. *Untrusted content → renderer* (markdown, HTML, chat, a synced note, a workspace file). |
| 25 | 2. *Renderer → main* across the `contextBridge`/`ipcRenderer` (the security-critical seam). |
| 26 | 3. *External app → app* via the **custom protocol handler** (`obsidian://`, `cursor://`, |
| 27 | `discord://`) and deep links / file associations. |
| 28 | 4. *Plugin/extension → app core* (third-party JS running in-process). |
| 29 | |
| 30 | **Boundary map template:** |
| 31 | ``` |
| 32 | principals: remote-content-author | plugin-author | another-local-app | host-user |
| 33 | trust inputs: markdown/HTML note, chat msg, opened repo files, deep link (custom://...), |
| 34 | .asar contents, plugin manifest+code, auto-update feed, workspace settings |
| 35 | enforcement pts: nodeIntegration:false, contextIsolation:true, sandbox:true, |
| 36 | webSecurity, the contextBridge API allowlist, CSP, protocol handler arg parsing, |
| 37 | plugin permission model, update signature check |
| 38 | cross-boundary |
| 39 | sinks: renderer DOM sink reaching a bridged IPC that calls fs/child_process/shell ; |
| 40 | ipcMain handler doing shell.openExternal/exec/require on renderer-supplied arg ; |
| 41 | protocol handler passing arg into exec/file path ; preload leaking require/process |
| 42 | confusion |
| 43 | hypotheses: "contextIsolation is on but the bridge exposes a too-powerful method"; |
| 44 | "one BrowserWindow/webview still has nodeIntegration:true"; |
| 45 | "the custom-protocol arg flows into a shell without sanitization"; |
| 46 | "asar integrity is unchecked so a swapped file runs" |
| 47 | ``` |
| 48 | |
| 49 | --- |
| 50 | |
| 51 | ## Kill chains (each lands on a Gate family) |
| 52 | |
| 53 | ### KC-1 - Renderer XSS → nodeIntegration / leaked bridge → RCE (Family 1) |
| 54 | Find a DOM-injection in rendered content (markdown→HTML, a chat embed, an SVG/iframe, a |
| 55 | `dangerouslySetInnerHTML`/`innerHTML` sink). Then find that the renderer can reach Node - either a |
| 56 | window with `nodeIntegration:true`, OR a `contextBridge` method that ultimately calls |
| 57 | `child_process`, `fs.writeFile`, `require`, or `shell.openExternal` with attacker data. |
| 58 | - **Sinks to CPG-trace:** every `ipcMain.handle`/`on` whose handler reaches `exec`/`execFile`/`spawn`/ |
| 59 | `fs`/`require`/`vm`/`shell.*`; every `contextBridge.exposeInMainWorld` surface; `webview`/`<iframe>` |
| 60 | with elevated perms; `webPreferences` with `nodeIntegration:true` or `sandbox:false`. |
| 61 | - **Oracle:** the in-app XSS payload triggers `child_process.exec('id')`/writes our canary file → RCE. |
| 62 | |
| 63 | ### KC-2 - Custom protocol handler argument injection (Family 1) *(pre-interaction, high value)* |
| 64 | `obsidian://`, `cursor://open?file=...`, `discord://`, deep links and file associations let *another |
| 65 | local app or a webpage* hand the target a string. If that string flows into a file path, a shell, or |
| 66 | a "open this repo/file" routine, a malicious link → code execution with minimal user interaction. |
| 67 | - **Sinks:** the `app.setAsDefaultProtocolClient` handler, `open-url`/`second-instance` event |
| 68 | handlers, anything parsing `argv` into `exec`/path/`loadURL(...)` (a `file://` or `javascript:` URL |
| 69 | into `loadURL` is itself an RCE bridge). |
| 70 | - **Oracle:** a crafted `app://...` link from a webpage causes command execution / arbitrary file |
| 71 | open that fires our canary → Family 1. |
| 72 | |
| 73 | ### KC-3 - ASAR / preload tamper & integrity bypass (Family 1) |
| 74 | Apps often lack ASAR integrity checks; a local-write primitive (from another bug, or a malicious |
| 75 | installer/update) that swaps a file inside `app.asar` or the preload runs as the app. More |
| 76 | interesting remotely: a **preload script that leaks `require`/`process`** into the renderer defeats |
| 77 | contextIsolation entirely. |
| 78 | - **Sinks:** `webPreferences.preload` scripts that assign Node primitives onto `window`; missing |
| 79 | `enableEmbeddedAsarIntegrityValidation`; an auto-updater that fetches over HTTP or skips signature |
| 80 | verification (→ also [[cicd-supplychain]]). |
| 81 | - **Oracle:** a tampered/leaked path yields code execution → Family 1. |
| 82 | |
| 83 | ### KC-4 - Plugin / extension supply chain (Family 1, bridges [[cicd-supplychain]]) |
| 84 | Obsidian community plugins, VS Code/Cursor extensions, Logseq plugins run **in-process with full |
| 85 | app/Node powers** and install from registries with weak review. A malicious or compromised plugin is |
| 86 | straight RCE; the *novel* angle is a **core flaw that lets a low-trust plugin exceed its declared |
| 87 | permissions**, or a plugin-update/registry takeover. |
| 88 | - **Sinks:** the plugin loader, the (often nonexistent) permission boundary, the plugin update URL. |
| 89 | - **Oracle:** a plugin doing what it shouldn't be able to → callback → Family 1. |
| 90 | |
| 91 | --- |
| 92 | |
| 93 | ## The dynamic oracle, concretely |
| 94 | |
| 95 | 1. Pull the app's source / unpack the `.asar` (`npx asar extract`). Pin version. |
| 96 | 2. CPG/grep the **renderer→main seam**: enumerate every `ipcMain` handler and every `contextBridge` |
| 97 | export; for each, trace whether renderer-controlled args reach an OS sink. |
| 98 | 3. Stand up the app in a disposable VM. Inject the content-level payload (a malicious note / chat / |
| 99 | deep link) and watch for the canary (`/tmp/cyclops-$RAND`, a DNS callback). |
| 100 | 4. REPLICA_CONFIRMED = the canary fires from the desktop app on our own machine. LIVE_CANARY only if |
| 101 | the program/scope permits. |
| 102 | |
| 103 | Tooling: `analyze_js`/`audit_js_source` for the bundle, `joern_query` for IPC dataflow, |
| 104 | `sandbox_create` for the disposable detonation VM, `start_callback_server`. |
| 105 | |
| 106 | --- |
| 107 | |
| 108 | ## BORING traps - do NOT spend time here |
| 109 | |
| 110 | - **Self-XSS / XSS that never reaches Node.** A renderer with `contextIsolation:true`, `sandbox:true`, |
| 111 | no powerful bridge, and a strong CSP - an XSS there is a web bug, not Family 1. Prove the Node jump |
| 112 | or it's cleared-axes. |
| 113 | - **"Missing CSP" / "webSecurity could be off".** A config observation is not a finding; show the |
| 114 | exploit it enables. |
| 115 | - **DevTools / `--inspect` local access.** Needs local access already; not a remote finding. |
| 116 | - **Reporting a known Electron CVE for an outdated bundled Chromium.** Sibling-CVE / version-banner. |
| 117 | - **A deep link that only navigates within the app** with no sink. Navigation ≠ execution. |