{"repro_id":"REPRO-2026-00326","version":6,"title":"Hermes Agent Electron preview webview sandbox escape via CVE-2026-70608","repro_type":"security","status":"published","severity":"high","description":"R3B — escalation residual after ARGUS-SCA-...-R2 (CONFIRMED on the ArtifactPreview iframe path, where the main window's setWindowOpenHandler caps the effect at an allowlisted external-URL open). THE OPEN QUESTION: hermes's OTHER untrusted-content surface — the right-rail URL preview (apps/desktop/src/app/chat/right-rail/preview-pane.tsx) renders remote HTML in a <webview partition=\"persist:hermes-preview\" webpreferences=\"contextIsolation=yes,nodeIntegration=no,sandbox=yes\">. The guest webContents has NO setWindowOpenHandler (nothing wires did-attach / web-contents-created handlers — verified by grep). In Electron, no handler = DEFAULT ALLOW. If the CVE-2026-70608 OpenURL bypass fires in that guest, the effect escalates from 'external URL opens in the browser' to 'a real application WINDOW spawns with attacker content' (and any chrome/permission prompts that follow).","root_cause":"# RCA Report — CVE-2026-70608 R3B: preview-pane webview guest window.open escalation\n\n## Summary\n\nhermes-agent's right-rail URL preview (`apps/desktop/src/app/chat/right-rail/preview-pane.tsx:557-561`)\ncreates a `<webview partition=\"persist:hermes-preview\"\nwebpreferences=\"contextIsolation=yes,nodeIntegration=no,sandbox=yes\">` whose guest webContents\ncarries **no `setWindowOpenHandler`** (verified: no `web-contents-created` / `did-attach` /\n`new-window` wiring exists anywhere in `apps/desktop/electron/main.ts`). In Electron, no handler\nmeans **default ALLOW**. On Electron 40.10.2 (pinned in `apps/desktop/package.json`), the\nCVE-2026-70608 OpenURL bypass — a synthetic ctrl/meta-click dispatched from a sandboxed iframe\n**without `allow-popups`** — fires inside that guest and, because no handler exists to deny it,\nspawns a **real application BrowserWindow** loading attacker content. On Electron 41.10.3 the\nidentical iframe trigger is blocked (fix confirmed). This escalates the R2 finding (where the main\nwindow's `setWindowOpenHandler` capped the effect at an allowlisted external-URL open) to outcome\n(a) of the ticket: a real window spawns.\n\n## Impact\n\n- Product: hermes-agent desktop (`apps/desktop`), repo commit `e3fab0437ee50ebe511cec57b9ac36f0c2803268`\n- Affected runtime: Electron **40.10.2** (pinned in the repo); fixed in Electron **41.10.3**\n- Any page rendered in the right-rail URL preview (attacker-selected URL, e.g. a link the agent\n  was asked to preview) can, without any user gesture, spawn real Electron `BrowserWindow`s\n  loading attacker-controlled content — from the top-level guest document AND, on the vulnerable\n  Electron, from a sandboxed iframe (`sandbox=\"allow-scripts\"`, no `allow-popups`) inside it.\n- Consequences: attacker-controlled application windows inside the Hermes desktop (phishing\n  chrome, permission prompts, further drive-by surface) — beyond R2's browser-external-open cap.\n\n## Impact Parity\n\n- Disclosed/claimed maximum impact: sandbox escape (guest content escapes the preview confinement\n  into a real application window); the ticket asked to determine outcome (a) real window spawn /\n  (b) external path / (c) blocked.\n- Reproduced impact: **outcome (a)** — real `BrowserWindow` creation from the CVE bypass inside\n  the preview webview guest (main-process `browser-window-created` + guest `did-create-window` +\n  `did-finish-load` of the attacker marker URL), twice on 40.10.2; blocked twice on 41.10.3.\n- Parity: **full** for the claimed escalation question. (No renderer code execution, Node\n  integration, or permission escalation inside the spawned window was attempted or claimed here;\n  the spawned windows use Electron default webPreferences.)\n\n## Root Cause\n\n1. `preview-pane.tsx` creates the preview webview for URL targets with a persistent partition and\n   sandboxed renderer prefs, but nothing in the main process ever attaches a\n   `setWindowOpenHandler` (or `web-contents-created` policy) to that guest. Electron's default in\n   this case is to honor window-open requests by creating new windows.\n2. Electron ≤ 40.10.2 (CVE-2026-70608): the OpenURLFromTab path omits the initiating-frame\n   popup-sandbox check, so a synthetic modifier-click on an anchor inside an iframe sandboxed\n   **without `allow-popups`** is still routed as a `foreground-tab` open (the R2 primitive,\n   revalidated here inside the webview guest).\n3. Combined: the iframe sandbox bypass reaches the guest's window-open path and the guest has no\n   handler to deny it → a real window is created. On 41.10.3 the popup-sandbox check is restored,\n   so the iframe trigger is blocked before any window is created.\n\nFix upstream: Electron 41.10.3 (CVE-2026-70608 fix). Product-side defense in depth: attach a\n`setWindowOpenHandler` (deny or route through `openExternalUrl`) to every webview guest via\n`app.on('web-contents-created')` for `type === 'webview'`.\n\n## Reproduction Steps\n\n1. `bundle/repro/reproduction_steps.sh` (self-contained; idempotent).\n2. What it does:\n   - Seeds the work repo from the prepared project cache (or clones) and checks out\n     `e3fab0437ee50ebe511cec57b9ac36f0c2803268`.\n   - Installs node deps with pnpm (workspace file + `pnpm import` of the root package-lock;\n     npm itself OOMs in this container's 1.5GB cgroup) and the Python backend venv the desktop\n     spawns (`venv/bin/python -m hermes_cli.main serve`).\n   - Applies **observation-only** instrumentation to `electron/main.ts`\n     (`bundle/repro/patch_instrumentation.py`, gated on `HERMES_REPRO_INSTRUMENT_LOG`):\n     records `browser-window-created`, `web-contents-created`, guest `did-create-window`,\n     guest console, `openExternalUrl`, `shell.openExternal` (record-only stub — never executes\n     a real OS open), and the main window's `setWindowOpenHandler`. Guest behavior is NOT\n     modified.\n   - Bundles the electron main process with the product's own\n     `scripts/bundle-electron-main.mjs --dev`, and bundles the SAME renderer sources with esbuild\n     (`bundle/repro/bundle_renderer.mjs`; vite build/dev OOMs in the cgroup), served statically\n     at `http://127.0.0.1:5174` and loaded via the product's own `HERMES_DESKTOP_DEV_SERVER`\n     dev-mode entry.\n   - Runs the Playwright driver (`bundle/repro/repro.spec.ts`) under Xvfb, twice on Electron\n     40.10.2 and twice on 41.10.3. Each attempt: launches the real app with a real backend and a\n     mock OpenAI provider; sends a chat message asking to preview `http://127.0.0.1:<port>/x`; the\n     mock returns a real `open_preview` tool call which the backend executes; the driver clicks the\n     product's \"Open Preview\" affordance and, if needed, falls back to the product's own\n     localStorage restore path (`hermes.desktop.previewTabs.v2` + reload) to mount the pane. The\n     preview-pane then creates the webview pointed at the attacker page, which fires\n     `window.open` plus synthetic ctrl+meta-clicks from the top level and from a sandboxed iframe\n     (`sandbox=\"allow-scripts\"`, no `allow-popups`).\n3. Expected evidence: per-attempt `guest.did-create-window` / `browser-window-created` /\n   `did-finish-load` events for `…/marker?src=frame-click` (CVE bypass) on 40.10.2, and their\n   absence (with the iframe beacon still dispatched) on 41.10.3.\n\n## Evidence\n\nPer-attempt artifacts (two runs of the script, both `confirmed=true`):\n\n- `bundle/logs/vulnerable_attempt_{1,2}.{json,main.jsonl,log,diag.log,requests.json}` — Electron 40.10.2\n- `bundle/logs/fixed_attempt_{1,2}.{json,main.jsonl,log,diag.log,requests.json}` — Electron 41.10.3\n- `bundle/repro/runtime_manifest.json` — entrypoint, target identity, artifact hashes\n- `bundle/logs/reproduction_steps.log` — full script log\n\nKey excerpts (run 2, vulnerable attempt 1, Electron 40.10.2):\n\n```\nweb-contents-created {type:'webview'}                       # preview guest mounted\nguest-console: PRUVA src=page-loaded                        # attacker page ran in the guest\nguest-console: PRUVA src=windowopen-result&value=null       # gesture-less window.open blocked\nguest.did-create-window url=…/marker?src=top-click  disposition=foreground-tab\nweb-contents-created {type:'window'} + win.did-finish-load …/marker?src=top-click\nguest-console: PRUVA-FRAME src=frame-windowopen-result&value=null   # iframe window.open blocked\nguest.did-create-window url=…/marker?src=frame-click disposition=foreground-tab   # CVE bypass fires\nweb-contents-created {type:'window'} + win.did-finish-load …/marker?src=frame-click  # REAL window\n```\n\nFixed attempt 1 (Electron 41.10.3): the iframe still dispatches\n(`PRUVA-FRAME src=frame-click-dispatched` beacon) but **no** `did-create-window` /\nwindow for `src=frame-click` ever occurs; only the generic top-level clicks spawn windows\n(unchanged pre-existing default-allow behavior for a handler-less guest, present on both versions).\n\nOutcome matrix (both full script runs):\n\n| attempt | Electron | outcome |\n|---|---|---|\n| vulnerable #1 | 40.10.2 | A_REAL_WINDOW_SPAWNED_CVE_BYPASS (cveWindows=1, genericWindows=2) |\n| vulnerable #2 | 40.10.2 | A_REAL_WINDOW_SPAWNED_CVE_BYPASS (cveWindows=1, genericWindows=2) |\n| fixed #1 | 41.10.3 | A_REAL_WINDOW_SPAWNED_GENERIC_ONLY (cveWindows=0, genericWindows=2) |\n| fixed #2 | 41.10.3 | A_REAL_WINDOW_SPAWNED_GENERIC_ONLY (cveWindows=0, genericWindows=2) |\n\nEnvironment: Ubuntu 26.04 container, x86_64, Xvfb, UID 1000, all traffic to 127.0.0.1,\n`shell.openExternal` record-only. Chromium OS sandbox disabled (`--no-sandbox`) because the\ncontainer blocks all namespace creation (EPERM even for root); this matches the product's own e2e\nfixtures and does not affect the Blink iframe popup-sandbox under test.\n\n## Recommendations / Next Steps\n\n- Upgrade the desktop app to Electron ≥ 41.10.3 (upstream CVE-2026-70608 fix).\n- Defense in depth (works regardless of Electron version): in the main process, attach\n  `setWindowOpenHandler` to webview guests (`app.on('web-contents-created')`, `type==='webview'`)\n  that denies or routes through the allowlisted `openExternalUrl`, mirroring the main window.\n- Consider a restrictive `session.setPermissionRequestHandler` / CSP for `persist:hermes-preview`.\n- Test: an e2e spec asserting that a sandboxed iframe in a preview webview cannot create windows.\n\n## Additional Notes\n\n- Idempotency: the script was run twice consecutively end-to-end; both runs printed\n  `confirmed=true` with the same 2×2 outcome matrix. All setup steps skip when outputs exist.\n- The chat → tool-call → gateway path executed for real (mock provider issued a genuine\n  `open_preview` tool call the backend ran); the pane mount additionally used the product's own\n  persisted-store restore path when the in-test click did not land (a test-harness timing issue,\n  not a product defect).\n- The renderer is bundled by esbuild instead of vite purely because vite/rolldown exceeds the\n  container's 1.5GB memory cgroup; identical sources and product code paths are exercised.\n- Separately observed (both Electron versions, pre-existing, not the CVE): the top-level guest\n  page's synthetic ctrl+click also spawns real windows (default allow, no handler). Worth its own\n  product hardening note.\n","cve_id":"CVE-2026-70608","cwe_id":"CWE-693","source_url":"https://github.com/electron/electron/security/advisories/GHSA-9f4c-93c8-jc8g","package":{"name":"hermes-agent","ecosystem":"github","affected_versions":"electron <39.8.10, >=40.0.0-alpha.1 <41.10.3, >=42.0.0-alpha.1 <42.0.1","fixed_version":"41.10.3"},"reproduced_at":"2026-08-23T15:38:31.072995+00:00","duration_secs":16268.0,"tool_calls":541,"handoffs":2,"total_cost_usd":31.178101,"agent_costs":{"claim_matcher":0.028805,"judge":0.738733,"learning_policy":0.012926,"repro":30.327516,"support":0.070121},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.028805},"judge":{"gpt-5.5":0.738733},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.012926},"repro":{"accounts/fireworks/models/kimi-k3":30.327516},"support":{"accounts/fireworks/models/kimi-k3":0.070121}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"dependency_reachability","schema_version":2,"stages":["support","claim_contract","repro","judge"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-08-23T15:38:31.882098+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":10205,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":13672,"category":"reproduction_script"},{"path":"bundle/logs/fixed_attempt_1.diag.log","filename":"fixed_attempt_1.diag.log","size":11344,"category":"log"},{"path":"bundle/logs/fixed_attempt_1.log","filename":"fixed_attempt_1.log","size":6933,"category":"log"},{"path":"bundle/logs/fixed_attempt_1.requests.json","filename":"fixed_attempt_1.requests.json","size":6483,"category":"other"},{"path":"bundle/logs/fixed_attempt_2.diag.log","filename":"fixed_attempt_2.diag.log","size":11499,"category":"log"},{"path":"bundle/logs/fixed_attempt_2.log","filename":"fixed_attempt_2.log","size":6933,"category":"log"},{"path":"bundle/logs/fixed_attempt_2.requests.json","filename":"fixed_attempt_2.requests.json","size":6483,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":4180,"category":"log"},{"path":"bundle/logs/vulnerable_attempt_1.diag.log","filename":"vulnerable_attempt_1.diag.log","size":14453,"category":"log"},{"path":"bundle/logs/vulnerable_attempt_1.log","filename":"vulnerable_attempt_1.log","size":7594,"category":"log"},{"path":"bundle/logs/vulnerable_attempt_1.requests.json","filename":"vulnerable_attempt_1.requests.json","size":6593,"category":"other"},{"path":"bundle/logs/vulnerable_attempt_2.diag.log","filename":"vulnerable_attempt_2.diag.log","size":14509,"category":"log"},{"path":"bundle/logs/vulnerable_attempt_2.log","filename":"vulnerable_attempt_2.log","size":7443,"category":"log"},{"path":"bundle/logs/vulnerable_attempt_2.requests.json","filename":"vulnerable_attempt_2.requests.json","size":6593,"category":"other"},{"path":"bundle/repro/bundle_renderer.mjs","filename":"bundle_renderer.mjs","size":6004,"category":"other"},{"path":"bundle/repro/patch_instrumentation.py","filename":"patch_instrumentation.py","size":5791,"category":"script"},{"path":"bundle/repro/repro.spec.ts","filename":"repro.spec.ts","size":24945,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":4069,"category":"other"},{"path":"bundle/repro/serve_static.mjs","filename":"serve_static.mjs","size":1294,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1530,"category":"other"}]}