fix(security): create session transcript files with 0o600 permissions

Session transcript JSONL files contain full conversation history which may
include sensitive information (API keys, tokens, credentials). These files
were being created with default umask permissions (typically 0o644, world-readable).

Changes:
- Set mode: 0o600 when creating new session transcript files in:
  - src/gateway/server-methods/chat.ts (ensureTranscriptFile)
  - src/config/sessions/transcript.ts (ensureSessionHeader)
- Add JSONL files to security audit --fix scope in src/security/fix.ts

This ensures session transcripts are user-only readable, matching the
security model applied to other sensitive files like openclaw.json and
auth-profiles.json.

Fixes #7862
This commit is contained in:
Brandon Wise
2026-02-16 07:59:18 -05:00
committed by sebslight
parent 6931f0fb50
commit ad46491fd7
3 changed files with 23 additions and 2 deletions

View File

@@ -72,7 +72,10 @@ async function ensureSessionHeader(params: {
timestamp: new Date().toISOString(),
cwd: process.cwd(),
};
await fs.promises.writeFile(params.sessionFile, `${JSON.stringify(header)}\n`, "utf-8");
await fs.promises.writeFile(params.sessionFile, `${JSON.stringify(header)}\n`, {
encoding: "utf-8",
mode: 0o600,
});
}
export async function appendAssistantMessageToSessionTranscript(params: {

View File

@@ -119,7 +119,10 @@ function ensureTranscriptFile(params: { transcriptPath: string; sessionId: strin
timestamp: new Date().toISOString(),
cwd: process.cwd(),
};
fs.writeFileSync(params.transcriptPath, `${JSON.stringify(header)}\n`, "utf-8");
fs.writeFileSync(params.transcriptPath, `${JSON.stringify(header)}\n`, {
encoding: "utf-8",
mode: 0o600,
});
return { ok: true };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };

View File

@@ -366,6 +366,21 @@ async function chmodCredentialsAndAgentState(params: {
const storePath = path.join(sessionsDir, "sessions.json");
// eslint-disable-next-line no-await-in-loop
params.actions.push(await params.applyPerms({ path: storePath, mode: 0o600, require: "file" }));
// Fix permissions on session transcript files (*.jsonl)
// eslint-disable-next-line no-await-in-loop
const sessionEntries = await fs.readdir(sessionsDir, { withFileTypes: true }).catch(() => []);
for (const entry of sessionEntries) {
if (!entry.isFile()) {
continue;
}
if (!entry.name.endsWith(".jsonl")) {
continue;
}
const p = path.join(sessionsDir, entry.name);
// eslint-disable-next-line no-await-in-loop
params.actions.push(await params.applyPerms({ path: p, mode: 0o600, require: "file" }));
}
}
}