Skip to content

Commit 91879ac

Browse files
authored
Harden config backup restore permissions (#77488)
* Harden config backup restore permissions * docs(changelog): credit config restore mode hardening Adds the user-facing Unreleased Fixes entry for the suspicious-read backup restore chmod hardening shipped in this PR.
1 parent a387068 commit 91879ac

3 files changed

Lines changed: 37 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ Docs: https://docs.openclaw.ai
325325
- Codex/app-server: stabilize transcript mirror dedupe across re-mirrored turns so reordered snapshots no longer drop reasoning entries or duplicate the assistant reply. Refs #77012. (#77046) Thanks @openperf.
326326
- Agents/auth-profiles: do not record request-shape (`format`) rejections as auth-profile health failures, so a single per-session transcript-shape error (such as a prefill-strict 400 "conversation must end with a user message") no longer triggers a profile-wide cooldown that blocks every other healthy session sharing the same auth profile. Refs #77228. (#77280) Thanks @openperf.
327327
- CLI/update: stop dev-channel source updates immediately when `git fetch` fails, so tag conflicts cannot keep preflight, rebase, or build steps running against stale refs while the Gateway is still on the old runtime. (#77845) Thanks @obviyus.
328+
- Config/recovery: chmod restored `openclaw.json` back to owner-only (`0600`) after suspicious-read backup recovery on POSIX hosts, so a previously world-readable config mode cannot persist into a freshly restored credential-bearing config. (#77488) Thanks @drobison00.
328329

329330
## 2026.5.3-1
330331

src/config/io.observe-recovery.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,22 @@ describe("config observe recovery", () => {
275275
});
276276
});
277277

278+
it("hardens async backup restores to owner-only config permissions", async () => {
279+
if (process.platform === "win32") {
280+
return;
281+
}
282+
await withSuiteHome(async (home) => {
283+
const { deps, configPath } = makeDeps(home);
284+
await seedConfigBackup(configPath, recoverableTelegramConfig);
285+
await writeClobberedUpdateChannel(configPath);
286+
await fsp.chmod(configPath, 0o644);
287+
288+
await recoverClobberedUpdateChannel({ deps, configPath });
289+
290+
expect((await fsp.stat(configPath)).mode & 0o777).toBe(0o600);
291+
});
292+
});
293+
278294
it("auto-restores after a large size drop against last-good config", async () => {
279295
await withSuiteHome(async (home) => {
280296
const { deps, configPath, auditPath } = makeDeps(home);
@@ -456,6 +472,22 @@ describe("config observe recovery", () => {
456472
});
457473
});
458474

475+
it("hardens sync backup restores to owner-only config permissions", async () => {
476+
if (process.platform === "win32") {
477+
return;
478+
}
479+
await withSuiteHome(async (home) => {
480+
const { deps, configPath } = makeDeps(home);
481+
await seedConfigBackup(configPath, recoverableTelegramConfig);
482+
await writeClobberedUpdateChannel(configPath);
483+
await fsp.chmod(configPath, 0o644);
484+
485+
recoverClobberedUpdateChannelSync({ deps, configPath });
486+
487+
expect((await fsp.stat(configPath)).mode & 0o777).toBe(0o600);
488+
});
489+
});
490+
459491
it("logs async health-state write failures", async () => {
460492
await withSuiteHome(async (home) => {
461493
const { deps, configPath, warn } = makeDeps(home);

src/config/io.observe-recovery.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,7 @@ export async function maybeRecoverSuspiciousConfigRead(params: {
640640
let restoreError: unknown;
641641
try {
642642
await params.deps.fs.promises.copyFile(backupPath, params.configPath);
643+
await params.deps.fs.promises.chmod?.(params.configPath, 0o600).catch(() => {});
643644
restoredFromBackup = true;
644645
} catch (error) {
645646
restoreError = error;
@@ -747,6 +748,9 @@ export function maybeRecoverSuspiciousConfigReadSync(params: {
747748
let restoreError: unknown;
748749
try {
749750
params.deps.fs.copyFileSync(backupPath, params.configPath);
751+
try {
752+
params.deps.fs.chmodSync?.(params.configPath, 0o600);
753+
} catch {}
750754
restoredFromBackup = true;
751755
} catch (error) {
752756
restoreError = error;

0 commit comments

Comments
 (0)