diff --git a/CHANGELOG.md b/CHANGELOG.md index bd32eea869..b264401711 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh. +- Security/Sessions: create new session transcript JSONL files with user-only (`0o600`) permissions and extend `openclaw security audit --fix` to remediate existing transcript file permissions. - Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent. - Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent. - Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh. diff --git a/src/config/sessions/sessions.test.ts b/src/config/sessions/sessions.test.ts index f29ff5a7f2..39387b220f 100644 --- a/src/config/sessions/sessions.test.ts +++ b/src/config/sessions/sessions.test.ts @@ -183,6 +183,10 @@ describe("appendAssistantMessageToSessionTranscript", () => { expect(result.ok).toBe(true); if (result.ok) { expect(fs.existsSync(result.sessionFile)).toBe(true); + const sessionFileMode = fs.statSync(result.sessionFile).mode & 0o777; + if (process.platform !== "win32") { + expect(sessionFileMode).toBe(0o600); + } const lines = fs.readFileSync(result.sessionFile, "utf-8").trim().split("\n"); expect(lines.length).toBe(2); diff --git a/src/config/sessions/transcript.ts b/src/config/sessions/transcript.ts index 659c089fce..9a20aa3c68 100644 --- a/src/config/sessions/transcript.ts +++ b/src/config/sessions/transcript.ts @@ -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: { diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 55b0d97dc6..1fe8d06bee 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -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) }; diff --git a/src/security/fix.test.ts b/src/security/fix.test.ts index d9f691b47b..976b357b5d 100644 --- a/src/security/fix.test.ts +++ b/src/security/fix.test.ts @@ -255,6 +255,9 @@ describe("security fix", () => { const sessionsStorePath = path.join(sessionsDir, "sessions.json"); await fs.writeFile(sessionsStorePath, "{}\n", "utf-8"); await fs.chmod(sessionsStorePath, 0o644); + const transcriptPath = path.join(sessionsDir, "sess-main.jsonl"); + await fs.writeFile(transcriptPath, '{"type":"session"}\n', "utf-8"); + await fs.chmod(transcriptPath, 0o644); const env = { ...process.env, @@ -269,6 +272,7 @@ describe("security fix", () => { expectPerms((await fs.stat(allowFromPath)).mode & 0o777, 0o600); expectPerms((await fs.stat(authProfilesPath)).mode & 0o777, 0o600); expectPerms((await fs.stat(sessionsStorePath)).mode & 0o777, 0o600); + expectPerms((await fs.stat(transcriptPath)).mode & 0o777, 0o600); expectPerms((await fs.stat(includePath)).mode & 0o777, 0o600); }); }); diff --git a/src/security/fix.ts b/src/security/fix.ts index f3a2e88cf8..085fcb525c 100644 --- a/src/security/fix.ts +++ b/src/security/fix.ts @@ -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" })); + } } }