From bbb51dbf99055de79a39f52dcfe394a4efbdf8d4 Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Wed, 28 Jan 2026 22:12:20 +0300 Subject: [PATCH] fix: enforce WORKSPACES_ROOT in folder browser and folder creation --- server/index.js | 63 +++++++++++++++--------- server/routes/projects.js | 4 +- src/components/ProjectCreationWizard.jsx | 2 +- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/server/index.js b/server/index.js index 719e1c3..1c42d30 100755 --- a/server/index.js +++ b/server/index.js @@ -70,7 +70,7 @@ import mcpUtilsRoutes from './routes/mcp-utils.js'; import commandsRoutes from './routes/commands.js'; import settingsRoutes from './routes/settings.js'; import agentRoutes from './routes/agent.js'; -import projectsRoutes, { FORBIDDEN_PATHS } from './routes/projects.js'; +import projectsRoutes, { WORKSPACES_ROOT, validateWorkspacePath } from './routes/projects.js'; import cliAuthRoutes from './routes/cli-auth.js'; import userRoutes from './routes/user.js'; import codexRoutes from './routes/codex.js'; @@ -484,22 +484,42 @@ app.post('/api/projects/create', authenticateToken, async (req, res) => { } }); +const expandWorkspacePath = (inputPath) => { + if (!inputPath) return inputPath; + if (inputPath === '~') { + return WORKSPACES_ROOT; + } + if (inputPath.startsWith('~/') || inputPath.startsWith('~\\')) { + return path.join(WORKSPACES_ROOT, inputPath.slice(2)); + } + return inputPath; +}; + // Browse filesystem endpoint for project suggestions - uses existing getFileTree app.get('/api/browse-filesystem', authenticateToken, async (req, res) => { try { const { path: dirPath } = req.query; + console.log('[API] Browse filesystem request for path:', dirPath); + console.log('[API] WORKSPACES_ROOT is:', WORKSPACES_ROOT); // Default to home directory if no path provided - const homeDir = os.homedir(); - let targetPath = dirPath ? dirPath.replace('~', homeDir) : homeDir; + const defaultRoot = WORKSPACES_ROOT; + let targetPath = dirPath ? expandWorkspacePath(dirPath) : defaultRoot; // Resolve and normalize the path targetPath = path.resolve(targetPath); + + // Security check - ensure path is within allowed workspace root + const validation = await validateWorkspacePath(targetPath); + if (!validation.valid) { + return res.status(403).json({ error: validation.error }); + } + const resolvedPath = validation.resolvedPath || targetPath; // Security check - ensure path is accessible try { - await fs.promises.access(targetPath); - const stats = await fs.promises.stat(targetPath); + await fs.promises.access(resolvedPath); + const stats = await fs.promises.stat(resolvedPath); if (!stats.isDirectory()) { return res.status(400).json({ error: 'Path is not a directory' }); @@ -509,7 +529,7 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => { } // Use existing getFileTree function with shallow depth (only direct children) - const fileTree = await getFileTree(targetPath, 1, 0, false); // maxDepth=1, showHidden=false + const fileTree = await getFileTree(resolvedPath, 1, 0, false); // maxDepth=1, showHidden=false // Filter only directories and format for suggestions const directories = fileTree @@ -529,7 +549,13 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => { // Add common directories if browsing home directory const suggestions = []; - if (targetPath === homeDir) { + let resolvedWorkspaceRoot = defaultRoot; + try { + resolvedWorkspaceRoot = await fsPromises.realpath(defaultRoot); + } catch (error) { + // Use default root as-is if realpath fails + } + if (resolvedPath === resolvedWorkspaceRoot) { const commonDirs = ['Desktop', 'Documents', 'Projects', 'Development', 'Dev', 'Code', 'workspace']; const existingCommon = directories.filter(dir => commonDirs.includes(dir.name)); const otherDirs = directories.filter(dir => !commonDirs.includes(dir.name)); @@ -540,7 +566,7 @@ app.get('/api/browse-filesystem', authenticateToken, async (req, res) => { } res.json({ - path: targetPath, + path: resolvedPath, suggestions: suggestions }); @@ -556,22 +582,13 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => { if (!folderPath) { return res.status(400).json({ error: 'Path is required' }); } - const homeDir = os.homedir(); - const targetPath = path.resolve(folderPath.replace('~', homeDir)); - const normalizedPath = path.normalize(targetPath); - const comparePath = normalizedPath.toLowerCase(); - const forbiddenLower = FORBIDDEN_PATHS.map(p => p.toLowerCase()); - if (forbiddenLower.includes(comparePath) || comparePath === '/') { - return res.status(403).json({ error: 'Cannot create folders in system directories' }); - } - for (const forbidden of forbiddenLower) { - if (comparePath.startsWith(forbidden + path.sep)) { - if (forbidden === '/var' && (comparePath.startsWith('/var/tmp') || comparePath.startsWith('/var/folders'))) { - continue; - } - return res.status(403).json({ error: `Cannot create folders in system directory: ${forbidden}` }); - } + const expandedPath = expandWorkspacePath(folderPath); + const resolvedInput = path.resolve(expandedPath); + const validation = await validateWorkspacePath(resolvedInput); + if (!validation.valid) { + return res.status(403).json({ error: validation.error }); } + const targetPath = validation.resolvedPath || resolvedInput; const parentDir = path.dirname(targetPath); try { await fs.promises.access(parentDir); diff --git a/server/routes/projects.js b/server/routes/projects.js index 7db0537..2f41610 100644 --- a/server/routes/projects.js +++ b/server/routes/projects.js @@ -13,7 +13,7 @@ function sanitizeGitError(message, token) { } // Configure allowed workspace root (defaults to user's home directory) -const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir(); +export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir(); // System-critical paths that should never be used as workspace directories export const FORBIDDEN_PATHS = [ @@ -48,7 +48,7 @@ export const FORBIDDEN_PATHS = [ * @param {string} requestedPath - The path to validate * @returns {Promise<{valid: boolean, resolvedPath?: string, error?: string}>} */ -async function validateWorkspacePath(requestedPath) { +export async function validateWorkspacePath(requestedPath) { try { // Resolve to absolute path let absolutePath = path.resolve(requestedPath); diff --git a/src/components/ProjectCreationWizard.jsx b/src/components/ProjectCreationWizard.jsx index dd6ab72..5126d44 100644 --- a/src/components/ProjectCreationWizard.jsx +++ b/src/components/ProjectCreationWizard.jsx @@ -183,7 +183,7 @@ const ProjectCreationWizard = ({ onClose, onProjectCreated }) => { const data = await response.json(); if (!response.ok) { - throw new Error(data.error || t('projectWizard.errors.failedToCreate')); + throw new Error(data.details || data.error || t('projectWizard.errors.failedToCreate')); } if (onProjectCreated) {