import path from "path"; import { randomBytes } from "crypto"; import { getUploadDir } from "./storage"; /** Allow only safe staging folder names from clients. */ export function sanitizeUploadSessionKey(raw?: string | null): string { const cleaned = (raw ?? "").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 64); if (cleaned.length >= 8) return cleaned; return `${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`; } function userUploadRoot(userId: string) { return path.resolve(getUploadDir(), userId); } function isInsideDir(filePath: string, dir: string) { const resolvedFile = path.resolve(filePath); const resolvedDir = path.resolve(dir); return resolvedFile === resolvedDir || resolvedFile.startsWith(resolvedDir + path.sep); } /** Ensure a path stays under uploads/{userId}/ (blocks traversal & cross-user access). */ export function assertPathInUserUploads(userId: string, filePath: string): string { if (!filePath?.trim()) { throw new Error("Invalid upload path"); } const resolved = path.resolve(filePath); const root = userUploadRoot(userId); if (!isInsideDir(resolved, root)) { throw new Error("Invalid upload path"); } return resolved; } export function getUserStagingDir(userId: string, sessionKey: string) { const safeSession = sanitizeUploadSessionKey(sessionKey); const dir = path.join(userUploadRoot(userId), "staging", safeSession); // Defense in depth: resolve and re-check const resolved = path.resolve(dir); if (!isInsideDir(resolved, userUploadRoot(userId))) { throw new Error("Invalid upload session"); } return resolved; }