- html viewer:.html/.htm 沙箱 iframe 预览(相对资源经 /sidebar/html 路径编码路由加载,CSP sandbox + 信任围栏 + cwd 边界),预览/编辑切换 - browser tab:+ 菜单多开(browser:<n>),地址栏后退/前进/刷新/在浏览器中打开(15px 图标),URL 与标题按会话持久化(patchTab + nextBrowser) - 安全:iframe 一律不透明源沙箱(无 allow-same-origin/top-navigation、no-referrer、权限策略全禁、禁 srcdoc);地址栏仅 http(s)、拒绝 localhost 等本机地址;界面实时沙箱状态(关闭红色警示)+ 临时解锁/恢复 - 设置:htmlViewerNoSandbox / htmlViewerDefaultUnsafe / browserNoSandbox / browserInterceptLinks 四个声明式二级设置(默认安全) - 外链拦截:聊天/界面 http(s) 外链默认在侧边栏打开(openTab seed.url),Ctrl/Cmd 点击放行 - 嵌入拒绝面板:browser.probe 路由探测 X-Frame-Options/frame-ancestors,被拒站点显示原因 + 在浏览器中打开/仍然加载 - 设置页可滚动;内置 7 tab + 9 viewer;新增设计文档;268 个测试全绿
80 lines
3.5 KiB
TypeScript
80 lines
3.5 KiB
TypeScript
/**
|
|
* Browser address-bar policy tests: only http(s) URLs may be navigated,
|
|
* loopback addresses and the GUI's own origin are refused outright. The
|
|
* iframe sandbox (opaque origin) is the primary security boundary; this
|
|
* policy is the address-bar gate on top of it.
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import { isLoopbackHostname, normalizeBrowserUrl } from '../src/client/browser.ts'
|
|
|
|
const SELF = 'http://127.0.0.1:3080'
|
|
|
|
describe('normalizeBrowserUrl', () => {
|
|
it('normalizes a bare domain to https', () => {
|
|
expect(normalizeBrowserUrl('example.com', SELF)).toEqual({ kind: 'ok', url: 'https://example.com/' })
|
|
})
|
|
|
|
it('normalizes a host with a port to https', () => {
|
|
expect(normalizeBrowserUrl('example.com:8080/path', SELF)).toEqual({ kind: 'ok', url: 'https://example.com:8080/path' })
|
|
})
|
|
|
|
it('keeps an explicit http:// scheme', () => {
|
|
expect(normalizeBrowserUrl('http://example.com/a?b=1', SELF)).toEqual({ kind: 'ok', url: 'http://example.com/a?b=1' })
|
|
})
|
|
|
|
it('accepts a non-loopback IP literal', () => {
|
|
expect(normalizeBrowserUrl('https://8.8.8.8/dns', SELF)?.kind).toBe('ok')
|
|
})
|
|
|
|
it('refuses non-http(s) schemes', () => {
|
|
expect(normalizeBrowserUrl('javascript:alert(1)', SELF)).toEqual({ kind: 'blocked', reason: 'scheme' })
|
|
expect(normalizeBrowserUrl('data:text/html,<b>x</b>', SELF)).toEqual({ kind: 'blocked', reason: 'scheme' })
|
|
expect(normalizeBrowserUrl('file:///etc/passwd', SELF)).toEqual({ kind: 'blocked', reason: 'scheme' })
|
|
expect(normalizeBrowserUrl('about:blank', SELF)).toEqual({ kind: 'blocked', reason: 'scheme' })
|
|
})
|
|
|
|
it('refuses loopback hostnames in every spelling', () => {
|
|
for (const input of [
|
|
'http://localhost/', 'https://localhost:3080/', 'http://LOCALHOST/',
|
|
'http://127.0.0.1/', 'http://127.255.255.255/',
|
|
'http://[::1]/', 'http://0.0.0.0/',
|
|
]) {
|
|
expect(normalizeBrowserUrl(input, SELF), input).toEqual({ kind: 'blocked', reason: 'loopback' })
|
|
}
|
|
})
|
|
|
|
it('allows the GUI\'s own origin (the sandbox keeps it opaque like any site)', () => {
|
|
// The user may browse the GUI itself in the sidebar; its host is
|
|
// loopback, so the self check must win BEFORE the loopback gate.
|
|
expect(normalizeBrowserUrl('http://127.0.0.1:3080/sidebar', SELF)).toEqual({
|
|
kind: 'ok', url: 'http://127.0.0.1:3080/sidebar',
|
|
})
|
|
expect(normalizeBrowserUrl('http://127.0.0.1:3080/', SELF)).toEqual({
|
|
kind: 'ok', url: 'http://127.0.0.1:3080/',
|
|
})
|
|
// A different port of the same loopback host is NOT the GUI origin and
|
|
// stays blocked.
|
|
expect(normalizeBrowserUrl('http://127.0.0.1:9999/', SELF)).toEqual({ kind: 'blocked', reason: 'loopback' })
|
|
})
|
|
|
|
it('reports invalid input', () => {
|
|
expect(normalizeBrowserUrl('', SELF)).toEqual({ kind: 'invalid' })
|
|
expect(normalizeBrowserUrl(' ', SELF)).toEqual({ kind: 'invalid' })
|
|
expect(normalizeBrowserUrl('ht tp://x', SELF)).toEqual({ kind: 'invalid' })
|
|
})
|
|
})
|
|
|
|
describe('isLoopbackHostname', () => {
|
|
it('flags localhost, IPv6 loopback, and the 127/8 block', () => {
|
|
expect(isLoopbackHostname('localhost')).toBe(true)
|
|
expect(isLoopbackHostname('::1')).toBe(true)
|
|
expect(isLoopbackHostname('[::1]')).toBe(true)
|
|
expect(isLoopbackHostname('127.0.0.1')).toBe(true)
|
|
expect(isLoopbackHostname('127.8.9.10')).toBe(true)
|
|
expect(isLoopbackHostname('0.0.0.0')).toBe(true)
|
|
expect(isLoopbackHostname('example.com')).toBe(false)
|
|
expect(isLoopbackHostname('128.0.0.1')).toBe(false)
|
|
expect(isLoopbackHostname('192.168.1.1')).toBe(false)
|
|
})
|
|
})
|