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:
1 parent
6f092a6b1a
commit
8014255541
8 files changed
+129
-7
No files matched your search
+29
-2
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user