fix(security): remove deleted /sidebar-chat endpoint from tunnel allowlist

TUNNEL_PATHS is the audited tunnel attack surface — its own comment says every
addition widens it. '/sidebar-chat' stayed in the set after the endpoint was
deleted with the chat-queue path, meaning any future route matching that path
would have been silently tunnel-exposed. The set is now exactly the pair
ceremony (/connect) and the scoped command endpoint (/command), and the
dual-listener closed-set pin enforces that.

Also repairs a pre-existing red pin in dual-listener.test.ts: v1.63.0.0 made
the tunnel allowlist args-aware (canDispatchOverTunnel gained a second param)
without updating the test — red on main since then, invisible because the free
suite had no CI job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-08-14 15:59:02 -07:00
parent a646a32b41
commit b241a79ee5
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
2 changed files with 9 additions and 4 deletions

View File

@ -303,7 +303,6 @@ export function resolveConfigFromEnv(): Omit<ServerConfig, 'browserManager' | 's
const TUNNEL_PATHS = new Set<string>([
'/connect',
'/command',
'/sidebar-chat',
]);
/**

View File

@ -48,9 +48,12 @@ describe('Dual-listener surface types', () => {
});
describe('Tunnel path allowlist', () => {
test('TUNNEL_PATHS is a closed set containing exactly /connect, /command, /sidebar-chat', () => {
test('TUNNEL_PATHS is a closed set containing exactly /connect, /command', () => {
// /sidebar-chat sat in this set long after the endpoint was deleted with
// the chat-queue path — a stale entry in the audited tunnel attack
// surface. The set is exactly the pair ceremony + command endpoint.
const paths = extractSetContents(SERVER_SRC, 'TUNNEL_PATHS');
expect(paths).toEqual(new Set(['/connect', '/command', '/sidebar-chat']));
expect(paths).toEqual(new Set(['/connect', '/command']));
});
test('TUNNEL_PATHS does NOT contain bootstrap or admin paths', () => {
@ -220,7 +223,10 @@ describe('/command tunnel command allowlist', () => {
'return handleCommand(body, tokenInfo)'
);
expect(commandBlock).toContain("surface === 'tunnel'");
expect(commandBlock).toContain('canDispatchOverTunnel(body?.command)');
// v1.63.0.0 made the allowlist args-aware (canDispatchOverTunnel gained a
// second param for --out denial); this pin was stale from then until the
// free suite got a CI job.
expect(commandBlock).toContain('canDispatchOverTunnel(body?.command, body?.args)');
expect(commandBlock).toContain('disallowed_command');
expect(commandBlock).toContain('is not allowed over the tunnel surface');
expect(commandBlock).toContain('status: 403');