# 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 `<webview partition="persist:hermes-preview"
webpreferences="contextIsolation=yes,nodeIntegration=no,sandbox=yes">` 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:<port>/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.
