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.
*/
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
}

View File

@ -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) {