diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 2551aee0..a61a0080 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -206,7 +206,7 @@ async function handleBootstrapCommand( console.log(content); } else { logger.info(`Writing ${args.output}`); - await lib.safeOutputFile(args.output, content); + await lib.safeOutputFile(args.output, content, { mode: 0o600 }); } } } diff --git a/packages/create/create.js b/packages/create/create.js index 457dae2b..5980f9d2 100755 --- a/packages/create/create.js +++ b/packages/create/create.js @@ -234,7 +234,7 @@ async function migrateRename(args) { if (await pathExists(source) && !await pathExists(destination)) { await safeOutputFile(destination, JSON.stringify( renameConfig(JSON.parse(await fs.readFile(source))), null, "\t" - )); + ), { mode: 0o600 }); logger.info(`Migrated ${source} to ${destination}`); } } diff --git a/packages/ctl/src/commands_host.ts b/packages/ctl/src/commands_host.ts index fc269e3b..e2ad8283 100644 --- a/packages/ctl/src/commands_host.ts +++ b/packages/ctl/src/commands_host.ts @@ -87,7 +87,7 @@ hostCommands.add(new lib.Command({ } else { logger.info(`Writing ${args.output}`); try { - await fs.writeFile(args.output, content, { flag: "wx" }); + await fs.writeFile(args.output, content, { flag: "wx", mode: 0o600 }); } catch (err: any) { if (err.code === "EEXIST") { throw new lib.CommandError(`File ${args.output} already exists`); diff --git a/packages/lib/src/config/classes.ts b/packages/lib/src/config/classes.ts index 6ca54746..769079a0 100644 --- a/packages/lib/src/config/classes.ts +++ b/packages/lib/src/config/classes.ts @@ -468,7 +468,7 @@ export class Config< if (!this.filepath) { throw new Error("Cannot save config which has no filepath"); } - await safeOutputFile(this.filepath, JSON.stringify(this, null, "\t")); + await safeOutputFile(this.filepath, JSON.stringify(this, null, "\t"), { mode: 0o600 }); this.dirty = false; } diff --git a/packages/lib/src/file_ops.ts b/packages/lib/src/file_ops.ts index b8899406..908f57a3 100644 --- a/packages/lib/src/file_ops.ts +++ b/packages/lib/src/file_ops.ts @@ -168,6 +168,10 @@ export async function safeOutputFile( throw err; } } + if (options?.mode !== undefined) { + // writeFile only applies mode when creating, a leftover temporary keeps its old mode. + await fs.chmod(temporary, options.mode); + } await fs.rename(temporary, file); } diff --git a/test/lib/config/classes.js b/test/lib/config/classes.js index 17ed8100..c953ea13 100644 --- a/test/lib/config/classes.js +++ b/test/lib/config/classes.js @@ -395,6 +395,15 @@ describe("lib/config/classes", function() { const json = JSON.parse(await fs.readFile(filepath, "utf8")); assert.deepEqual(json, testInstance.toJSON()); }); + it("should not be readable by other users", async function() { + if (process.platform === "win32") { + this.skip(); + } + const testInstance = new TestConfig("local", { "alpha.foo": "a" }, filepath); + testInstance.set("alpha.foo", "b"); // Sets the dirty flag + await testInstance.save(); + assert.equal((await fs.stat(filepath)).mode & 0o777, 0o600); + }); it("should clear the dirty flag after saving", async function() { const testInstance = new TestConfig("local", { "alpha.foo": "a" }, filepath); testInstance.set("alpha.foo", "b"); // Sets the dirty flag diff --git a/test/lib/file_ops.js b/test/lib/file_ops.js index 036544a4..2f4993e7 100644 --- a/test/lib/file_ops.js +++ b/test/lib/file_ops.js @@ -100,6 +100,16 @@ describe("lib/file_ops", function() { await assert.rejects(fs.access(target.replace(".txt", ".tmp.txt")), "temporary was left behind"); assert.equal(await fs.readFile(target, "utf8"), "current"); }); + it("should apply mode to a leftover temporary file", async function() { + if (process.platform === "win32") { + this.skip(); + } + let target = path.join(baseDir, "safe", "mode.txt"); + await fs.writeFile(target.replace(".txt", ".tmp.txt"), "stale", { mode: 0o644 }); + await lib.safeOutputFile(target, "private", { mode: 0o600 }); + assert.equal((await fs.stat(target)).mode & 0o777, 0o600); + assert.equal(await fs.readFile(target, "utf8"), "private"); + }); it("should handle creating file in current working directory", async function() { let target = "temporary-file-made-to-test-cwd.txt"; try {