diff --git a/src/config/io.ts b/src/config/io.ts index 2b542bba755..fba17f253aa 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -164,6 +164,32 @@ function hashConfigRaw(raw: string | null): string { .digest("hex"); } +async function tightenStateDirPermissionsIfNeeded(params: { + configPath: string; + env: NodeJS.ProcessEnv; + homedir: () => string; + fsModule: typeof fs; +}): Promise { + if (process.platform === "win32") { + return; + } + const stateDir = resolveStateDir(params.env, params.homedir); + const configDir = path.dirname(params.configPath); + if (path.resolve(configDir) !== path.resolve(stateDir)) { + return; + } + try { + const stat = await params.fsModule.promises.stat(configDir); + const mode = stat.mode & 0o777; + if ((mode & 0o077) === 0) { + return; + } + await params.fsModule.promises.chmod(configDir, 0o700); + } catch { + // Best-effort hardening only; callers still need the config write to proceed. + } +} + function formatConfigValidationFailure(pathLabel: string, issueMessage: string): string { const match = issueMessage.match(OPEN_DM_POLICY_ALLOW_FROM_RE); const policyPath = match?.groups?.policyPath?.trim(); @@ -1136,6 +1162,12 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { const dir = path.dirname(configPath); await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 }); + await tightenStateDirPermissionsIfNeeded({ + configPath, + env: deps.env, + homedir: deps.homedir, + fsModule: deps.fs, + }); const outputConfigBase = envRefMap && changedPaths ? (restoreEnvRefsFromMap(cfgToWrite, "", envRefMap, changedPaths) as OpenClawConfig) diff --git a/src/config/io.write-config.test.ts b/src/config/io.write-config.test.ts index 6b73b9fbd30..68709725d83 100644 --- a/src/config/io.write-config.test.ts +++ b/src/config/io.write-config.test.ts @@ -142,6 +142,28 @@ describe("config io write", () => { }); }); + it.runIf(process.platform !== "win32")( + "tightens world-writable state dir when writing the default config", + async () => { + await withSuiteHome(async (home) => { + const stateDir = path.join(home, ".openclaw"); + await fs.mkdir(stateDir, { recursive: true, mode: 0o777 }); + await fs.chmod(stateDir, 0o777); + + const io = createConfigIO({ + env: {} as NodeJS.ProcessEnv, + homedir: () => home, + logger: silentLogger, + }); + + await io.writeConfigFile({ gateway: { mode: "local" } }); + + const stat = await fs.stat(stateDir); + expect(stat.mode & 0o777).toBe(0o700); + }); + }, + ); + it('shows actionable guidance for dmPolicy="open" without wildcard allowFrom', async () => { await withSuiteHome(async (home) => { const io = createConfigIO({