Helper buildSafePath

This commit is contained in:
David Arranz 2026-01-13 14:35:52 +01:00
parent 6414abf7b5
commit 5696edf0b0
2 changed files with 80 additions and 0 deletions

View File

@ -5,5 +5,6 @@ export * from "./patch-field";
export * from "./result";
export * from "./result-collection";
export * from "./rule-validator";
export * from "./safe-path";
export * from "./types";
export * from "./utils";

View File

@ -0,0 +1,79 @@
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()}`;
}