# REPRO-2026-00326: Hermes Agent Electron preview webview sandbox escape via CVE-2026-70608 ## Summary Status: published Severity: high CVSS: Unknown CWE: CWE-693 Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00326 CVE: CVE-2026-70608 ## Package Name: hermes-agent Ecosystem: github Affected: electron <39.8.10, >=40.0.0-alpha.1 <41.10.3, >=42.0.0-alpha.1 <42.0.1 Fixed: 41.10.3 ## Root Cause # RCA Report — CVE-2026-70608 R3B: preview-pane webview guest window.open escalation ## Summary hermes-agent's right-rail URL preview (`apps/desktop/src/app/chat/right-rail/preview-pane.tsx:557-561`) creates a `` whose guest webContents carries **no `setWindowOpenHandler`** (verified: no `web-contents-created` / `did-attach` / `new-window` wiring exists anywhere in `apps/desktop/electron/main.ts`). In Electron, no handler means **default ALLOW**. On Electron 40.10.2 (pinned in `apps/desktop/package.json`), the CVE-2026-70608 OpenURL bypass — a synthetic ctrl/meta-click dispatched from a sandboxed iframe **without `allow-popups`** — fires inside that guest and, because no handler exists to deny it, spawns a **real application BrowserWindow** loading attacker content. On Electron 41.10.3 the identical iframe trigger is blocked (fix confirmed). This escalates the R2 finding (where the main window's `setWindowOpenHandler` capped the effect at an allowlisted external-URL open) to outcome (a) of the ticket: a real window spawns. ## Impact - Product: hermes-agent desktop (`apps/desktop`), repo commit `e3fab0437ee50ebe511cec57b9ac36f0c2803268` - Affected runtime: Electron **40.10.2** (pinned in the repo); fixed in Electron **41.10.3** - Any page rendered in the right-rail URL preview (attacker-selected URL, e.g. a link the agent was asked to preview) can, without any user gesture, spawn real Electron `BrowserWindow`s loading attacker-controlled content — from the top-level guest document AND, on the vulnerable Electron, from a sandboxed iframe (`sandbox="allow-scripts"`, no `allow-popups`) inside it. - Consequences: attacker-controlled application windows inside the Hermes desktop (phishing chrome, permission prompts, further drive-by surface) — beyond R2's browser-external-open cap. ## Impact Parity - Disclosed/claimed maximum impact: sandbox escape (guest content escapes the preview confinement into a real application window); the ticket asked to determine outcome (a) real window spawn / (b) external path / (c) blocked. - Reproduced impact: **outcome (a)** — real `BrowserWindow` creation from the CVE bypass inside the preview webview guest (main-process `browser-window-created` + guest `did-create-window` + `did-finish-load` of the attacker marker URL), twice on 40.10.2; blocked twice on 41.10.3. - Parity: **full** for the claimed escalation question. (No renderer code execution, Node integration, or permission escalation inside the spawned window was attempted or claimed here; the spawned windows use Electron default webPreferences.) ## Root Cause 1. `preview-pane.tsx` creates the preview webview for URL targets with a persistent partition and sandboxed renderer prefs, but nothing in the main process ever attaches a `setWindowOpenHandler` (or `web-contents-created` policy) to that guest. Electron's default in this case is to honor window-open requests by creating new windows. 2. Electron ≤ 40.10.2 (CVE-2026-70608): the OpenURLFromTab path omits the initiating-frame popup-sandbox check, so a synthetic modifier-click on an anchor inside an iframe sandboxed **without `allow-popups`** is still routed as a `foreground-tab` open (the R2 primitive, revalidated here inside the webview guest). 3. Combined: the iframe sandbox bypass reaches the guest's window-open path and the guest has no handler to deny it → a real window is created. On 41.10.3 the popup-sandbox check is restored, so the iframe trigger is blocked before any window is created. Fix upstream: Electron 41.10.3 (CVE-2026-70608 fix). Product-side defense in depth: attach a `setWindowOpenHandler` (deny or route through `openExternalUrl`) to every webview guest via `app.on('web-contents-created')` for `type === 'webview'`. ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (self-contained; idempotent). 2. What it does: - Seeds the work repo from the prepared project cache (or clones) and checks out `e3fab0437ee50ebe511cec57b9ac36f0c2803268`. - Installs node deps with pnpm (workspace file + `pnpm import` of the root package-lock; npm itself OOMs in this container's 1.5GB cgroup) and the Python backend venv the desktop spawns (`venv/bin/python -m hermes_cli.main serve`). - Applies **observation-only** instrumentation to `electron/main.ts` (`bundle/repro/patch_instrumentation.py`, gated on `HERMES_REPRO_INSTRUMENT_LOG`): records `browser-window-created`, `web-contents-created`, guest `did-create-window`, guest console, `openExternalUrl`, `shell.openExternal` (record-only stub — never executes a real OS open), and the main window's `setWindowOpenHandler`. Guest behavior is NOT modified. - Bundles the electron main process with the product's own `scripts/bundle-electron-main.mjs --dev`, and bundles the SAME renderer sources with esbuild (`bundle/repro/bundle_renderer.mjs`; vite build/dev OOMs in the cgroup), served statically at `http://127.0.0.1:5174` and loaded via the product's own `HERMES_DESKTOP_DEV_SERVER` dev-mode entry. - Runs the Playwright driver (`bundle/repro/repro.spec.ts`) under Xvfb, twice on Electron 40.10.2 and twice on 41.10.3. Each attempt: launches the real app with a real backend and a mock OpenAI provider; sends a chat message asking to preview `http://127.0.0.1:/x`; the mock returns a real `open_preview` tool call which the backend executes; the driver clicks the product's "Open Preview" affordance and, if needed, falls back to the product's own localStorage restore path (`hermes.desktop.previewTabs.v2` + reload) to mount the pane. The preview-pane then creates the webview pointed at the attacker page, which fires `window.open` plus synthetic ctrl+meta-clicks from the top level and from a sandboxed iframe (`sandbox="allow-scripts"`, no `allow-popups`). 3. Expected evidence: per-attempt `guest.did-create-window` / `browser-window-created` / `did-finish-load` events for `…/marker?src=frame-click` (CVE bypass) on 40.10.2, and their absence (with the iframe beacon still dispatched) on 41.10.3. ## Evidence Per-attempt artifacts (two runs of the script, both `confirmed=true`): - `bundle/logs/vulnerable_attempt_{1,2}.{json,main.jsonl,log,diag.log,requests.json}` — Electron 40.10.2 - `bundle/logs/fixed_attempt_{1,2}.{json,main.jsonl,log,diag.log,requests.json}` — Electron 41.10.3 - `bundle/repro/runtime_manifest.json` — entrypoint, target identity, artifact hashes - `bundle/logs/reproduction_steps.log` — full script log Key excerpts (run 2, vulnerable attempt 1, Electron 40.10.2): ``` web-contents-created {type:'webview'} # preview guest mounted guest-console: PRUVA src=page-loaded # attacker page ran in the guest guest-console: PRUVA src=windowopen-result&value=null # gesture-less window.open blocked guest.did-create-window url=…/marker?src=top-click disposition=foreground-tab web-contents-created {type:'window'} + win.did-finish-load …/marker?src=top-click guest-console: PRUVA-FRAME src=frame-windowopen-result&value=null # iframe window.open blocked guest.did-create-window url=…/marker?src=frame-click disposition=foreground-tab # CVE bypass fires web-contents-created {type:'window'} + win.did-finish-load …/marker?src=frame-click # REAL window ``` Fixed attempt 1 (Electron 41.10.3): the iframe still dispatches (`PRUVA-FRAME src=frame-click-dispatched` beacon) but **no** `did-create-window` / window for `src=frame-click` ever occurs; only the generic top-level clicks spawn windows (unchanged pre-existing default-allow behavior for a handler-less guest, present on both versions). Outcome matrix (both full script runs): | attempt | Electron | outcome | |---|---|---| | vulnerable #1 | 40.10.2 | A_REAL_WINDOW_SPAWNED_CVE_BYPASS (cveWindows=1, genericWindows=2) | | vulnerable #2 | 40.10.2 | A_REAL_WINDOW_SPAWNED_CVE_BYPASS (cveWindows=1, genericWindows=2) | | fixed #1 | 41.10.3 | A_REAL_WINDOW_SPAWNED_GENERIC_ONLY (cveWindows=0, genericWindows=2) | | fixed #2 | 41.10.3 | A_REAL_WINDOW_SPAWNED_GENERIC_ONLY (cveWindows=0, genericWindows=2) | Environment: Ubuntu 26.04 container, x86_64, Xvfb, UID 1000, all traffic to 127.0.0.1, `shell.openExternal` record-only. Chromium OS sandbox disabled (`--no-sandbox`) because the container blocks all namespace creation (EPERM even for root); this matches the product's own e2e fixtures and does not affect the Blink iframe popup-sandbox under test. ## Recommendations / Next Steps - Upgrade the desktop app to Electron ≥ 41.10.3 (upstream CVE-2026-70608 fix). - Defense in depth (works regardless of Electron version): in the main process, attach `setWindowOpenHandler` to webview guests (`app.on('web-contents-created')`, `type==='webview'`) that denies or routes through the allowlisted `openExternalUrl`, mirroring the main window. - Consider a restrictive `session.setPermissionRequestHandler` / CSP for `persist:hermes-preview`. - Test: an e2e spec asserting that a sandboxed iframe in a preview webview cannot create windows. ## Additional Notes - Idempotency: the script was run twice consecutively end-to-end; both runs printed `confirmed=true` with the same 2×2 outcome matrix. All setup steps skip when outputs exist. - The chat → tool-call → gateway path executed for real (mock provider issued a genuine `open_preview` tool call the backend ran); the pane mount additionally used the product's own persisted-store restore path when the in-test click did not land (a test-harness timing issue, not a product defect). - The renderer is bundled by esbuild instead of vite purely because vite/rolldown exceeds the container's 1.5GB memory cgroup; identical sources and product code paths are exercised. - Separately observed (both Electron versions, pre-existing, not the CVE): the top-level guest page's synthetic ctrl+click also spawns real windows (default allow, no handler). Worth its own product hardening note. ## Reproduction Details Reproduced: 2026-08-23T15:38:31.072Z Duration: 16268 seconds Tool calls: 541 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00326 pruva-verify CVE-2026-70608 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00326&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00326/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-70608 - Source: https://github.com/electron/electron/security/advisories/GHSA-9f4c-93c8-jc8g ## Artifacts - bundle/repro/rca_report.md (analysis, 10205 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 13672 bytes) - bundle/logs/fixed_attempt_1.diag.log (log, 11344 bytes) - bundle/logs/fixed_attempt_1.log (log, 6933 bytes) - bundle/logs/fixed_attempt_1.requests.json (other, 6483 bytes) - bundle/logs/fixed_attempt_2.diag.log (log, 11499 bytes) - bundle/logs/fixed_attempt_2.log (log, 6933 bytes) - bundle/logs/fixed_attempt_2.requests.json (other, 6483 bytes) - bundle/logs/reproduction_steps.log (log, 4180 bytes) - bundle/logs/vulnerable_attempt_1.diag.log (log, 14453 bytes) - bundle/logs/vulnerable_attempt_1.log (log, 7594 bytes) - bundle/logs/vulnerable_attempt_1.requests.json (other, 6593 bytes) - bundle/logs/vulnerable_attempt_2.diag.log (log, 14509 bytes) - bundle/logs/vulnerable_attempt_2.log (log, 7443 bytes) - bundle/logs/vulnerable_attempt_2.requests.json (other, 6593 bytes) - bundle/repro/bundle_renderer.mjs (other, 6004 bytes) - bundle/repro/patch_instrumentation.py (script, 5791 bytes) - bundle/repro/repro.spec.ts (other, 24945 bytes) - bundle/repro/runtime_manifest.json (other, 4069 bytes) - bundle/repro/serve_static.mjs (other, 1294 bytes) - bundle/repro/validation_verdict.json (other, 1530 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00326 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00326/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00326 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev