Tighten H5 policy trusted routes

The classifier should not treat every /sdk path as trusted by pathname, because LAN-exposed servers already have a narrower local SDK trust boundary.

Constraint: Desktop local chat, Tauri WebView, loopback adapters, and local SDK routes must remain tokenless.

Rejected: Path-only /sdk trust | a non-local caller could reach the SDK route through a LAN-bound server.

Confidence: high

Scope-risk: narrow

Directive: Keep remote /sdk and hostile-origin loopback requests in the H5/browser bucket unless a dedicated SDK auth layer is verified.

Tested: bun test src/server/__tests__/h5-access-policy.test.ts
This commit is contained in:
程序员阿江(Relakkes) 2026-05-12 14:26:00 +08:00
parent 115e6f3b36
commit a5cc7ff0c1
2 changed files with 28 additions and 4 deletions

View File

@ -25,18 +25,40 @@ describe('h5AccessPolicy', () => {
expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, explicitAuthRequired: false })).toBe(false)
})
test('keeps internal SDK websocket routes tokenless', () => {
const request = req('http://192.168.0.20:3456/sdk/session-1')
test('keeps local internal SDK websocket routes tokenless', () => {
const request = req('http://127.0.0.1:3456/sdk/session-1')
expect(classifyH5Request(request, new URL(request.url))).toBe('internal-sdk')
expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, explicitAuthRequired: false })).toBe(false)
})
test('does not trust remote SDK websocket routes by path alone', () => {
const request = req('http://192.168.0.20:3456/sdk/session-1')
expect(classifyH5Request(request, new URL(request.url))).toBe('h5-browser')
expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, explicitAuthRequired: false })).toBe(true)
})
test('keeps adapter API routes tokenless for local integrations', () => {
const request = req('http://127.0.0.1:3456/api/adapters')
expect(classifyH5Request(request, new URL(request.url))).toBe('local-trusted')
expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, explicitAuthRequired: false })).toBe(false)
})
test('does not trust loopback adapter requests from non-local browser origins', () => {
const request = req('http://127.0.0.1:3456/api/adapters', {
headers: { Origin: 'https://blocked.example.com' },
})
expect(classifyH5Request(request, new URL(request.url))).toBe('h5-browser')
expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, explicitAuthRequired: false })).toBe(true)
})
test('keeps local desktop chat websocket routes tokenless', () => {
for (const init of [{}, { headers: { Origin: 'http://tauri.localhost' } }]) {
const request = req('http://127.0.0.1:3456/ws/session-1', init)
expect(classifyH5Request(request, new URL(request.url))).toBe('local-trusted')
expect(shouldRequireH5Token({ request, url: new URL(request.url), h5Enabled: true, explicitAuthRequired: false })).toBe(false)
}
})
test('requires H5 token for LAN browser API, proxy, and chat websocket routes when enabled', () => {
for (const pathname of ['/api/status', '/proxy/openai/v1/chat/completions', '/ws/session-1']) {
const request = req(`http://192.168.0.20:3456${pathname}`, {

View File

@ -27,11 +27,13 @@ function isLocalOrigin(origin: string | null): boolean {
}
export function classifyH5Request(request: Request, url: URL): H5RequestKind {
if (url.pathname.startsWith('/sdk/')) {
const localTrusted = isLoopbackHost(url.hostname) && isLocalOrigin(request.headers.get('Origin'))
if (url.pathname.startsWith('/sdk/') && localTrusted) {
return 'internal-sdk'
}
if (isLoopbackHost(url.hostname) && isLocalOrigin(request.headers.get('Origin'))) {
if (localTrusted) {
return 'local-trusted'
}