Helper buildSafePath
This commit is contained in:
parent
6414abf7b5
commit
5696edf0b0
@ -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";
|
||||
|
||||
79
packages/rdx-utils/src/helpers/safe-path.ts
Normal file
79
packages/rdx-utils/src/helpers/safe-path.ts
Normal 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()}`;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user