This commit is contained in:
Autonomy AI, LLC 2026-07-14 19:16:59 -07:00 committed by GitHub
commit 498c0d2837
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 83 additions and 3 deletions

View File

@ -1665,8 +1665,41 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
// `authToken` (the cfg-derived value) explicitly.
const browserManager = cfgBrowserManager;
// CORS wrapper for the Side Panel. The extension's sidepanel.html runs in a
// chrome-extension:// origin and fetches /health, /command, /sse-session,
// /refs, /activity/stream, /pty-session, etc. /health is localhost-only, so
// reflecting Origin back is safe — we only do it for chrome-extension://
// requests, not arbitrary websites.
const withCors = (
handler: (req: Request) => Promise<Response>,
) => async (req: Request): Promise<Response> => {
const origin = req.headers.get('origin');
const isExt = origin?.startsWith('chrome-extension://');
if (isExt && req.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': origin!,
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400',
},
});
}
const resp = await handler(req);
if (!isExt) return resp;
const headers = new Headers(resp.headers);
headers.set('Access-Control-Allow-Origin', origin!);
headers.set('Access-Control-Allow-Credentials', 'true');
return new Response(resp.body, {
status: resp.status,
statusText: resp.statusText,
headers,
});
};
const makeFetchHandler = (surface: Surface) => async (req: Request): Promise<Response> => {
const makeFetchHandler = (surface: Surface) => withCors(async (req: Request): Promise<Response> => {
const url = new URL(req.url);
// ─── Tunnel surface filter (runs before any route dispatch) ──
@ -2846,7 +2879,7 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle {
}
return new Response('Not found', { status: 404 });
};
});
return {
fetchLocal: makeFetchHandler('local'),

View File

@ -562,9 +562,18 @@ function buildServer() {
if (url.pathname === '/claude-available' && req.method === 'GET') {
writeClaudeAvailable();
const found = findClaude();
const origin = req.headers.get('origin');
return new Response(JSON.stringify({ available: !!found, path: found }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...(origin?.startsWith('chrome-extension://')
? {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
}
: {}),
},
});
}

View File

@ -61,6 +61,28 @@ describe('Server auth security', () => {
expect(refsBlock).not.toContain("'*'");
});
// Test 3a: withCors middleware exists and reflects only chrome-extension origins.
// Background: the Side Panel runs in a chrome-extension:// origin and fetches
// /health, /command, /refs, /activity/stream, etc. with credentials:'include'.
// Without CORS reflection, every fetch fails and the panel never reaches the
// "connected" state. Reflecting * would be unsafe; reflecting only
// chrome-extension:// keeps arbitrary websites blocked.
test('withCors middleware reflects only chrome-extension origins', () => {
expect(SERVER_SRC).toContain('const withCors = ');
expect(SERVER_SRC).toContain("origin?.startsWith('chrome-extension://')");
// Must include Allow-Credentials because the panel uses credentials:'include'.
expect(SERVER_SRC).toContain("'Access-Control-Allow-Credentials'");
// Must NOT wildcard.
expect(SERVER_SRC).not.toContain("'Access-Control-Allow-Origin': '*'");
});
// Test 3b: makeFetchHandler is wrapped with withCors so every route gets
// CORS handling for free (otherwise a future endpoint added without
// remembering CORS would silently break the Side Panel again).
test('makeFetchHandler is wrapped with withCors', () => {
expect(SERVER_SRC).toMatch(/makeFetchHandler[^=]*=[^=]*withCors\(/);
});
// Test 4: /activity/history requires auth via validateAuth
test('/activity/history requires authentication', () => {
const historyBlock = sliceBetween(SERVER_SRC, "url.pathname === '/activity/history'", 'Batch endpoint');

View File

@ -221,3 +221,19 @@ describe('Source-level guard: server.ts /pty-session route', () => {
expect(route).toContain('buildPtySetCookie');
});
});
describe('Source-level guard: terminal-agent /claude-available CORS', () => {
// Background: sidepanel-terminal.js fetches /claude-available with
// credentials:'include' to decide whether to show the "install Claude
// Code" bootstrap card. Without CORS reflection, the fetch throws and
// the card stays stuck on "not found" even when claude is installed.
test('/claude-available reflects Allow-Origin + Allow-Credentials for chrome-extension origins', () => {
const route = AGENT_SRC.slice(AGENT_SRC.indexOf("url.pathname === '/claude-available'"));
const responseBlock = route.slice(0, route.indexOf("// /ws"));
expect(responseBlock).toContain("origin?.startsWith('chrome-extension://')");
expect(responseBlock).toContain("'Access-Control-Allow-Origin'");
expect(responseBlock).toContain("'Access-Control-Allow-Credentials'");
// Must NOT wildcard.
expect(responseBlock).not.toContain("'Access-Control-Allow-Origin': '*'");
});
});