Files
apes-sidebar/tests/browser.spec.ts
T
Menghuan1918 302c20109f feat: 内置 HTML 预览器 + 沙箱浏览器 tab(v0.5.0)
- 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 个测试全绿
2026-08-11 19:28:48 +08:00

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)
})
})