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

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 962f497d24
Co-authored-by: brandonwise <21148772+brandonwise@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
This commit is contained in:
brandonwise
2026-02-16 08:33:40 -05:00
committed by GitHub
parent 6931f0fb50
commit 095d522099
6 changed files with 32 additions and 2 deletions

View File

@@ -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.

View File

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

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

@@ -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);
});
});

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" }));
}
}
}