Security Considerations
Living document. A running tracker of the threat model, mitigations, and open issues for the guest↔host RPC surface. Seeded from the adoption plan; expand it every time we touch the guest↔host surface (new capability, transport change, trust-model change, a webview setting). See also the Guest↔Host RPC architecture doc.
Status legend: [done] mitigated in code · [partial] partially addressed · [open] tracked risk, not yet addressed.
Threat model
Borrowed from VS Code webviews — Bierner: defense in depth. Assume webview content can be compromised (an injection in a trusted page, or an outright hostile remote page). A compromised page must not read arbitrary files, reach the host DOM/process, or act on another page’s behalf. Design so that even a successful in-page attacker is boxed in.
The tranquil browser loads content over both file:// (trusted local mockups) and http(s):// (arbitrary remote), and the <webview> runs with nodeIntegration: on. So any RPC layer must
assume hostile content and default-deny.
From the VS Code webview learnings (mapped to our Electron <webview>)
- Isolate content from the host. Scripts in a page must not touch host DOM or host process. We
rely on
contextIsolation: on+ a minimalcontextBridge(window.electron). Audit that surface — it is the entire attack surface from page → host. [partial] nodeIntegration: onis a red flag. The tranquil-browser<webview>(tranquil-browser/lib/tranquil-browser-view.js) enables Node integration for all pages, including remotehttp(s)://. Track: can we scope Node access to trusted origins, or disable it and provide needed APIs via capabilities? This is the single biggest open risk. [open]- Isolate webviews from each other. One page must not observe/alter another’s state or pane. Our
per-session
{ item, webview, url, subscriptions }context enforces pane scoping (a capability matches onlyctx.item); verify no shared mutable registry leaks across sessions. [partial] - Controlled local-resource loading. A compromised page should not read arbitrary disk paths.
Track whether our
file://webviews can be constrained (custom protocol / path allowlist) the way VS Code restrictsvscode-resource:. [open] - Unique origin / no cross-webview state bleed. VS Code serves webviews from isolated origins so
localStorage/cookies/service-workers don’t leak between them. Track our exposure: do multiple
file://webviews share an origin/partition? (webviewpartitionattribute.) [open] - Scripts off by default / opt-in. VS Code disables scripts unless the content opts in. Our analog: the guest RPC runtime is injected only for trusted origins — untrusted pages get no runtime at all. [done]
- Intercept navigation.
httpslinks /new-windowshould route deliberately, not silently navigate the pane or host. Verify the existingnew-window/will-navigatehandling can’t be abused to load hostile content into a trusted context. [open] - CSP + nonces. VS Code sets a restrictive CSP;
srcdociframes inherit parent CSP and can’t bypass it. Our mockups currently ship no CSP — track adding a strict CSP (nonce’d scripts) to trusted mockups so an injection can’t pull in remote code. [open] - Sandbox semantics.
sandbox="allow-scripts"withoutallow-same-originforces a unique origin. Track whether our<webview>usage should adopt equivalent hardening. [open]
From this project (RPC / capability-specific)
- Trust classifier is security-critical.
isTrusted(lib/trust.js) must default-deny; trust onlyfile://under explicitly registered roots; never trusthttp(s)://. A bug here grants capabilities to hostile pages. No wildcard/prefix footguns — matching is exact path or a real child path segment, so…/mockupsnever matches…/mockups-evil. Unit-tested (13/13 cases). Needs review on every change. [done] - Untrusted = zero RPC surface. No guest runtime injected, no session, empty HostApi. Even so,
the host must treat any inbound
tranquil:rpcmessage as untrusted input (a remote page can callsendToHostdirectly) — only trusted sessions carry capabilities; messages from untrusted webviews get no capability-bearing session. [done] for injection; [partial] — the host binds a session only afterisTrusted(url), but should also validate the sender frame (below). - Capabilities are the authority boundary. A page can only do what its handed
HostApiallows. Keep each capability narrow and least-privilege; never expose a generic “run/eval/dispatch arbitrary command” capability to pages. Review every new capability as a privilege grant. [done] (current caps:ping,notify,paneControls.register— all narrow.) - No host-side eval of page-provided strings. Actions execute in the page (Cap’n Web function
stubs), never by the host
executeJavaScript-ing page-supplied code. [done] for the RPC path. Pre-existing risk to track: the bp-client preloadeval()s context-menufnstrings (tranquil-browser/resources/bp-client.js) — flagged as pre-existing. [open] - Only pass data + stubs across RPC. Never hand a page a broad host object; expose scoped
RpcTargets.PaneControlsCap.registerreturnstrue, never the raw Disposable. Be deliberate about what a returned/queued stub can reach. [done] - Lifecycle / DoS. Stubs and registrations must dispose on page close/reload. Each session
carries a
CompositeDisposable; retained action stubs are.dup()ed and disposed on teardown, and pane-control registrations are removed with the session (reloads don’t accumulate). Watch for a page spammingregisterto exhaust host resources — consider rate/size limits on registrations. [partial] - Supply chain. Pin
capnweb(currently0.9.0, exact) and the esbuild toolchain; review updates. The committeddist/bundles are trusted code injected into pages — treat bundle integrity as security-relevant. [partial] - Injection integrity. The guest bundle is injected via
executeJavaScriptinto trusted pages; ensure the bundle source can’t be tampered with at rest and is only injected after the trust check (host.jsreadsdist/tranquil-rpc-guest.jsand injects only whenisTrusted(url)). [done] for ordering; [open] for at-rest integrity.
From Electron security guidance (general checklist + issues in our code)
Source: Electron Security guidance. These are broader tranquil-browser issues surfaced while designing the RPC trust model; tracked here because the RPC layer’s safety depends on them.
- Overly broad
contextBridge.window.electronexposessend/sendToHost/sendSync/invoke/receiveon arbitrary channel names (bp-client.js) — a compromised page can reach anyipcMainhandler. Move toward an allowlist of channels (and expose scoped methods, not a generic IPC pipe). High priority; our RPC channel (tranquil:rpc) should be the model. [open] - Validate IPC sender.
ipcMain/host handlers must checkevent.senderFrame(origin/URL) and not trust a message just because it arrived on a channel. The host must bind a capability-bearing session only to a verified trusted frame. [open] remotemodule. The preload imports and uses Electronremote(bp-client.js,remote.getCurrentWebContents()) — deprecated and a known escalation vector (removed in modern Electron). Track removal. [open]- Auto-granting permissions. The webview allows every permission request
(
tranquil-browser-view.js,e.request.allow()) — camera, mic, geolocation, etc. granted unconditionally to any page, including remote. Add a permission handler that denies by default and gates by origin/trust. [open] - Enable the renderer
sandbox. Our<webview>runs unsandboxed withnodeIntegration: on. Electron recommendssandbox: true+nodeIntegration: falsefor anything loading untrusted content. Track scoping Node/sandbox by trust. [open] allowfileaccessfromfiles: "on"relaxes file access fromfile://origins (tranquil-browser-view.js) — review whether it’s needed; it widens what a compromisedfile://page can read. [open]- Never enable
webSecurity: false/allowRunningInsecureContent.disablewebsecurityis currently commented out — keep it off. [done] - Restrict navigation & window creation. Gate
will-navigateand new-window/setWindowOpenHandlerto known destinations; the currentnew-windowhandler opense.urlviaatom.workspace.open— ensure a hostile page can’t drive navigation into a trusted context or spawn unexpected windows. [open] shell.openExternalwith untrusted URLs can launch external handlers — validate/scheme-check any URL a page can cause to be opened externally. [open]- No
allowpopupson the<webview>, noenableBlinkFeatures/experimental features, and scope thewebviewTagcapability to where it’s needed. [partial] - Only load secure remote content (HTTPS); block/verify mixed and HTTP content. [open]
- Stay on a current, patched Electron. Track the version and apply security releases; the
<webview>tag itself is a legacy surface Electron discourages — note as a longer-term migration. [open] - Keep the preload minimal & audited. It has full Node; every symbol it exposes is attack
surface. The bp-client preload also
eval()s injected strings (noted above) — audit all of it. [open]
Open-issues summary (priority order)
nodeIntegration: onfor all pages incl. remote — scope Node access by trust. [open]- Over-broad
contextBridge(arbitrary channels) + no IPC-sender validation. [open] - Auto-granted permissions for every page. [open]
- Deprecated
remotemodule in the preload. [open] - No CSP on trusted mockups;
allowfileaccessfromfiles; navigation/window-open gating. [open] - Rate/size limits on
paneControls.register;dist/at-rest integrity. [partial]
Sources
- Matt Bierner, “What I’ve learned so far while bringing VS Code’s Webviews to the web” (July 1, 2019) — blog.mattbierner.com/vscode-webview-web-learnings. Source of the webview threat model and isolation/CSP/sandbox learnings above. ↩
- Electron Security guidance — electronjs.org/docs/latest/tutorial/security.
Source of the checklist items above (sandbox, context isolation, IPC sender validation, permission
handlers, navigation restriction, avoiding
remote, patched versions). ↩ - Cap’n Web (Cloudflare, MIT) — https://github.com/cloudflare/capnweb, npm
capnwebhttps://www.npmjs.com/package/capnweb. Source of the RPC model,RpcTarget,RpcTransport, custom-transportRpcSession, and promise-pipelining details.