mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-05 13:15:24 +02:00
cd85bdc196
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>
81 lines
3.8 KiB
TypeScript
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': '*'");
|
|
});
|
|
});
|