From 5c49a0357187e5caddbd3ae6660aa77c0a2ad64a Mon Sep 17 00:00:00 2001 From: Revopush Date: Thu, 15 Oct 2026 21:06:27 +0300 Subject: [PATCH] Save the access-key config file readable only by its owner Co-Authored-By: Claude Opus 5.5 --- script/auth/connection.ts | 19 +++++++++++++-- test/connection.ts | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 test/connection.ts diff --git a/script/auth/connection.ts b/script/auth/connection.ts index 2aafc85..fbab57c 100644 --- a/script/auth/connection.ts +++ b/script/auth/connection.ts @@ -7,7 +7,10 @@ import AccountManager = require("../management-sdk"); const packageJson = require("../../../package.json"); -const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".revopush.config"); +export const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".revopush.config"); + +// The file holds the access key, so only its owner may read it +const OWNER_ONLY = 0o600; const CLI_HEADERS: Headers = { "X-CodePush-CLI-Version": packageJson.version, @@ -39,6 +42,7 @@ export function deserializeConnectionInfo(): ILoginConnectionInfo { const savedConnection: string = fs.readFileSync(configFilePath, { encoding: "utf8", }); + restrictToOwner(); let connectionInfo: ILegacyLoginConnectionInfo | ILoginConnectionInfo = JSON.parse(savedConnection); // If the connection info is in the legacy format, convert it to the modern format @@ -54,6 +58,15 @@ export function deserializeConnectionInfo(): ILoginConnectionInfo { } } +// Also fixes files saved by older versions, which other users could read +function restrictToOwner(): void { + try { + fs.chmodSync(configFilePath, OWNER_ONLY); + } catch { + // The file doesn't exist yet, or belongs to another user + } +} + export function serializeConnectionInfo(accessKey: string, preserveAccessKeyOnLogout: boolean, customServerUrl?: string): void { const connectionInfo: ILoginConnectionInfo = { accessKey: accessKey, @@ -64,7 +77,9 @@ export function serializeConnectionInfo(accessKey: string, preserveAccessKeyOnLo } const json: string = JSON.stringify(connectionInfo); - fs.writeFileSync(configFilePath, json, { encoding: "utf8" }); + // `mode` applies only when the file is created; an existing file is restricted before the key is written to it + restrictToOwner(); + fs.writeFileSync(configFilePath, json, { encoding: "utf8", mode: OWNER_ONLY }); log( `\r\nSuccessfully logged-in. Your session file was written to ${chalk.cyan(configFilePath)}. You can run the ${chalk.cyan( diff --git a/test/connection.ts b/test/connection.ts new file mode 100644 index 0000000..ec15825 --- /dev/null +++ b/test/connection.ts @@ -0,0 +1,51 @@ +import { strict as assert } from "assert"; +import * as fs from "fs"; +import * as sinon from "sinon"; +import { + configFilePath as configFile, + deleteConnectionInfoCache, + deserializeConnectionInfo, + serializeConnectionInfo, +} from "../script/auth/connection"; +import * as terminal from "../script/ui/terminal"; + +// File modes don't apply on Windows +const describeOnPosix = process.platform === "win32" ? describe.skip : describe; + +describeOnPosix("connection", () => { + // test/setup.ts points HOME at a temp folder + const mode = () => fs.statSync(configFile).mode & 0o777; + // Set the mode explicitly: the `mode` option is masked by the umask, which may already strip the read bits + const writeReadableByOthers = (content: string) => { + fs.writeFileSync(configFile, content); + fs.chmodSync(configFile, 0o644); + }; + + beforeEach(() => sinon.stub(terminal, "log")); + afterEach(() => { + sinon.restore(); + deleteConnectionInfoCache(/* printMessage */ false); + }); + + it("saves the access key readable only by the owner", () => { + serializeConnectionInfo("key", false); + + assert.equal(mode(), 0o600); + assert.equal(deserializeConnectionInfo().accessKey, "key"); + }); + + it("restricts a config file that other users could read before saving the key", () => { + writeReadableByOthers("{}"); + + serializeConnectionInfo("key", false); + + assert.equal(mode(), 0o600); + }); + + it("restricts a config file saved by an older version when reading it", () => { + writeReadableByOthers(JSON.stringify({ accessKey: "key" })); + + assert.equal(deserializeConnectionInfo().accessKey, "key"); + assert.equal(mode(), 0o600); + }); +});