80 lines
1.9 KiB
TypeScript
80 lines
1.9 KiB
TypeScript
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()}`;
|
|
}
|