From 7c8f2bd21f19c86f52667001f2c2382779a6503e Mon Sep 17 00:00:00 2001 From: Kolega AI Date: Sat, 14 Feb 2026 11:47:08 +0000 Subject: [PATCH] Ensure Secure File Permissions for Device Identity --- src/infra/device-identity.ts | 73 ++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/src/infra/device-identity.ts b/src/infra/device-identity.ts index d152e26fedd4f..4f05cfa7247b8 100644 --- a/src/infra/device-identity.ts +++ b/src/infra/device-identity.ts @@ -3,6 +3,63 @@ import fs from "node:fs"; import path from "node:path"; import { STATE_DIR } from "../config/paths.js"; +// Security: Function to safely set file permissions for sensitive data +function setSecureFilePermissions(filePath: string): void { + try { + fs.chmodSync(filePath, 0o600); + } catch (error) { + const errorMessage = `Failed to set secure permissions (0600) on device identity file: ${error instanceof Error ? error.message : String(error)}`; + console.error(`[SECURITY] ${errorMessage}`); + throw new Error(`Security violation: ${errorMessage}`); + } +} + +// Security: Write file with secure permissions from creation (prevents race conditions) +function writeSecureFile(filePath: string, data: string): void { + // Set restrictive umask before file operations to ensure secure default permissions + const oldUmask = process.umask(0o077); // Only owner can access newly created files + + try { + // Write file with explicit secure mode + fs.writeFileSync(filePath, data, { mode: 0o600 }); + // Verify permissions were actually set correctly + verifySecurePermissions(filePath); + } finally { + // Always restore original umask + process.umask(oldUmask); + } +} + +// Security: Verify file has secure permissions +function verifySecurePermissions(filePath: string): void { + try { + // Use lstatSync to not follow symlinks (prevents symlink attacks) + const stats = fs.lstatSync(filePath); + + // Security check: ensure it's not a symlink + if (stats.isSymbolicLink()) { + const errorMessage = `Security violation: device identity file is a symbolic link: ${filePath}`; + console.error(`[SECURITY] ${errorMessage}`); + throw new Error(errorMessage); + } + + const permissions = stats.mode & 0o777; + + // File should be readable/writable by owner only (0600) + if (permissions !== 0o600) { + const errorMessage = `Insecure permissions detected on device identity file: ${permissions.toString(8)} (expected 0600). Fix with: chmod 600 "${filePath}"`; + console.error(`[SECURITY] ${errorMessage}`); + throw new Error(`Security violation: ${errorMessage}`); + } + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + // File doesn't exist, which is expected in some cases + return; + } + throw error; + } +} + export type DeviceIdentity = { deviceId: string; publicKeyPem: string; @@ -64,6 +121,8 @@ function generateIdentity(): DeviceIdentity { export function loadOrCreateDeviceIdentity(filePath: string = DEFAULT_FILE): DeviceIdentity { try { if (fs.existsSync(filePath)) { + // Security: Verify existing identity file has secure permissions + verifySecurePermissions(filePath); const raw = fs.readFileSync(filePath, "utf8"); const parsed = JSON.parse(raw) as StoredIdentity; if ( @@ -78,12 +137,7 @@ export function loadOrCreateDeviceIdentity(filePath: string = DEFAULT_FILE): Dev ...parsed, deviceId: derivedId, }; - fs.writeFileSync(filePath, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 }); - try { - fs.chmodSync(filePath, 0o600); - } catch { - // best-effort - } + writeSecureFile(filePath, `${JSON.stringify(updated, null, 2)}\n`); return { deviceId: derivedId, publicKeyPem: parsed.publicKeyPem, @@ -110,12 +164,7 @@ export function loadOrCreateDeviceIdentity(filePath: string = DEFAULT_FILE): Dev privateKeyPem: identity.privateKeyPem, createdAtMs: Date.now(), }; - fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600 }); - try { - fs.chmodSync(filePath, 0o600); - } catch { - // best-effort - } + writeSecureFile(filePath, `${JSON.stringify(stored, null, 2)}\n`); return identity; }