Fix: Centralize default download path and enhance path validation logic

This commit is contained in:
MasterAcnolo
2026-02-17 23:29:31 +01:00
parent 45e0840fea
commit 32c9a66a35
6 changed files with 94 additions and 40 deletions

46
main.js
View File

@@ -18,7 +18,8 @@ const basePath = config.localMode
const configFolderPath = path.join(basePath, "config" ,"config.json"); const configFolderPath = path.join(basePath, "config" ,"config.json");
const defaultDownloadPath = path.join(os.homedir(), "Downloads", "Freedom Loader"); // Default download path (centralized, lazy loaded)
let defaultDownloadPath;
app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows app.setAppUserModelId("com.masteracnolo.freedomloader"); // pour notifications Windows
app.disableHardwareAcceleration(); app.disableHardwareAcceleration();
@@ -30,7 +31,7 @@ const gotLock = app.requestSingleInstanceLock();
// Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno) // Native dependencies check (yt-dlp.exe, ffmpeg.exe, ffprobe.exe, Deno)
function checkNativeDependencies() { function checkNativeDependencies() {
// Import des chemins centralisés après l'initialisation de l'app // Import centralized paths after app initialization
const { binaryPaths } = require("./server/helpers/path"); const { binaryPaths } = require("./server/helpers/path");
const deps = [ const deps = [
@@ -113,23 +114,29 @@ async function createMainWindow() {
} }
function validateDownloadPath(userPath) { function validateDownloadPath(userPath) {
const userHome = os.homedir(); // C:\Users\<User> const { isSafePath } = require("./server/helpers/validation");
if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader"); // Lazy load default path
if (!defaultDownloadPath) {
const { defaultDownloadFolder } = require("./server/helpers/path");
defaultDownloadPath = defaultDownloadFolder;
}
if (!userPath) return defaultDownloadPath;
try { try {
// Résolution canonique et suivi des symlinks // Canonical resolution and symlink following
const resolved = fs.realpathSync(path.resolve(userPath)); const resolved = fs.realpathSync(path.resolve(userPath));
const normalizedHome = path.resolve(userHome) + path.sep;
if (!resolved.startsWith(normalizedHome)) { // Use the same validation as backend (allows all drives except system folders)
throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !"); if (!isSafePath(resolved)) {
throw new Error("Path not allowed: system folders are blocked!");
} }
return resolved; return resolved;
} catch (err) { } catch (err) {
logger.error(`Invalid download path: ${userPath} - ${err.message}`); logger.error(`Invalid download path: ${userPath} - ${err.message}`);
throw new Error(`Chemin invalide ou inaccessible : ${err.message}`); throw new Error(`Invalid or inaccessible path: ${err.message}`);
} }
} }
@@ -138,14 +145,19 @@ function validateDownloadPath(userPath) {
ipcMain.handle("select-download-folder", async () => { ipcMain.handle("select-download-folder", async () => {
try { try {
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] }); const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
if (!result.canceled && result.filePaths.length > 0) { if (result.canceled) {
const validatedPath = validateDownloadPath(result.filePaths[0]); logger.info("Folder selection cancelled by user");
logger.info(`Folder Checked and Valid : ${validatedPath}`); return null;
}
if (result.filePaths.length > 0) {
const selectedPath = result.filePaths[0];
const validatedPath = validateDownloadPath(selectedPath);
logger.info(`Folder selected and validated: ${validatedPath}`);
return validatedPath; return validatedPath;
} }
return null; return null;
} catch (err) { } catch (err) {
logger.error(`An Error Occured when validating folder : ${err.message}`); logger.warn(`Unsafe or invalid folder rejected: ${err.message}`);
return null; return null;
} }
}); });
@@ -155,7 +167,13 @@ ipcMain.handle("validate-download-path", (event, userPath) => {
}); });
ipcMain.handle("get-default-download-path", () => defaultDownloadPath); ipcMain.handle("get-default-download-path", () => {
if (!defaultDownloadPath) {
const { defaultDownloadFolder } = require("./server/helpers/path");
defaultDownloadPath = defaultDownloadFolder;
}
return defaultDownloadPath;
});
ipcMain.on("set-progress", (event, percent) => { ipcMain.on("set-progress", (event, percent) => {
if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1 if (mainWindow) mainWindow.setProgressBar(percent / 100); // Electron attend 0 → 1

View File

@@ -45,18 +45,15 @@ window.addEventListener("DOMContentLoaded", async () => {
.getElementById("changePath") .getElementById("changePath")
.addEventListener("click", async () => { .addEventListener("click", async () => {
try { try {
const selectedPath = // selectDownloadFolder already returns a validated path
const validatedPath =
await window.electronAPI.selectDownloadFolder(); await window.electronAPI.selectDownloadFolder();
if (!selectedPath) return; // annulé if (!validatedPath) return; // cancelled
// Validation back obligatoire
const validatedPath =
await window.electronAPI.getValidatedDownloadPath(selectedPath);
await applyPathFromBack(validatedPath); await applyPathFromBack(validatedPath);
} catch (err) { } catch (err) {
alert("Dossier non autorisé."); alert("Folder not allowed.");
} }
}); });
}); });

View File

@@ -16,11 +16,14 @@ async function downloadController(req, res) {
}; };
if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !"); if (!options.url || !isValidUrl(options.url)) return res.status(400).send("❌ Invalid URL !");
if (options.outputFolder && !isSafePath(options.outputFolder)) return res.status(400).send("❌ Save Path Not Allowed."); if (options.outputFolder && !isSafePath(options.outputFolder)) {
logger.warn(`Unsafe download path rejected: ${options.outputFolder}`);
return res.status(400).send("❌ Save Path Not Allowed.");
}
// Get output folder when the download is finished, // Get output folder when the download is finished
const filePath = await fetchDownload(options, listeners, speedListeners); const outputFolder = await fetchDownload(options, listeners, speedListeners);
notifyDownloadFinished(filePath); notifyDownloadFinished(outputFolder);
res.send("✅ Download Done !"); res.send("✅ Download Done !");
} catch (err) { } catch (err) {

View File

@@ -1,16 +1,20 @@
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const os = require("os");
const { app } = require("electron"); const { app } = require("electron");
const config = require("../../config"); const config = require("../../config");
const { logger } = require("../logger.js"); const { logger } = require("../logger.js");
// Centralisation de tous les chemins de ressources // Centralized resource paths
const resourcesPath = config.localMode const resourcesPath = config.localMode
? path.join(__dirname, "../../ressources") ? path.join(__dirname, "../../ressources")
: process.resourcesPath; : process.resourcesPath;
// Chemins des binaires // Default download folder (centralized)
const defaultDownloadFolder = path.join(os.homedir(), "Downloads", "Freedom Loader");
// Binary paths
let userYtDlp; let userYtDlp;
let ffmpegPath; let ffmpegPath;
let denoPath; let denoPath;
@@ -32,14 +36,14 @@ if (config.localMode) {
} }
// Chemins des icônes de notification // Notification icon paths
const iconPaths = { const iconPaths = {
confirm: path.join(resourcesPath, "confirm-icon.png"), confirm: path.join(resourcesPath, "confirm-icon.png"),
error: path.join(resourcesPath, "error.png"), error: path.join(resourcesPath, "error.png"),
app: path.join(resourcesPath, "app-icon.ico") app: path.join(resourcesPath, "app-icon.ico")
}; };
// Chemins des binaires pour vérification // Binary paths for verification
const binaryPaths = { const binaryPaths = {
ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"), ytDlp: path.join(resourcesPath, "binaries", "yt-dlp.exe"),
ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"), ffmpeg: path.join(resourcesPath, "binaries", "ffmpeg.exe"),
@@ -51,4 +55,4 @@ if (!userYtDlp){ logger.error("Missing YT-DLP")}
if (!ffmpegPath){ logger.error("Missing FFMPEG")} if (!ffmpegPath){ logger.error("Missing FFMPEG")}
if (!denoPath){ logger.error("Missing DENO")} if (!denoPath){ logger.error("Missing DENO")}
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath }; module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder };

