feat(browser): loopback allowlist with same-origin for local dev servers

Add a side-card preference 'browserAllowedLoopback' (comma-separated
host or host:port entries) that lets the sidebar browser navigate to
explicitly trusted loopback addresses. Bare hosts (localhost,
127.0.0.1) match every port; host:port entries match exactly.

Allowlisted local addresses additionally get the 'allow-same-origin'
sandbox token in the iframe: local dev servers (Vite etc.) need a real
origin for their module/HMR/fetch pipeline and would otherwise render
blank in the opaque-origin sandbox. The page stays cross-origin to the
GUI and to every other site.

The server-side browser.probe route mirrors the same allowlist so
probed local servers are not rejected as 'local addresses are not
probed'.
This commit is contained in:
trinhthiminh650-tech committed 2026-08-15 01:37:39 +08:00
1 parent 6f092a6b1a
commit 8014255541
8 files changed
+129 -7

No files matched your search

+29 -2
View File
@@ -176,6 +176,26 @@ export interface SidebarSettingsFace {
update(patch: Record<string, unknown>, expectedRevision?: number): Promise<{ value?: unknown; revision?: number }>
}
/**
* Parse the browser tab's `browserAllowedLoopback` allowlist into a matcher
* over host:port (same contract as the client-side helper in
* src/client/browser.ts — kept in sync). Bare hosts (`localhost`,
* `127.0.0.1`) match every port; `host:port` entries match exactly.
*/
function parseLoopbackAllowlist(allowlist: string): (host: string, port: string) => boolean {
const entries = allowlist.split(',').map(entry => entry.trim().toLowerCase()).filter(entry => entry !== '')
const exact = new Set(entries)
const hosts = new Set<string>()
for (const entry of entries) {
if (!entry.includes(':')) hosts.add(entry.replace(/^\[|\]$/g, ''))
}
return (host, port) => {
const key = `${host}:${port}`
if (exact.has(key) || exact.has(host)) return true
return port !== '' && hosts.has(host)
}
}
/** Build the API method table bound to the plugin context, pty manager, agent pty registry, and resolved config. */
function buildApi(
ctx: Context,
@@ -380,9 +400,16 @@ function buildApi(
throw new SidebarError('bad-request', 'only http/https urls can be probed', 400)
}
// Mirror the browser tab's address-bar policy: loopback stays unreachable
// from the sidebar, so probing it would leak nothing the tab could use.
// from the sidebar (unless the user allowlisted it), so probing it would
// leak nothing the tab could use.
if (isLoopbackHostname(parsed.hostname)) {
throw new SidebarError('bad-request', 'local addresses are not probed', 400)
const prefs = getSettings()?.get()?.value as SidebarPrefs | undefined
const allowlist = typeof prefs?.browserAllowedLoopback === 'string' ? prefs.browserAllowedLoopback : ''
const allowed = allowlist.trim() !== ''
&& parseLoopbackAllowlist(allowlist)(parsed.hostname, parsed.port)
if (!allowed) {
throw new SidebarError('bad-request', 'local addresses are not probed', 400)
}
}
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 8000)