Initial open-source self-hosted Songs2YT edition
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user