mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
fix: Add file path validation with dialog-based approval
- Implement dialog-based path approval system for file operations - Track user-approved paths from OS file dialogs - Validate paths before read/write operations - Prevent path traversal attacks with normalization - Reject relative paths and null bytes - Users can save/open files anywhere they choose - Blocks arbitrary file access from compromised renderer process This prevents path traversal vulnerabilities while maintaining full user freedom for file selection. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8e84cbf609
commit
1084633b64
1 changed files with 95 additions and 14 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { app, BrowserWindow, ipcMain, dialog, protocol } from "electron";
|
import { app, BrowserWindow, ipcMain, dialog, protocol } from "electron";
|
||||||
import { join } from "path";
|
import { join, resolve, normalize, isAbsolute } from "path";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
import Store from "electron-store";
|
import Store from "electron-store";
|
||||||
import { updateElectronApp, UpdateSourceType } from "update-electron-app";
|
import { updateElectronApp, UpdateSourceType } from "update-electron-app";
|
||||||
|
|
@ -52,7 +52,13 @@ async function setupCustomProtocol() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve relative to app resources
|
// Resolve relative to app resources
|
||||||
const resourcePath = join(__dirname, "..", "renderer", MAIN_WINDOW_VITE_NAME, filePath);
|
const resourcePath = join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"renderer",
|
||||||
|
MAIN_WINDOW_VITE_NAME,
|
||||||
|
filePath,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await fs.readFile(resourcePath);
|
const data = await fs.readFile(resourcePath);
|
||||||
|
|
@ -298,6 +304,9 @@ ipcMain.handle("dialog:openFile", async (_event, options) => {
|
||||||
const filePath = result.filePaths[0];
|
const filePath = result.filePaths[0];
|
||||||
const fileName = filePath.split(/[\\/]/).pop() || "";
|
const fileName = filePath.split(/[\\/]/).pop() || "";
|
||||||
|
|
||||||
|
// Approve path for file operations since user explicitly selected it
|
||||||
|
approvePath(filePath);
|
||||||
|
|
||||||
console.log("[Dialog] File selected:", fileName);
|
console.log("[Dialog] File selected:", fileName);
|
||||||
return { filePath, fileName };
|
return { filePath, fileName };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -317,6 +326,9 @@ ipcMain.handle("dialog:saveFile", async (_event, options) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Approve path for file operations since user explicitly selected it
|
||||||
|
approvePath(result.filePath);
|
||||||
|
|
||||||
console.log("[Dialog] Save file selected:", result.filePath);
|
console.log("[Dialog] Save file selected:", result.filePath);
|
||||||
return result.filePath;
|
return result.filePath;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -325,8 +337,67 @@ ipcMain.handle("dialog:saveFile", async (_event, options) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// File system handlers
|
// File system handlers with dialog-based path validation
|
||||||
ipcMain.handle("fs:readFile", async (_event, filePath) => {
|
// Track paths approved by user through OS file dialogs
|
||||||
|
// This prevents arbitrary file access while allowing users full freedom
|
||||||
|
const userApprovedPaths = new Set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a file path was approved by the user through a dialog
|
||||||
|
* Also performs basic sanitization to prevent obvious attacks
|
||||||
|
*/
|
||||||
|
function isPathApproved(filePath: string): boolean {
|
||||||
|
// Reject empty, null, or undefined paths
|
||||||
|
if (!filePath) {
|
||||||
|
console.warn("[FS Security] Rejected empty path");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject relative paths - must be absolute
|
||||||
|
if (!isAbsolute(filePath)) {
|
||||||
|
console.warn("[FS Security] Rejected relative path:", filePath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject paths with null bytes (security vulnerability)
|
||||||
|
if (filePath.includes("\0")) {
|
||||||
|
console.warn("[FS Security] Rejected path with null byte:", filePath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize the path to prevent traversal tricks
|
||||||
|
const normalizedPath = normalize(resolve(filePath));
|
||||||
|
|
||||||
|
// Check if path was approved through a dialog
|
||||||
|
const isApproved = userApprovedPaths.has(normalizedPath);
|
||||||
|
|
||||||
|
if (!isApproved) {
|
||||||
|
console.warn(
|
||||||
|
"[FS Security] Rejected path - not approved through file dialog:",
|
||||||
|
filePath,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isApproved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approves a path for file operations after user selection via dialog
|
||||||
|
*/
|
||||||
|
function approvePath(filePath: string): void {
|
||||||
|
const normalizedPath = normalize(resolve(filePath));
|
||||||
|
userApprovedPaths.add(normalizedPath);
|
||||||
|
console.log("[FS Security] Approved path:", normalizedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("fs:readFile", async (_event, filePath: string) => {
|
||||||
|
// Validate path was approved by user
|
||||||
|
if (!isPathApproved(filePath)) {
|
||||||
|
throw new Error(
|
||||||
|
"Access denied: File path not approved. Please select the file through the file dialog.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buffer = await fs.readFile(filePath);
|
const buffer = await fs.readFile(filePath);
|
||||||
console.log("[FS] Read file:", filePath, "Size:", buffer.length);
|
console.log("[FS] Read file:", filePath, "Size:", buffer.length);
|
||||||
|
|
@ -337,7 +408,16 @@ ipcMain.handle("fs:readFile", async (_event, filePath) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("fs:writeFile", async (_event, filePath, data) => {
|
ipcMain.handle(
|
||||||
|
"fs:writeFile",
|
||||||
|
async (_event, filePath: string, data: number[]) => {
|
||||||
|
// Validate path was approved by user
|
||||||
|
if (!isPathApproved(filePath)) {
|
||||||
|
throw new Error(
|
||||||
|
"Access denied: File path not approved. Please select the file through the save dialog.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.writeFile(filePath, Buffer.from(data));
|
await fs.writeFile(filePath, Buffer.from(data));
|
||||||
console.log("[FS] Wrote file:", filePath);
|
console.log("[FS] Wrote file:", filePath);
|
||||||
|
|
@ -346,4 +426,5 @@ ipcMain.handle("fs:writeFile", async (_event, filePath, data) => {
|
||||||
console.error("[FS] Failed to write file:", err);
|
console.error("[FS] Failed to write file:", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue