From dd66e4cda2160826fe945357af5cdcef6f4ab2f9 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 19:14:58 -0700 Subject: [PATCH] fix(security): wire appendSecureFile at the four real log-append sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit file-permissions.ts carries a 24-line rationale for why POSIX mode bits are insufficient on Windows and implements appendSecureFile (0600 at create, Windows ACL on first write only) — but its single caller was the dead logAttempt, while the four REAL page-content log writers (console/network/ dialog logs in server.ts, the command audit log) used raw fs.appendFileSync with no mode. Page-content-derived logs now get owner-only permissions from birth on every platform. Verified before wiring: mode applies atomically at create via appendFileSync {mode}, and the ACL pass runs only on first write — no per-append subprocess cost on the hot console-log path. Co-Authored-By: Claude Fable 5 --- browse/src/audit.ts | 4 ++-- browse/src/server.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/browse/src/audit.ts b/browse/src/audit.ts index b6e546388..f93b47fb0 100644 --- a/browse/src/audit.ts +++ b/browse/src/audit.ts @@ -13,7 +13,7 @@ * All writes are best-effort — audit failures never cause command failures. */ -import * as fs from 'fs'; +import { appendSecureFile } from './file-permissions'; export interface AuditEntry { ts: string; @@ -62,7 +62,7 @@ export function writeAuditEntry(entry: AuditEntry): void { if (entry.aliasOf) record.aliasOf = entry.aliasOf; if (truncatedError) record.error = truncatedError; - fs.appendFileSync(auditPath, JSON.stringify(record) + '\n'); + appendSecureFile(auditPath, JSON.stringify(record) + '\n'); } catch { // Audit write failures are silent — never block command execution } diff --git a/browse/src/server.ts b/browse/src/server.ts index f4194bf0c..466c6f212 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -26,7 +26,7 @@ import { } from './content-security'; import { getStatus as getSecurityStatus } from './security'; import { isSidecarAvailable, scanWithSidecar } from './security-sidecar-client'; -import { writeSecureFile, mkdirSecure } from './file-permissions'; +import { writeSecureFile, mkdirSecure, appendSecureFile } from './file-permissions'; import { handleSnapshot, SNAPSHOT_FLAGS } from './snapshot'; import { initRegistry, validateToken as validateScopedToken, checkScope, checkDomain, @@ -586,7 +586,7 @@ async function flushBuffers() { const lines = entries.map(e => `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}` ).join('\n') + '\n'; - fs.appendFileSync(CONSOLE_LOG_PATH, lines); + appendSecureFile(CONSOLE_LOG_PATH, lines); lastConsoleFlushed = consoleBuffer.totalAdded; } @@ -597,7 +597,7 @@ async function flushBuffers() { const lines = entries.map(e => `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url} → ${e.status || 'pending'} (${e.duration || '?'}ms, ${e.size || '?'}B)` ).join('\n') + '\n'; - fs.appendFileSync(NETWORK_LOG_PATH, lines); + appendSecureFile(NETWORK_LOG_PATH, lines); lastNetworkFlushed = networkBuffer.totalAdded; } @@ -608,7 +608,7 @@ async function flushBuffers() { const lines = entries.map(e => `[${new Date(e.timestamp).toISOString()}] [${e.type}] "${e.message}" → ${e.action}${e.response ? ` "${e.response}"` : ''}` ).join('\n') + '\n'; - fs.appendFileSync(DIALOG_LOG_PATH, lines); + appendSecureFile(DIALOG_LOG_PATH, lines); lastDialogFlushed = dialogBuffer.totalAdded; } } catch (err: any) {