mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-06 13:45:35 +02:00
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>
This commit is contained in:
+14
-5
@@ -893,6 +893,16 @@ async function handleCommand(body: any, tokenInfo?: TokenInfo | null): Promise<R
|
||||
|
||||
// ─── newtab with ownership for scoped tokens ──────────────
|
||||
if (command === 'newtab' && tokenInfo && tokenInfo.clientId !== 'root') {
|
||||
// Domain check for newtab URL (same as goto)
|
||||
if (args[0] && !checkDomain(tokenInfo, args[0])) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Domain not allowed by your token scope',
|
||||
hint: `Allowed domains: ${tokenInfo.domains?.join(', ') || 'none configured'}`,
|
||||
}), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
const newId = await browserManager.newTab(args[0] || undefined, tokenInfo.clientId);
|
||||
return new Response(JSON.stringify({
|
||||
tabId: newId,
|
||||
@@ -1201,11 +1211,10 @@ async function start() {
|
||||
uptime: Math.floor((Date.now() - startTime) / 1000),
|
||||
tabs: browserManager.getTabCount(),
|
||||
currentUrl: browserManager.getCurrentUrl(),
|
||||
// Auth token for extension bootstrap. Only returned when the request
|
||||
// comes from a Chrome extension (Origin: chrome-extension://...).
|
||||
// Previously served unconditionally, but that leaks the token if the
|
||||
// server is tunneled to the internet (ngrok, SSH tunnel).
|
||||
...(req.headers.get('origin')?.startsWith('chrome-extension://') ? { token: AUTH_TOKEN } : {}),
|
||||
// Auth token NOT served here. Extension reads from ~/.gstack/.auth.json
|
||||
// (written by launchHeaded at browser-manager.ts:243). Serving the token
|
||||
// on an unauthenticated endpoint is unsafe because Origin headers are
|
||||
// trivially spoofable, and ngrok exposes /health to the internet.
|
||||
chatEnabled: true,
|
||||
agent: {
|
||||
status: agentStatus,
|
||||
|
||||
@@ -169,6 +169,18 @@ export function createToken(opts: CreateTokenOptions): TokenInfo {
|
||||
expiresSeconds = 86400, // 24h default
|
||||
} = opts;
|
||||
|
||||
// Validate inputs
|
||||
const validScopes: ScopeCategory[] = ['read', 'write', 'admin', 'meta'];
|
||||
for (const s of scopes) {
|
||||
if (!validScopes.includes(s as ScopeCategory)) {
|
||||
throw new Error(`Invalid scope: ${s}. Valid: ${validScopes.join(', ')}`);
|
||||
}
|
||||
}
|
||||
if (rateLimit < 0) throw new Error('rateLimit must be >= 0');
|
||||
if (expiresSeconds !== null && expiresSeconds !== undefined && expiresSeconds < 0) {
|
||||
throw new Error('expiresSeconds must be >= 0 or null');
|
||||
}
|
||||
|
||||
const token = generateToken('gsk_sess_');
|
||||
const now = new Date();
|
||||
const expiresAt = expiresSeconds === null
|
||||
|
||||
Reference in New Issue
Block a user