View File

@@ -12,10 +12,35 @@ function isValidUrl(url) {
} }
function isSafePath(folder) { function isSafePath(folder) {
if (!folder || folder.length < 3) return false; if (!folder || typeof folder !== "string") return false;
const unsafe = ["System32", "/etc", "\\Windows"];
const resolved = path.resolve(folder); try {
return !unsafe.some(u => resolved.includes(u)); // Normalize path and resolve symlinks
const resolved = path.resolve(folder).toLowerCase().replace(/\//g, "\\");
// Block Windows system directories (on any drive)
const unsafePaths = [
"\\windows\\",
"\\system32\\",
"\\program files\\",
"\\program files (x86)\\",
"\\programdata\\",
"\\$recycle.bin\\",
"\\system volume information\\"
];
// Check if path contains any unsafe directory
if (unsafePaths.some(unsafe => resolved.includes(unsafe))) {
return false;
}
// Allow all drives (C:, D:, E:, etc.) but block system folders
return true;
} catch (err) {
// In case of path resolution error
return false;
}
} }
module.exports = { isValidUrl, isSafePath }; module.exports = { isValidUrl, isSafePath };

View File

@@ -1,6 +1,5 @@
const { execFile } = require("child_process"); const { execFile } = require("child_process");
const { userYtDlp } = require("../helpers/path"); const { userYtDlp, defaultDownloadFolder } = require("../helpers/path");
const path = require("path");
const fs = require("fs"); const fs = require("fs");
const { logger } = require("../logger"); const { logger } = require("../logger");
const { buildYtDlpArgs } = require("../helpers/buildArgs"); const { buildYtDlpArgs } = require("../helpers/buildArgs");
@@ -9,8 +8,16 @@ const notify = require("../helpers/notify")
function fetchDownload(options, listeners, speedListeners) { function fetchDownload(options, listeners, speedListeners) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const outputFolder = options.outputFolder || path.join(process.env.USERPROFILE, "Downloads", "Freedom Loader"); const outputFolder = options.outputFolder || defaultDownloadFolder;
// Create download folder if it doesn't exist
try {
fs.mkdirSync(outputFolder, { recursive: true }); fs.mkdirSync(outputFolder, { recursive: true });
logger.info(`Output folder ready: ${outputFolder}`);
} catch (err) {
logger.error(`Failed to create output folder: ${err.message}`);
return reject(new Error(`Unable to create download folder: ${err.message}`));
}
const args = buildYtDlpArgs({ ...options, outputFolder }); const args = buildYtDlpArgs({ ...options, outputFolder });
logger.info(`[yt-dlp args] ${args.join(" ")}`); logger.info(`[yt-dlp args] ${args.join(" ")}`);
@@ -31,7 +38,7 @@ function fetchDownload(options, listeners, speedListeners) {
if (!line.trim()) return; if (!line.trim()) return;
logger.info(`[yt-dlp] ${line}`); logger.info(`[yt-dlp] ${line}`);
/* Barre de Chargement*/ /* Progress Bar */
if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset")); if (line.startsWith("[download] Destination:")) listeners.forEach(fn => fn("reset"));
const match = line.match(/\[download\]\s+(\d+\.\d+)%/); const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
if (match) listeners.forEach(fn => fn(parseFloat(match[1]))); if (match) listeners.forEach(fn => fn(parseFloat(match[1])));