fix(security): wire appendSecureFile at the four real log-append sites

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 <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-08-14 19:14:58 -07:00
parent 5099637f95
commit dd66e4cda2
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
2 changed files with 6 additions and 6 deletions

View File

@ -13,7 +13,7 @@
* All writes are best-effort audit failures never cause command failures. * All writes are best-effort audit failures never cause command failures.
*/ */
import * as fs from 'fs'; import { appendSecureFile } from './file-permissions';
export interface AuditEntry { export interface AuditEntry {
ts: string; ts: string;
@ -62,7 +62,7 @@ export function writeAuditEntry(entry: AuditEntry): void {
if (entry.aliasOf) record.aliasOf = entry.aliasOf; if (entry.aliasOf) record.aliasOf = entry.aliasOf;
if (truncatedError) record.error = truncatedError; if (truncatedError) record.error = truncatedError;
fs.appendFileSync(auditPath, JSON.stringify(record) + '\n'); appendSecureFile(auditPath, JSON.stringify(record) + '\n');
} catch { } catch {
// Audit write failures are silent — never block command execution // Audit write failures are silent — never block command execution
} }

View File

@ -26,7 +26,7 @@ import {
} from './content-security'; } from './content-security';
import { getStatus as getSecurityStatus } from './security'; import { getStatus as getSecurityStatus } from './security';
import { isSidecarAvailable, scanWithSidecar } from './security-sidecar-client'; 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 { handleSnapshot, SNAPSHOT_FLAGS } from './snapshot';
import { import {
initRegistry, validateToken as validateScopedToken, checkScope, checkDomain, initRegistry, validateToken as validateScopedToken, checkScope, checkDomain,
@ -586,7 +586,7 @@ async function flushBuffers() {
const lines = entries.map(e => const lines = entries.map(e =>
`[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}` `[${new Date(e.timestamp).toISOString()}] [${e.level}] ${e.text}`
).join('\n') + '\n'; ).join('\n') + '\n';
fs.appendFileSync(CONSOLE_LOG_PATH, lines); appendSecureFile(CONSOLE_LOG_PATH, lines);
lastConsoleFlushed = consoleBuffer.totalAdded; lastConsoleFlushed = consoleBuffer.totalAdded;
} }
@ -597,7 +597,7 @@ async function flushBuffers() {
const lines = entries.map(e => const lines = entries.map(e =>
`[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url}${e.status || 'pending'} (${e.duration || '?'}ms, ${e.size || '?'}B)` `[${new Date(e.timestamp).toISOString()}] ${e.method} ${e.url}${e.status || 'pending'} (${e.duration || '?'}ms, ${e.size || '?'}B)`
).join('\n') + '\n'; ).join('\n') + '\n';
fs.appendFileSync(NETWORK_LOG_PATH, lines); appendSecureFile(NETWORK_LOG_PATH, lines);
lastNetworkFlushed = networkBuffer.totalAdded; lastNetworkFlushed = networkBuffer.totalAdded;
} }
@ -608,7 +608,7 @@ async function flushBuffers() {
const lines = entries.map(e => const lines = entries.map(e =>
`[${new Date(e.timestamp).toISOString()}] [${e.type}] "${e.message}" → ${e.action}${e.response ? ` "${e.response}"` : ''}` `[${new Date(e.timestamp).toISOString()}] [${e.type}] "${e.message}" → ${e.action}${e.response ? ` "${e.response}"` : ''}`
).join('\n') + '\n'; ).join('\n') + '\n';
fs.appendFileSync(DIALOG_LOG_PATH, lines); appendSecureFile(DIALOG_LOG_PATH, lines);
lastDialogFlushed = dialogBuffer.totalAdded; lastDialogFlushed = dialogBuffer.totalAdded;
} }
} catch (err: any) { } catch (err: any) {