import path from "node:path"; /** * Construye rutas de filesystem POSIX de forma segura. * * Garantías: * - Previene path traversal * - Elimina caracteres no seguros * - Siempre resuelve dentro de basePath * - Fail-fast si no es posible construir una ruta válida * * Pensado para Unix/Linux. */ export interface SafePathOptions { basePath: string; segments: string[]; filename?: string; } export function buildSafePath(options: SafePathOptions): string { const { basePath, segments, filename } = options; if (!basePath) { throw new Error("basePath is required"); } const safeSegments = segments.map(sanitizeSegment); const relativePath = filename ? path.posix.join(...safeSegments, sanitizeFilename(filename)) : path.posix.join(...safeSegments); const absolutePath = path.resolve(basePath, relativePath); const normalizedBasePath = path.resolve(basePath); // 🔐 Defensa CRÍTICA contra path traversal if (!absolutePath.startsWith(normalizedBasePath + path.sep)) { throw new Error(`Resolved path escapes basePath: ${absolutePath}`); } return absolutePath; } /** * Sanitiza un segmento de path (directorio). * Whitelist estricta. */ function sanitizeSegment(value: string): string { const sanitized = value .normalize("NFKD") .replace(/[^a-zA-Z0-9-_]/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, "") .toLowerCase(); if (!sanitized) { throw new Error(`Invalid path segment: "${value}"`); } return sanitized; } /** * Sanitiza un nombre de fichero. * Mantiene la extensión. */ function sanitizeFilename(filename: string): string { const ext = path.extname(filename); const name = path.basename(filename, ext); const safeName = sanitizeSegment(name); if (!ext || ext.length > 10) { throw new Error(`Invalid or missing file extension in "${filename}"`); } return `${safeName}${ext.toLowerCase()}`; }