From ce0ec92a5a3d2bf2a985e746fbfca1f2f214cf9b Mon Sep 17 00:00:00 2001 From: raystorm Date: Sun, 12 Jul 2026 07:45:05 +0800 Subject: [PATCH] fix: harden daemon discovery file trust --- dist/daemon/discovery.js | 35 +++++++++++++++++- dist/daemon/discovery.js.map | 2 +- plugins/retinue/dist/daemon/discovery.js | 35 +++++++++++++++++- plugins/retinue/dist/daemon/discovery.js.map | 2 +- src/daemon/discovery.ts | 38 +++++++++++++++++++- tests/daemon-discovery.test.ts | 34 +++++++++++++++++- 6 files changed, 140 insertions(+), 6 deletions(-) diff --git a/dist/daemon/discovery.js b/dist/daemon/discovery.js index 7830d01..97c68f8 100644 --- a/dist/daemon/discovery.js +++ b/dist/daemon/discovery.js @@ -5,21 +5,54 @@ import path from "node:path"; import { getDaemonDiscoveryPath } from "../core/paths.js"; export async function writeDaemonDiscovery(stateDir, value) { const filePath = getDaemonDiscoveryPath(stateDir); - await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 }); const tempPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`; await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); await fs.rename(tempPath, filePath); } export async function readDaemonDiscovery(stateDir) { const filePath = getDaemonDiscoveryPath(stateDir); + await assertDiscoveryPathSecure(filePath); const parsed = JSON.parse(await fs.readFile(filePath, "utf8")); return normalizeDiscovery(parsed); } export function readDaemonDiscoverySync(stateDir) { const filePath = getDaemonDiscoveryPath(stateDir); + assertDiscoveryPathSecureSync(filePath); const parsed = JSON.parse(fsSync.readFileSync(filePath, "utf8")); return normalizeDiscovery(parsed); } +async function assertDiscoveryPathSecure(filePath) { + if (process.platform === "win32") { + return; + } + const [fileStat, dirStat] = await Promise.all([fs.lstat(filePath), fs.lstat(path.dirname(filePath))]); + assertSecureDiscoveryStats(fileStat, dirStat); +} +function assertDiscoveryPathSecureSync(filePath) { + if (process.platform === "win32") { + return; + } + assertSecureDiscoveryStats(fsSync.lstatSync(filePath), fsSync.lstatSync(path.dirname(filePath))); +} +function assertSecureDiscoveryStats(fileStat, dirStat) { + const uid = typeof process.getuid === "function" ? process.getuid() : undefined; + if (!dirStat.isDirectory()) { + throw new Error("Invalid daemon discovery: state directory is not a directory"); + } + if (!fileStat.isFile()) { + throw new Error("Invalid daemon discovery: discovery file is not a regular file"); + } + if (uid !== undefined && (dirStat.uid !== uid || fileStat.uid !== uid)) { + throw new Error("Invalid daemon discovery: discovery path is not owned by the current user"); + } + if ((dirStat.mode & 0o022) !== 0) { + throw new Error("Invalid daemon discovery: state directory is writable by other users"); + } + if ((fileStat.mode & 0o077) !== 0) { + throw new Error("Invalid daemon discovery: discovery file is readable or writable by other users"); + } +} function normalizeDiscovery(parsed) { const discovery = validateDiscovery(parsed); if (!isPidAlive(discovery.pid)) { diff --git a/dist/daemon/discovery.js.map b/dist/daemon/discovery.js.map index ecd053f..0e78a6b 100644 --- a/dist/daemon/discovery.js.map +++ b/dist/daemon/discovery.js.map @@ -1 +1 @@ -{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/daemon/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAU1D,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAgB,EAAE,KAAsB;IACjF,MAAM,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,GAAG,QAAQ,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,UAAU,EAAE,MAAM,CAAC;IAChF,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACvG,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IACxD,MAAM,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAA6B,CAAC;IAC3F,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,MAAM,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAA6B,CAAC;IAC7F,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAgC;IAC1D,MAAM,SAAS,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,+BAA+B,SAAS,CAAC,GAAG,eAAe,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,iBAAiB,CAAC,KAA+B;IACxD,MAAM,GAAG,GAAG,oBAAoB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5C,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,0BAA0B,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAC5C,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO;QACL,GAAG;QACH,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,sBAAsB,CAAC,KAAK,CAAC,KAAK,CAAC;KAC3C,CAAC;AACJ,CAAC;AAED,SAAS,sBAAsB,CAAC,KAAc;IAC5C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAc;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC;AAED,SAAS,0BAA0B,CAAC,KAAa;IAC/C,IAAI,CAAC,+CAA+C,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,KAAK,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC7B,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACb,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"} \ No newline at end of file +{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/daemon/discovery.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/plugins/retinue/dist/daemon/discovery.js b/plugins/retinue/dist/daemon/discovery.js index 7830d01..97c68f8 100644 --- a/plugins/retinue/dist/daemon/discovery.js +++ b/plugins/retinue/dist/daemon/discovery.js @@ -5,21 +5,54 @@ import path from "node:path"; import { getDaemonDiscoveryPath } from "../core/paths.js"; export async function writeDaemonDiscovery(stateDir, value) { const filePath = getDaemonDiscoveryPath(stateDir); - await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 }); const tempPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`; await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); await fs.rename(tempPath, filePath); } export async function readDaemonDiscovery(stateDir) { const filePath = getDaemonDiscoveryPath(stateDir); + await assertDiscoveryPathSecure(filePath); const parsed = JSON.parse(await fs.readFile(filePath, "utf8")); return normalizeDiscovery(parsed); } export function readDaemonDiscoverySync(stateDir) { const filePath = getDaemonDiscoveryPath(stateDir); + assertDiscoveryPathSecureSync(filePath); const parsed = JSON.parse(fsSync.readFileSync(filePath, "utf8")); return normalizeDiscovery(parsed); } +async function assertDiscoveryPathSecure(filePath) { + if (process.platform === "win32") { + return; + } + const [fileStat, dirStat] = await Promise.all([fs.lstat(filePath), fs.lstat(path.dirname(filePath))]); + assertSecureDiscoveryStats(fileStat, dirStat); +} +function assertDiscoveryPathSecureSync(filePath) { + if (process.platform === "win32") { + return; + } + assertSecureDiscoveryStats(fsSync.lstatSync(filePath), fsSync.lstatSync(path.dirname(filePath))); +} +function assertSecureDiscoveryStats(fileStat, dirStat) { + const uid = typeof process.getuid === "function" ? process.getuid() : undefined; + if (!dirStat.isDirectory()) { + throw new Error("Invalid daemon discovery: state directory is not a directory"); + } + if (!fileStat.isFile()) { + throw new Error("Invalid daemon discovery: discovery file is not a regular file"); + } + if (uid !== undefined && (dirStat.uid !== uid || fileStat.uid !== uid)) { + throw new Error("Invalid daemon discovery: discovery path is not owned by the current user"); + } + if ((dirStat.mode & 0o022) !== 0) { + throw new Error("Invalid daemon discovery: state directory is writable by other users"); + } + if ((fileStat.mode & 0o077) !== 0) { + throw new Error("Invalid daemon discovery: discovery file is readable or writable by other users"); + } +} function normalizeDiscovery(parsed) { const discovery = validateDiscovery(parsed); if (!isPidAlive(discovery.pid)) { diff --git a/plugins/retinue/dist/daemon/discovery.js.map b/plugins/retinue/dist/daemon/discovery.js.map index ecd053f..0e78a6b 100644 --- a/plugins/retinue/dist/daemon/discovery.js.map +++ b/plugins/retinue/dist/daemon/discovery.js.map @@ -1 +1 @@ -{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/daemon/discovery.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/daemon/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,MAAM,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAU1D,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAgB,EAAE,KAAsB;IACjF,MAAM,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,GAAG,QAAQ,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,UAAU,EAAE,MAAM,CAAC;IAChF,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACvG,MAAM,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IACxD,MAAM,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,yBAAyB,CAAC,QAAQ,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAA6B,CAAC;IAC3F,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,MAAM,QAAQ,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IAClD,6BAA6B,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAA6B,CAAC;IAC7F,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,KAAK,UAAU,yBAAyB,CAAC,QAAgB;IACvD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO;IACT,CAAC;IACD,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACtG,0BAA0B,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,6BAA6B,CAAC,QAAgB;IACrD,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,OAAO;IACT,CAAC;IACD,0BAA0B,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AACnG,CAAC;AAED,SAAS,0BAA0B,CAAC,QAAsB,EAAE,OAAqB;IAC/E,MAAM,GAAG,GAAG,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAChF,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;IAClF,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,gEAAgE,CAAC,CAAC;IACpF,CAAC;IACD,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,KAAK,GAAG,IAAI,QAAQ,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC/F,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,sEAAsE,CAAC,CAAC;IAC1F,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QAClC,MAAM,IAAI,KAAK,CAAC,iFAAiF,CAAC,CAAC;IACrG,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,MAAgC;IAC1D,MAAM,SAAS,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,+BAA+B,SAAS,CAAC,GAAG,eAAe,CAAC,CAAC;IAC/E,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,iBAAiB,CAAC,KAA+B;IACxD,MAAM,GAAG,GAAG,oBAAoB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5C,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,0BAA0B,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAC5C,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IACD,OAAO;QACL,GAAG;QACH,GAAG,EAAE,KAAK,CAAC,GAAG;QACd,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,KAAK,EAAE,sBAAsB,CAAC,KAAK,CAAC,KAAK,CAAC;KAC3C,CAAC;AACJ,CAAC;AAED,SAAS,sBAAsB,CAAC,KAAc;IAC5C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,oBAAoB,CAAC,KAAc;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC;AAED,SAAS,0BAA0B,CAAC,KAAa;IAC/C,IAAI,CAAC,+CAA+C,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;IAC/B,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,KAAK,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC7B,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACb,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/src/daemon/discovery.ts b/src/daemon/discovery.ts index 80cdeea..ff23813 100644 --- a/src/daemon/discovery.ts +++ b/src/daemon/discovery.ts @@ -14,7 +14,7 @@ export interface DaemonDiscovery { export async function writeDaemonDiscovery(stateDir: string, value: DaemonDiscovery): Promise { const filePath = getDaemonDiscoveryPath(stateDir); - await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 }); const tempPath = `${filePath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`; await fs.writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 }); await fs.rename(tempPath, filePath); @@ -22,16 +22,52 @@ export async function writeDaemonDiscovery(stateDir: string, value: DaemonDiscov export async function readDaemonDiscovery(stateDir: string): Promise { const filePath = getDaemonDiscoveryPath(stateDir); + await assertDiscoveryPathSecure(filePath); const parsed = JSON.parse(await fs.readFile(filePath, "utf8")) as Partial; return normalizeDiscovery(parsed); } export function readDaemonDiscoverySync(stateDir: string): DaemonDiscovery { const filePath = getDaemonDiscoveryPath(stateDir); + assertDiscoveryPathSecureSync(filePath); const parsed = JSON.parse(fsSync.readFileSync(filePath, "utf8")) as Partial; return normalizeDiscovery(parsed); } +async function assertDiscoveryPathSecure(filePath: string): Promise { + if (process.platform === "win32") { + return; + } + const [fileStat, dirStat] = await Promise.all([fs.lstat(filePath), fs.lstat(path.dirname(filePath))]); + assertSecureDiscoveryStats(fileStat, dirStat); +} + +function assertDiscoveryPathSecureSync(filePath: string): void { + if (process.platform === "win32") { + return; + } + assertSecureDiscoveryStats(fsSync.lstatSync(filePath), fsSync.lstatSync(path.dirname(filePath))); +} + +function assertSecureDiscoveryStats(fileStat: fsSync.Stats, dirStat: fsSync.Stats): void { + const uid = typeof process.getuid === "function" ? process.getuid() : undefined; + if (!dirStat.isDirectory()) { + throw new Error("Invalid daemon discovery: state directory is not a directory"); + } + if (!fileStat.isFile()) { + throw new Error("Invalid daemon discovery: discovery file is not a regular file"); + } + if (uid !== undefined && (dirStat.uid !== uid || fileStat.uid !== uid)) { + throw new Error("Invalid daemon discovery: discovery path is not owned by the current user"); + } + if ((dirStat.mode & 0o022) !== 0) { + throw new Error("Invalid daemon discovery: state directory is writable by other users"); + } + if ((fileStat.mode & 0o077) !== 0) { + throw new Error("Invalid daemon discovery: discovery file is readable or writable by other users"); + } +} + function normalizeDiscovery(parsed: Partial): DaemonDiscovery { const discovery = validateDiscovery(parsed); if (!isPidAlive(discovery.pid)) { diff --git a/tests/daemon-discovery.test.ts b/tests/daemon-discovery.test.ts index 031efd2..6d245b3 100644 --- a/tests/daemon-discovery.test.ts +++ b/tests/daemon-discovery.test.ts @@ -64,7 +64,8 @@ describe("daemon discovery", () => { it("rejects discovery metadata with missing or empty url", async () => { await fs.writeFile( getDaemonDiscoveryPath(tempDir), - JSON.stringify({ pid: process.pid, startedAt: "2026-05-04T00:00:00.000Z", version: "0.1.0" }) + JSON.stringify({ pid: process.pid, startedAt: "2026-05-04T00:00:00.000Z", version: "0.1.0" }), + { encoding: "utf8", mode: 0o600 } ); await expect(readDaemonDiscovery(tempDir)).rejects.toThrow(/url/i); @@ -214,4 +215,35 @@ describe("daemon discovery", () => { await expect(readDaemonDiscovery(tempDir)).rejects.toThrow(/stale/i); }); + + it("rejects discovery files readable by other users", async () => { + if (process.platform === "win32") { + return; + } + await writeDaemonDiscovery(tempDir, { + url: "http://127.0.0.1:27777", + pid: process.pid, + startedAt: "2026-05-04T00:00:00.000Z", + version: "0.1.0" + }); + await fs.chmod(getDaemonDiscoveryPath(tempDir), 0o644); + + await expect(readDaemonDiscovery(tempDir)).rejects.toThrow(/discovery file/i); + }); + + it("rejects discovery from shared writable state directories", async () => { + if (process.platform === "win32") { + return; + } + await writeDaemonDiscovery(tempDir, { + url: "http://127.0.0.1:27777", + pid: process.pid, + startedAt: "2026-05-04T00:00:00.000Z", + version: "0.1.0" + }); + await fs.chmod(tempDir, 0o777); + + await expect(readDaemonDiscovery(tempDir)).rejects.toThrow(/state directory/i); + }); + });