Feat: Path Verification (Security #4)

This commit is contained in:
MasterAcnolo
2026-01-14 21:47:38 +01:00
parent adcee62386
commit 489a4766fa
3 changed files with 76 additions and 31 deletions

29
main.js
View File

@@ -101,21 +101,44 @@ async function createMainWindow() {
}); });
} }
function validateDownloadPath(userPath) {
const userHome = os.homedir(); // C:\Users\<User>
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 // IPC
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 && result.filePaths.length > 0) {
logger.info(`Dossier sélectionné : ${result.filePaths[0]}`); const validatedPath = validateDownloadPath(result.filePaths[0]);
return result.filePaths[0]; logger.info(`Folder Checked and Valid : ${validatedPath}`);
return validatedPath;
} }
return null; return null;
} catch (err) { } catch (err) {
logger.error(`Error when creating Output Folder : ${err.message}`); logger.error(`An Error Occured when validating folder : ${err.message}`);
return null; return null;
} }
}); });
ipcMain.handle("validate-download-path", (event, userPath) => {
return validateDownloadPath(userPath);
});
ipcMain.handle("get-default-download-path", () => defaultDownloadPath); ipcMain.handle("get-default-download-path", () => defaultDownloadPath);
ipcMain.on("set-progress", (event, percent) => { ipcMain.on("set-progress", (event, percent) => {

View File

@@ -4,7 +4,8 @@ contextBridge.exposeInMainWorld("electronAPI", {
getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"), getDefaultDownloadPath: () => ipcRenderer.invoke("get-default-download-path"),
selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"), selectDownloadFolder: () => ipcRenderer.invoke("select-download-folder"),
setProgress: (percent) => ipcRenderer.send("set-progress", percent), 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 // Contrôles de fenêtre et outils custom pour la topbar

View File

@@ -1,41 +1,62 @@
window.addEventListener("DOMContentLoaded", async () => { window.addEventListener("DOMContentLoaded", async () => {
const savePathElem = document.getElementById("savePath"); const savePathElem = document.getElementById("savePath");
const form = document.getElementById("downloadForm");
// Essayer de charger depuis le localStorage // input caché = DERNIER chemin validé par le back
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à
let hidden = document.getElementById("savePathInput"); let hidden = document.getElementById("savePathInput");
if (!hidden) { if (!hidden) {
hidden = document.createElement("input"); hidden = document.createElement("input");
hidden.type = "hidden"; hidden.type = "hidden";
hidden.name = "savePath"; hidden.name = "savePath";
hidden.id = "savePathInput"; hidden.id = "savePathInput";
document.getElementById("downloadForm").appendChild(hidden); form.appendChild(hidden);
} }
hidden.value = savedPath;
// Gestion du bouton de modification async function applyPathFromBack(path) {
document.getElementById("changePath").addEventListener("click", async () => { savePathElem.textContent = path;
const selectedPath = await window.electronAPI.selectDownloadFolder(); hidden.value = path;
if (selectedPath) { localStorage.setItem("customDownloadPath", path); // UX only
// Met à jour l'affichage }
savePathElem.textContent = selectedPath;
hidden.value = selectedPath;
// Et le stocke en localStorage pour la prochaine fois async function loadInitialPath() {
localStorage.setItem("customDownloadPath", selectedPath); // 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é.");
} }
}); });
}); });