From c7fc5e5ca9bfdecc710c3ae425b85a270d5c59a1 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 4 Apr 2026 21:22:12 -0700 Subject: [PATCH] fix: harden file/directory permissions to owner-only (C5+H9+M9+M10) Add mode 0o700 to all mkdirSync calls for state/session directories. Add mode 0o600 to all writeFileSync calls for session.json, chat.jsonl, and log files. Add umask 077 to setup script. Prevents auth tokens, chat history, and browser logs from being world-readable on multi-user systems. Closes C5, H9, M9, M10 from security audit #783. Co-Authored-By: Claude Opus 4.6 (1M context) --- browse/src/config.ts | 2 +- browse/src/server.ts | 20 ++++++++++---------- browse/src/sidebar-agent.ts | 8 ++++---- setup | 1 + 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/browse/src/config.ts b/browse/src/config.ts index 04f166433..498c083b5 100644 --- a/browse/src/config.ts +++ b/browse/src/config.ts @@ -79,7 +79,7 @@ export function resolveConfig( */ export function ensureStateDir(config: BrowseConfig): void { try { - fs.mkdirSync(config.stateDir, { recursive: true }); + fs.mkdirSync(config.stateDir, { recursive: true, mode: 0o700 }); } catch (err: any) { if (err.code === 'EACCES') { throw new Error(`Cannot create state directory ${config.stateDir}: permission denied`); diff --git a/browse/src/server.ts b/browse/src/server.ts index ceff414c9..0348489a6 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -398,10 +398,10 @@ function createSession(): SidebarSession { lastActiveAt: new Date().toISOString(), }; const sessionDir = path.join(SESSIONS_DIR, id); - fs.mkdirSync(sessionDir, { recursive: true }); - fs.writeFileSync(path.join(sessionDir, 'session.json'), JSON.stringify(session, null, 2)); - fs.writeFileSync(path.join(sessionDir, 'chat.jsonl'), ''); - fs.writeFileSync(path.join(SESSIONS_DIR, 'active.json'), JSON.stringify({ id })); + fs.mkdirSync(sessionDir, { recursive: true, mode: 0o700 }); + fs.writeFileSync(path.join(sessionDir, 'session.json'), JSON.stringify(session, null, 2), { mode: 0o600 }); + fs.writeFileSync(path.join(sessionDir, 'chat.jsonl'), '', { mode: 0o600 }); + fs.writeFileSync(path.join(SESSIONS_DIR, 'active.json'), JSON.stringify({ id }), { mode: 0o600 }); chatBuffer = []; chatNextId = 0; return session; @@ -411,7 +411,7 @@ function saveSession(): void { if (!sidebarSession) return; sidebarSession.lastActiveAt = new Date().toISOString(); const sessionFile = path.join(SESSIONS_DIR, sidebarSession.id, 'session.json'); - try { fs.writeFileSync(sessionFile, JSON.stringify(sidebarSession, null, 2)); } catch (err: any) { + try { fs.writeFileSync(sessionFile, JSON.stringify(sidebarSession, null, 2), { mode: 0o600 }); } catch (err: any) { console.error('[browse] Failed to save session:', err.message); } } @@ -558,7 +558,7 @@ function spawnClaude(userMessage: string, extensionUrl?: string | null, forTabId tabId: agentTabId, }); try { - fs.mkdirSync(gstackDir, { recursive: true }); + fs.mkdirSync(gstackDir, { recursive: true, mode: 0o700 }); fs.appendFileSync(agentQueue, entry + '\n'); } catch (err: any) { addChatEntry({ ts: new Date().toISOString(), role: 'agent', type: 'agent_error', error: `Failed to queue: ${err.message}` }); @@ -614,7 +614,7 @@ function startAgentHealthCheck(): void { // Initialize session on startup function initSidebarSession(): void { - fs.mkdirSync(SESSIONS_DIR, { recursive: true }); + fs.mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o700 }); sidebarSession = loadSession(); if (!sidebarSession) { sidebarSession = createSession(); @@ -1331,7 +1331,7 @@ async function start() { chatBuffer = []; chatNextId = 0; if (sidebarSession) { - try { fs.writeFileSync(path.join(SESSIONS_DIR, sidebarSession.id, 'chat.jsonl'), ''); } catch (err: any) { + try { fs.writeFileSync(path.join(SESSIONS_DIR, sidebarSession.id, 'chat.jsonl'), '', { mode: 0o600 }); } catch (err: any) { console.error('[browse] Failed to clear chat file:', err.message); } } @@ -1693,8 +1693,8 @@ start().catch((err) => { // stderr because the server is launched with detached: true, stdio: 'ignore'. try { const errorLogPath = path.join(config.stateDir, 'browse-startup-error.log'); - fs.mkdirSync(config.stateDir, { recursive: true }); - fs.writeFileSync(errorLogPath, `${new Date().toISOString()} ${err.message}\n${err.stack || ''}\n`); + fs.mkdirSync(config.stateDir, { recursive: true, mode: 0o700 }); + fs.writeFileSync(errorLogPath, `${new Date().toISOString()} ${err.message}\n${err.stack || ''}\n`, { mode: 0o600 }); } catch { // stateDir may not exist — nothing more we can do } diff --git a/browse/src/sidebar-agent.ts b/browse/src/sidebar-agent.ts index 2f13b2433..67fe27505 100644 --- a/browse/src/sidebar-agent.ts +++ b/browse/src/sidebar-agent.ts @@ -49,7 +49,7 @@ function writeToInbox(message: string, pageUrl?: string, sessionId?: string): vo } const inboxDir = path.join(gitRoot, '.context', 'sidebar-inbox'); - fs.mkdirSync(inboxDir, { recursive: true }); + fs.mkdirSync(inboxDir, { recursive: true, mode: 0o700 }); const now = new Date(); const timestamp = now.toISOString().replace(/:/g, '-'); @@ -65,7 +65,7 @@ function writeToInbox(message: string, pageUrl?: string, sessionId?: string): vo sidebarSessionId: sessionId || 'unknown', }; - fs.writeFileSync(tmpFile, JSON.stringify(inboxMessage, null, 2)); + fs.writeFileSync(tmpFile, JSON.stringify(inboxMessage, null, 2), { mode: 0o600 }); fs.renameSync(tmpFile, finalFile); console.log(`[sidebar-agent] Wrote inbox message: ${filename}`); } @@ -413,8 +413,8 @@ function pollKillFile(): void { async function main() { const dir = path.dirname(QUEUE); - fs.mkdirSync(dir, { recursive: true }); - if (!fs.existsSync(QUEUE)) fs.writeFileSync(QUEUE, ''); + fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); + if (!fs.existsSync(QUEUE)) fs.writeFileSync(QUEUE, '', { mode: 0o600 }); lastLine = countLines(); await refreshToken(); diff --git a/setup b/setup index 7e74c64f2..9136bef08 100755 --- a/setup +++ b/setup @@ -1,6 +1,7 @@ #!/usr/bin/env bash # gstack setup — build browser binary + register skills with Claude Code / Codex set -e +umask 077 # Restrict new files to owner-only (0o600 files, 0o700 dirs) if ! command -v bun >/dev/null 2>&1; then echo "Error: bun is required but not installed." >&2