From 489a4766fab0f775a23501bf18b93d53fda2f6b2 Mon Sep 17 00:00:00 2001 From: MasterAcnolo <68693319+MasterAcnolo@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:47:38 +0100 Subject: [PATCH] Feat: Path Verification (Security #4) --- main.js | 29 ++++++++++++-- preload.js | 3 +- public/script/custompath.js | 75 ++++++++++++++++++++++++------------- 3 files changed, 76 insertions(+), 31 deletions(-) diff --git a/main.js b/main.js index b8680de..afe90f4 100644 --- a/main.js +++ b/main.js @@ -101,21 +101,44 @@ async function createMainWindow() { }); } +function validateDownloadPath(userPath) { + const userHome = os.homedir(); // C:\Users\ + + if (!userPath) return path.join(userHome, "Downloads", "Freedom Loader"); + + // Résolution canonique et suivi des symlinks + const resolved = fs.realpathSync(path.resolve(userPath)); + const normalizedHome = path.resolve(userHome) + path.sep; + + if (!resolved.startsWith(normalizedHome)) { + throw new Error("Chemin non autorisé : uniquement les sous-dossiers du dossier utilisateur sont permis !"); + } + + return resolved; +} + + // IPC ipcMain.handle("select-download-folder", async () => { try { const result = await dialog.showOpenDialog({ properties: ["openDirectory"] }); if (!result.canceled && result.filePaths.length > 0) { - logger.info(`Dossier sélectionné : ${result.filePaths[0]}`); - return result.filePaths[0]; + const validatedPath = validateDownloadPath(result.filePaths[0]); + logger.info(`Folder Checked and Valid : ${validatedPath}`); + return validatedPath; } return null; } catch (err) { - logger.error(`Error when creating Output Folder : ${err.message}`); + logger.error(`An Error Occured when validating folder : ${err.message}`); return null; } }); +ipcMain.handle("validate-download-path", (event, userPath) => { + return validateDownloadPath(userPath); +}); + + ipcMain.handle("get-default-download-path", () => defaultDownloadPath); ipcMain.on("set-progress", (event, percent) => { diff --git a/preload.js b/preload.js index 5da0c1c..4e5eb5f 100644 --- a/preload.js +++ b/preload.js @@ -4,7 +4,8 @@ contextBridge.exposeInMainWorld("electronAPI", { getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"), selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"), setProgress: (percent) => ipcRenderer.send("set-progress", percent), - getFeatures: () => ipcRenderer.invoke("features") + getFeatures: () => ipcRenderer.invoke("features"), + getValidatedDownloadPath: (path) => ipcRenderer.invoke("validate-download-path", path) }); // Contrôles de fenêtre et outils custom pour la topbar diff --git a/public/script/custompath.js b/public/script/custompath.js index 4bb81ad..ccf01f0 100644 --- a/public/script/custompath.js +++ b/public/script/custompath.js @@ -1,41 +1,62 @@ window.addEventListener("DOMContentLoaded", async () => { const savePathElem = document.getElementById("savePath"); + const form = document.getElementById("downloadForm"); - // Essayer de charger depuis le localStorage - let savedPath = localStorage.getItem("customDownloadPath"); - - // Sinon demander le chemin par défaut à l'API Electron - if (!savedPath) { - savedPath = await window.electronAPI.getDefaultDownloadPath(); - } - - // Afficher le chemin - if (savePathElem) { - savePathElem.textContent = savedPath; - } - - // Créer l'input caché s'il n'existe pas déjà + // input caché = DERNIER chemin validé par le back let hidden = document.getElementById("savePathInput"); if (!hidden) { hidden = document.createElement("input"); hidden.type = "hidden"; hidden.name = "savePath"; hidden.id = "savePathInput"; - document.getElementById("downloadForm").appendChild(hidden); + form.appendChild(hidden); } - hidden.value = savedPath; - // Gestion du bouton de modification - document.getElementById("changePath").addEventListener("click", async () => { - const selectedPath = await window.electronAPI.selectDownloadFolder(); - if (selectedPath) { - // Met à jour l'affichage - savePathElem.textContent = selectedPath; - hidden.value = selectedPath; + async function applyPathFromBack(path) { + savePathElem.textContent = path; + hidden.value = path; + localStorage.setItem("customDownloadPath", path); // UX only + } - // Et le stocke en localStorage pour la prochaine fois - localStorage.setItem("customDownloadPath", selectedPath); + async function loadInitialPath() { + // On affiche ce que le user a vu la dernière fois (UX) + const cached = localStorage.getItem("customDownloadPath"); + if (cached) { + savePathElem.textContent = cached; } - }); -}); + // MAIS la source de vérité reste le back + try { + const validatedPath = await window.electronAPI.getValidatedDownloadPath( + cached + ); + await applyPathFromBack(validatedPath); + } catch { + // fallback sûr + const defaultPath = + await window.electronAPI.getDefaultDownloadPath(); + await applyPathFromBack(defaultPath); + } + } + + await loadInitialPath(); + + document + .getElementById("changePath") + .addEventListener("click", async () => { + try { + const selectedPath = + await window.electronAPI.selectDownloadFolder(); + + if (!selectedPath) return; // annulé + + // Validation back obligatoire + const validatedPath = + await window.electronAPI.getValidatedDownloadPath(selectedPath); + + await applyPathFromBack(validatedPath); + } catch (err) { + alert("Dossier non autorisé."); + } + }); +});