Files
gstack/browse/test/server-auth.test.ts
T
Garry Tan cd85bdc196 fix: CSO security fixes — token leak, domain bypass, input validation
1. Remove root token from /health endpoint entirely (CSO #1 CRITICAL).
   Origin header is spoofable. Extension reads from ~/.gstack/.auth.json.
2. Add domain check for newtab URL (CSO #5). Previously only goto was
   checked, allowing domain-restricted agents to bypass via newtab.
3. Validate scope values, rateLimit, expiresSeconds in createToken()
   (CSO #4). Rejects invalid scopes and negative values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 23:37:36 -07:00

81 lines
3.8 KiB
TypeScript

/**
* Server auth security tests — verify security remediation in server.ts
*
* Tests are source-level: they read server.ts and verify that auth checks,
* CORS restrictions, and token removal are correctly in place.
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
const SERVER_SRC = fs.readFileSync(path.join(import.meta.dir, '../src/server.ts'), 'utf-8');
// Helper: extract a block of source between two markers
function sliceBetween(source: string, startMarker: string, endMarker: string): string {
const startIdx = source.indexOf(startMarker);
if (startIdx === -1) throw new Error(`Marker not found: ${startMarker}`);
const endIdx = source.indexOf(endMarker, startIdx + startMarker.length);
if (endIdx === -1) throw new Error(`End marker not found: ${endMarker}`);
return source.slice(startIdx, endIdx);
}
describe('Server auth security', () => {
// Test 1: /health must NOT serve the auth token (CSO finding #1 — spoofable Origin)
// Extension reads token from ~/.gstack/.auth.json instead.
test('/health does NOT serve auth token', () => {
const healthBlock = sliceBetween(SERVER_SRC, "url.pathname === '/health'", "url.pathname === '/connect'");
// Token must not appear in the health response construction
expect(healthBlock).not.toContain('token: AUTH_TOKEN');
expect(healthBlock).not.toContain('token: AUTH');
// Should have a comment explaining why
expect(healthBlock).toContain('NOT served here');
});
// Test 1b: /health must not use chrome-extension Origin gating (spoofable)
test('/health does not use spoofable Origin header for token gating', () => {
const healthBlock = sliceBetween(SERVER_SRC, "url.pathname === '/health'", "url.pathname === '/connect'");
expect(healthBlock).not.toContain("chrome-extension://') ? { token");
});
// Test 1c: newtab must check domain restrictions (CSO finding #5)
test('newtab enforces domain restrictions', () => {
const newtabBlock = sliceBetween(SERVER_SRC, "newtab with ownership for scoped tokens", "Block mutation commands while watching");
expect(newtabBlock).toContain('checkDomain');
expect(newtabBlock).toContain('Domain not allowed');
});
// Test 2: /refs endpoint requires auth via validateAuth
test('/refs endpoint requires authentication', () => {
const refsBlock = sliceBetween(SERVER_SRC, "url.pathname === '/refs'", "url.pathname === '/activity/stream'");
expect(refsBlock).toContain('validateAuth');
});
// Test 3: /refs has no wildcard CORS header
test('/refs has no wildcard CORS header', () => {
const refsBlock = sliceBetween(SERVER_SRC, "url.pathname === '/refs'", "url.pathname === '/activity/stream'");
expect(refsBlock).not.toContain("'*'");
});
// Test 4: /activity/history requires auth via validateAuth
test('/activity/history requires authentication', () => {
const historyBlock = sliceBetween(SERVER_SRC, "url.pathname === '/activity/history'", 'Sidebar endpoints');
expect(historyBlock).toContain('validateAuth');
});
// Test 5: /activity/history has no wildcard CORS header
test('/activity/history has no wildcard CORS header', () => {
const historyBlock = sliceBetween(SERVER_SRC, "url.pathname === '/activity/history'", 'Sidebar endpoints');
expect(historyBlock).not.toContain("'*'");
});
// Test 6: /activity/stream requires auth (inline Bearer or ?token= check)
test('/activity/stream requires authentication with inline token check', () => {
const streamBlock = sliceBetween(SERVER_SRC, "url.pathname === '/activity/stream'", "url.pathname === '/activity/history'");
expect(streamBlock).toContain('validateAuth');
expect(streamBlock).toContain('AUTH_TOKEN');
// Should not have wildcard CORS for the SSE stream
expect(streamBlock).not.toContain("Access-Control-Allow-Origin': '*'");
});
});