/** * Security utilities for path validation * Enforces ALLOWED_ROOT_DIRECTORY constraint with appData exception */ import path from 'path'; /** * Error thrown when a path is not allowed by security policy */ export class PathNotAllowedError extends Error { constructor(filePath: string) { super(`Path not allowed: ${filePath}. Must be within ALLOWED_ROOT_DIRECTORY or DATA_DIR.`); this.name = 'PathNotAllowedError'; } } // Allowed root directory - main security boundary let allowedRootDirectory: string | null = null; // Data directory - always allowed for settings/credentials let dataDirectory: string | null = null; /** * Initialize security settings from environment variables * - ALLOWED_ROOT_DIRECTORY: main security boundary * - DATA_DIR: appData exception, always allowed */ export function initAllowedPaths(): void { // Load ALLOWED_ROOT_DIRECTORY const rootDir = process.env.ALLOWED_ROOT_DIRECTORY; if (rootDir) { allowedRootDirectory = path.resolve(rootDir); console.log(`[Security] ✓ ALLOWED_ROOT_DIRECTORY configured: ${allowedRootDirectory}`); } else { console.log('[Security] ⚠️ ALLOWED_ROOT_DIRECTORY not set - allowing access to all paths'); } // Load DATA_DIR (appData exception - always allowed) const dataDir = process.env.DATA_DIR; if (dataDir) { dataDirectory = path.resolve(dataDir); console.log(`[Security] ✓ DATA_DIR configured: ${dataDirectory}`); } } /** * Check if a path is allowed based on ALLOWED_ROOT_DIRECTORY * Returns true if: * - Path is within ALLOWED_ROOT_DIRECTORY, OR * - Path is within DATA_DIR (appData exception), OR * - No restrictions are configured (backward compatibility) */ export function isPathAllowed(filePath: string): boolean { const resolvedPath = path.resolve(filePath); // Always allow appData directory (settings, credentials) if (dataDirectory && isPathWithinDirectory(resolvedPath, dataDirectory)) { return true; } // If no ALLOWED_ROOT_DIRECTORY restriction is configured, allow all paths // Note: DATA_DIR is checked above as an exception, but doesn't restrict other paths if (!allowedRootDirectory) { return true; } // Allow if within ALLOWED_ROOT_DIRECTORY if (allowedRootDirectory && isPathWithinDirectory(resolvedPath, allowedRootDirectory)) { return true; } // If restrictions are configured but path doesn't match, deny return false; } /** * Validate a path - resolves it and checks permissions * Throws PathNotAllowedError if path is not allowed */ export function validatePath(filePath: string): string { const resolvedPath = path.resolve(filePath); if (!isPathAllowed(resolvedPath)) { throw new PathNotAllowedError(filePath); } return resolvedPath; } /** * Check if a path is within a directory, with protection against path traversal * Returns true only if resolvedPath is within directoryPath */ export function isPathWithinDirectory(resolvedPath: string, directoryPath: string): boolean { // Get the relative path from directory to the target const relativePath = path.relative(directoryPath, resolvedPath); // If relative path starts with "..", it's outside the directory // If relative path is absolute, it's outside the directory // If relative path is empty or ".", it's the directory itself return !relativePath.startsWith('..') && !path.isAbsolute(relativePath); } /** * Get the configured allowed root directory */ export function getAllowedRootDirectory(): string | null { return allowedRootDirectory; } /** * Get the configured data directory */ export function getDataDirectory(): string | null { return dataDirectory; } /** * Get list of allowed paths (for debugging) */ export function getAllowedPaths(): string[] { const paths: string[] = []; if (allowedRootDirectory) { paths.push(allowedRootDirectory); } if (dataDirectory) { paths.push(dataDirectory); } return paths; }