security: close capability, CSP and auth-bridge holes
capabilities/default.json - Drop every `store:*` grant. `@tauri-apps/plugin-store` has no caller in `app/src`, and the plugin's `resolve_store_path` is a `PathBuf::push` against AppData — `push` discards the base for an absolute path, so the grant was an arbitrary host read/write from the webview. - Replace `opener:default` with a scoped `opener:allow-open-url` (http/https only). That drops `reveal_item_in_dir`, which the plugin does not scope-check and nothing here calls, and the unused mailto:/tel: scope. - Record the unscopable `drag:allow-start-drag` residual risk in `description`. tauri.conf.json - Add `form-action 'none'`, `base-uri 'none'`, `object-src 'none'`. `form-action` has no `default-src` fallback, so an injected auto-submitting form was unblocked even though `script-src 'self'` blocks XSS. - Remove the dead `asset:` / `https://asset.localhost` img-src and `data:` font-src grants; `blob:` stays (the file viewer uses it). auth_bridge - The reserved-port set covered only this project's mappings and the two browser-view ranges. It now also covers the gateway, STT and web-terminal host ports (configured value and shipped default, read off the settings models) and every other project's published host port. A container binding container-loopback 4000 / 9876 / 7681 while those services were stopped had that port mirrored onto the host, unauthenticated, within one poll. - Gate the host listener on fetch metadata: refuse a request that is a cross-site sub-resource, allow navigations (the OAuth redirect) and anything without `Sec-Fetch-*`. Non-HTTP connections are classified from their first line and forwarded verbatim. Residual risk is spelled out in the module docs. - Bound the forwards: max concurrent connections per port, a first-byte deadline enforced before any `docker exec` is created, and an idle timeout. browser_view/mod.rs - `pick_viewer_port` reads procfs with `/usr/bin/cat`, not a bare `cat` the container can shim via its writable PATH entry. - Treat port choice as check-then-bind: walk to the next free candidate when the viewer does not come up, instead of failing the start. BrowserTab.tsx - Sandbox the viewer iframe. Container-controlled content could `top.location` the app's webview away. `allow-top-navigation*` and `allow-popups-to-escape-sandbox` are deliberately absent. HelpDialog.tsx - Escape the quote characters in the entity pass and escape captured attribute values. `href="$2"` with `$2` = `[^)]+` let remote GitHub markdown close the attribute and open another, in a document rendered with `dangerouslySetInnerHTML`. web_terminal/terminal.html - SRI hashes plus `crossorigin` on the three jsdelivr bundles and the stylesheet, and a CSP for the page — it is served 0.0.0.0 behind a permissive CORS layer and nothing else gives it one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
This commit is contained in:
@@ -457,6 +457,35 @@ export default function BrowserTab({ project, active }: Props) {
|
||||
// host-side gate checks before anything reaches the container.
|
||||
src={status.url ?? undefined}
|
||||
title={`Playwright browser view for ${project.name}`}
|
||||
// What is framed here is served by a process inside the container,
|
||||
// which is the untrusted side of this app. Unsandboxed, it could
|
||||
// simply set `top.location` and navigate the *app's* webview
|
||||
// somewhere of its choosing — the frame is cross-origin, so it cannot
|
||||
// read the app, but steering the whole window is not something a
|
||||
// viewer pane should be able to do.
|
||||
//
|
||||
// The allowances are what the Playwright dashboard actually needs and
|
||||
// no more:
|
||||
// allow-scripts — it is an application, not a document.
|
||||
// allow-same-origin — it must reach its own WebSocket and assets,
|
||||
// and the host-side gate recognises the pane's
|
||||
// own sub-resource requests by their
|
||||
// `Origin`/`Referer`; an opaque origin would
|
||||
// send `null` and be refused. This does not
|
||||
// grant access to *this* app: 127.0.0.1:4782x
|
||||
// is a different origin from the app's.
|
||||
// allow-forms/-modals/-downloads/-popups — dashboard UI affordances
|
||||
// (trace download, confirm dialogs, opening a
|
||||
// page in a new window).
|
||||
//
|
||||
// Deliberately absent, and the point of the attribute:
|
||||
// `allow-top-navigation`, `allow-top-navigation-by-user-activation`
|
||||
// and `allow-popups-to-escape-sandbox`. Do not add them.
|
||||
//
|
||||
// No `referrerPolicy` either: the gate in `browser_view/proxy.rs`
|
||||
// reads the token out of a same-origin `Referer`, so stripping it
|
||||
// would break the pane.
|
||||
sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-downloads allow-popups"
|
||||
className="flex-1 min-h-0 w-full border-0 bg-[var(--bg-primary)]"
|
||||
/>
|
||||
) : live ? (
|
||||
|
||||
Reference in New Issue
Block a user