diff --git a/apps/server/src/routes/agent/routes/send.ts b/apps/server/src/routes/agent/routes/send.ts index fa012e89..59575584 100644 --- a/apps/server/src/routes/agent/routes/send.ts +++ b/apps/server/src/routes/agent/routes/send.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import { AgentService } from "../../../services/agent-service.js"; import { createLogger } from "../../../lib/logger.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const logger = createLogger("Agent"); @@ -29,6 +30,27 @@ export function createSendHandler(agentService: AgentService) { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + if (workingDirectory) { + validatePath(workingDirectory); + } + if (imagePaths && imagePaths.length > 0) { + for (const imagePath of imagePaths) { + validatePath(imagePath); + } + } + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Start the message processing (don't await - it streams via WebSocket) agentService .sendMessage({ diff --git a/apps/server/src/routes/agent/routes/start.ts b/apps/server/src/routes/agent/routes/start.ts index 3686bad5..c4900cb1 100644 --- a/apps/server/src/routes/agent/routes/start.ts +++ b/apps/server/src/routes/agent/routes/start.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import { AgentService } from "../../../services/agent-service.js"; import { createLogger } from "../../../lib/logger.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const logger = createLogger("Agent"); @@ -24,6 +25,22 @@ export function createStartHandler(agentService: AgentService) { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + if (workingDirectory) { + try { + validatePath(workingDirectory); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + } + const result = await agentService.startConversation({ sessionId, workingDirectory, diff --git a/apps/server/src/routes/auto-mode/routes/analyze-project.ts b/apps/server/src/routes/auto-mode/routes/analyze-project.ts index 28a2d489..9f625abc 100644 --- a/apps/server/src/routes/auto-mode/routes/analyze-project.ts +++ b/apps/server/src/routes/auto-mode/routes/analyze-project.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import type { AutoModeService } from "../../../services/auto-mode-service.js"; import { createLogger } from "../../../lib/logger.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const logger = createLogger("AutoMode"); @@ -21,6 +22,20 @@ export function createAnalyzeProjectHandler(autoModeService: AutoModeService) { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Start analysis in background autoModeService.analyzeProject(projectPath).catch((error) => { logger.error(`[AutoMode] Project analysis error:`, error); diff --git a/apps/server/src/routes/auto-mode/routes/commit-feature.ts b/apps/server/src/routes/auto-mode/routes/commit-feature.ts index aaf2e6f5..3e4bcbea 100644 --- a/apps/server/src/routes/auto-mode/routes/commit-feature.ts +++ b/apps/server/src/routes/auto-mode/routes/commit-feature.ts @@ -5,6 +5,7 @@ import type { Request, Response } from "express"; import type { AutoModeService } from "../../../services/auto-mode-service.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; export function createCommitFeatureHandler(autoModeService: AutoModeService) { return async (req: Request, res: Response): Promise => { @@ -25,6 +26,23 @@ export function createCommitFeatureHandler(autoModeService: AutoModeService) { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + if (worktreePath) { + validatePath(worktreePath); + } + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const commitHash = await autoModeService.commitFeature( projectPath, featureId, diff --git a/apps/server/src/routes/auto-mode/routes/run-feature.ts b/apps/server/src/routes/auto-mode/routes/run-feature.ts index bae005f3..25405c29 100644 --- a/apps/server/src/routes/auto-mode/routes/run-feature.ts +++ b/apps/server/src/routes/auto-mode/routes/run-feature.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import type { AutoModeService } from "../../../services/auto-mode-service.js"; import { createLogger } from "../../../lib/logger.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const logger = createLogger("AutoMode"); @@ -26,6 +27,20 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) { return; } + // Validate path is within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Start execution in background // executeFeature derives workDir from feature.branchName autoModeService diff --git a/apps/server/src/routes/features/routes/create.ts b/apps/server/src/routes/features/routes/create.ts index fda12589..19601c71 100644 --- a/apps/server/src/routes/features/routes/create.ts +++ b/apps/server/src/routes/features/routes/create.ts @@ -7,7 +7,7 @@ import { FeatureLoader, type Feature, } from "../../../services/feature-loader.js"; -import { addAllowedPath } from "../../../lib/security.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; import { getErrorMessage, logError } from "../common.js"; export function createCreateHandler(featureLoader: FeatureLoader) { @@ -28,8 +28,19 @@ export function createCreateHandler(featureLoader: FeatureLoader) { return; } - // Add project path to allowed paths - addAllowedPath(projectPath); + // Validate path is within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } const created = await featureLoader.create(projectPath, feature); res.json({ success: true, feature: created }); diff --git a/apps/server/src/routes/features/routes/delete.ts b/apps/server/src/routes/features/routes/delete.ts index bf5408d5..9ee29b01 100644 --- a/apps/server/src/routes/features/routes/delete.ts +++ b/apps/server/src/routes/features/routes/delete.ts @@ -5,6 +5,7 @@ import type { Request, Response } from "express"; import { FeatureLoader } from "../../../services/feature-loader.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; export function createDeleteHandler(featureLoader: FeatureLoader) { return async (req: Request, res: Response): Promise => { @@ -24,6 +25,20 @@ export function createDeleteHandler(featureLoader: FeatureLoader) { return; } + // Validate path is within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const success = await featureLoader.delete(projectPath, featureId); res.json({ success }); } catch (error) { diff --git a/apps/server/src/routes/features/routes/get.ts b/apps/server/src/routes/features/routes/get.ts index 17900bb0..c7a6c095 100644 --- a/apps/server/src/routes/features/routes/get.ts +++ b/apps/server/src/routes/features/routes/get.ts @@ -5,6 +5,7 @@ import type { Request, Response } from "express"; import { FeatureLoader } from "../../../services/feature-loader.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; export function createGetHandler(featureLoader: FeatureLoader) { return async (req: Request, res: Response): Promise => { @@ -24,6 +25,20 @@ export function createGetHandler(featureLoader: FeatureLoader) { return; } + // Validate path is within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const feature = await featureLoader.get(projectPath, featureId); if (!feature) { res.status(404).json({ success: false, error: "Feature not found" }); diff --git a/apps/server/src/routes/features/routes/list.ts b/apps/server/src/routes/features/routes/list.ts index 33dc68b6..892a8c63 100644 --- a/apps/server/src/routes/features/routes/list.ts +++ b/apps/server/src/routes/features/routes/list.ts @@ -4,7 +4,7 @@ import type { Request, Response } from "express"; import { FeatureLoader } from "../../../services/feature-loader.js"; -import { addAllowedPath } from "../../../lib/security.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; import { getErrorMessage, logError } from "../common.js"; export function createListHandler(featureLoader: FeatureLoader) { @@ -19,8 +19,19 @@ export function createListHandler(featureLoader: FeatureLoader) { return; } - // Add project path to allowed paths - addAllowedPath(projectPath); + // Validate path is within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } const features = await featureLoader.getAll(projectPath); res.json({ success: true, features }); diff --git a/apps/server/src/routes/features/routes/update.ts b/apps/server/src/routes/features/routes/update.ts index 68be887b..b33eb549 100644 --- a/apps/server/src/routes/features/routes/update.ts +++ b/apps/server/src/routes/features/routes/update.ts @@ -8,6 +8,7 @@ import { type Feature, } from "../../../services/feature-loader.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; export function createUpdateHandler(featureLoader: FeatureLoader) { return async (req: Request, res: Response): Promise => { @@ -26,6 +27,20 @@ export function createUpdateHandler(featureLoader: FeatureLoader) { return; } + // Validate path is within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const updated = await featureLoader.update( projectPath, featureId, diff --git a/apps/server/src/routes/git/routes/diffs.ts b/apps/server/src/routes/git/routes/diffs.ts index eb532a03..a258f004 100644 --- a/apps/server/src/routes/git/routes/diffs.ts +++ b/apps/server/src/routes/git/routes/diffs.ts @@ -5,6 +5,7 @@ import type { Request, Response } from "express"; import { getErrorMessage, logError } from "../common.js"; import { getGitRepositoryDiffs } from "../../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; export function createDiffsHandler() { return async (req: Request, res: Response): Promise => { @@ -16,6 +17,20 @@ export function createDiffsHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + try { const result = await getGitRepositoryDiffs(projectPath); res.json({ diff --git a/apps/server/src/routes/git/routes/file-diff.ts b/apps/server/src/routes/git/routes/file-diff.ts index fdf66998..4229a123 100644 --- a/apps/server/src/routes/git/routes/file-diff.ts +++ b/apps/server/src/routes/git/routes/file-diff.ts @@ -7,6 +7,7 @@ import { exec } from "child_process"; import { promisify } from "util"; import { getErrorMessage, logError } from "../common.js"; import { generateSyntheticDiffForNewFile } from "../../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -25,6 +26,21 @@ export function createFileDiffHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + validatePath(filePath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + try { // First check if the file is untracked const { stdout: status } = await execAsync( diff --git a/apps/server/src/routes/settings/routes/get-project.ts b/apps/server/src/routes/settings/routes/get-project.ts index 58f6ce7e..9a2c9ba9 100644 --- a/apps/server/src/routes/settings/routes/get-project.ts +++ b/apps/server/src/routes/settings/routes/get-project.ts @@ -11,6 +11,7 @@ import type { Request, Response } from "express"; import type { SettingsService } from "../../../services/settings-service.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; /** * Create handler factory for POST /api/settings/project @@ -31,6 +32,20 @@ export function createGetProjectHandler(settingsService: SettingsService) { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const settings = await settingsService.getProjectSettings(projectPath); res.json({ diff --git a/apps/server/src/routes/settings/routes/update-project.ts b/apps/server/src/routes/settings/routes/update-project.ts index 5dc38df0..cccad9f4 100644 --- a/apps/server/src/routes/settings/routes/update-project.ts +++ b/apps/server/src/routes/settings/routes/update-project.ts @@ -12,6 +12,7 @@ import type { Request, Response } from "express"; import type { SettingsService } from "../../../services/settings-service.js"; import type { ProjectSettings } from "../../../types/settings.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; /** * Create handler factory for PUT /api/settings/project @@ -43,6 +44,20 @@ export function createUpdateProjectHandler(settingsService: SettingsService) { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const settings = await settingsService.updateProjectSettings( projectPath, updates diff --git a/apps/server/src/routes/suggestions/routes/generate.ts b/apps/server/src/routes/suggestions/routes/generate.ts index beafd10f..d9f4582b 100644 --- a/apps/server/src/routes/suggestions/routes/generate.ts +++ b/apps/server/src/routes/suggestions/routes/generate.ts @@ -12,6 +12,7 @@ import { logError, } from "../common.js"; import { generateSuggestions } from "../generate-suggestions.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const logger = createLogger("Suggestions"); @@ -28,6 +29,20 @@ export function createGenerateHandler(events: EventEmitter) { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const { isRunning } = getSuggestionsStatus(); if (isRunning) { res.json({ diff --git a/apps/server/src/routes/worktree/routes/commit.ts b/apps/server/src/routes/worktree/routes/commit.ts index 273c7964..69575a90 100644 --- a/apps/server/src/routes/worktree/routes/commit.ts +++ b/apps/server/src/routes/worktree/routes/commit.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import { exec } from "child_process"; import { promisify } from "util"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -25,6 +26,20 @@ export function createCommitHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(worktreePath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Check for uncommitted changes const { stdout: status } = await execAsync("git status --porcelain", { cwd: worktreePath, diff --git a/apps/server/src/routes/worktree/routes/create.ts b/apps/server/src/routes/worktree/routes/create.ts index 690afe48..c8953963 100644 --- a/apps/server/src/routes/worktree/routes/create.ts +++ b/apps/server/src/routes/worktree/routes/create.ts @@ -20,6 +20,7 @@ import { ensureInitialCommit, } from "../common.js"; import { trackBranch } from "./branch-tracking.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -91,6 +92,20 @@ export function createCreateHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + if (!(await isGitRepo(projectPath))) { res.status(400).json({ success: false, diff --git a/apps/server/src/routes/worktree/routes/delete.ts b/apps/server/src/routes/worktree/routes/delete.ts index a0cb8eea..08eb30d3 100644 --- a/apps/server/src/routes/worktree/routes/delete.ts +++ b/apps/server/src/routes/worktree/routes/delete.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import { exec } from "child_process"; import { promisify } from "util"; import { isGitRepo, getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -26,6 +27,21 @@ export function createDeleteHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + validatePath(worktreePath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + if (!(await isGitRepo(projectPath))) { res.status(400).json({ success: false, diff --git a/apps/server/src/routes/worktree/routes/diffs.ts b/apps/server/src/routes/worktree/routes/diffs.ts index d3b6ed09..8a94f21c 100644 --- a/apps/server/src/routes/worktree/routes/diffs.ts +++ b/apps/server/src/routes/worktree/routes/diffs.ts @@ -7,6 +7,7 @@ import path from "path"; import fs from "fs/promises"; import { getErrorMessage, logError } from "../common.js"; import { getGitRepositoryDiffs } from "../../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; export function createDiffsHandler() { return async (req: Request, res: Response): Promise => { @@ -26,6 +27,20 @@ export function createDiffsHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Git worktrees are stored in project directory const worktreePath = path.join(projectPath, ".worktrees", featureId); diff --git a/apps/server/src/routes/worktree/routes/file-diff.ts b/apps/server/src/routes/worktree/routes/file-diff.ts index 70306b6a..3d26bf42 100644 --- a/apps/server/src/routes/worktree/routes/file-diff.ts +++ b/apps/server/src/routes/worktree/routes/file-diff.ts @@ -9,6 +9,7 @@ import path from "path"; import fs from "fs/promises"; import { getErrorMessage, logError } from "../common.js"; import { generateSyntheticDiffForNewFile } from "../../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -29,6 +30,21 @@ export function createFileDiffHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + validatePath(filePath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Git worktrees are stored in project directory const worktreePath = path.join(projectPath, ".worktrees", featureId); diff --git a/apps/server/src/routes/worktree/routes/info.ts b/apps/server/src/routes/worktree/routes/info.ts index 1a5bb463..50dbb371 100644 --- a/apps/server/src/routes/worktree/routes/info.ts +++ b/apps/server/src/routes/worktree/routes/info.ts @@ -8,6 +8,7 @@ import { promisify } from "util"; import path from "path"; import fs from "fs/promises"; import { getErrorMessage, logError, normalizePath } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -29,6 +30,20 @@ export function createInfoHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Check if worktree exists (git worktrees are stored in project directory) const worktreePath = path.join(projectPath, ".worktrees", featureId); try { diff --git a/apps/server/src/routes/worktree/routes/init-git.ts b/apps/server/src/routes/worktree/routes/init-git.ts index 0aecc8af..49e7e64e 100644 --- a/apps/server/src/routes/worktree/routes/init-git.ts +++ b/apps/server/src/routes/worktree/routes/init-git.ts @@ -8,6 +8,7 @@ import { promisify } from "util"; import { existsSync } from "fs"; import { join } from "path"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -26,6 +27,20 @@ export function createInitGitHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Check if .git already exists const gitDirPath = join(projectPath, ".git"); if (existsSync(gitDirPath)) { diff --git a/apps/server/src/routes/worktree/routes/list-branches.ts b/apps/server/src/routes/worktree/routes/list-branches.ts index 0b07eb17..1b0cd9e6 100644 --- a/apps/server/src/routes/worktree/routes/list-branches.ts +++ b/apps/server/src/routes/worktree/routes/list-branches.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import { exec } from "child_process"; import { promisify } from "util"; import { getErrorMessage, logWorktreeError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -30,6 +31,20 @@ export function createListBranchesHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(worktreePath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Get current branch const { stdout: currentBranchOutput } = await execAsync( "git rev-parse --abbrev-ref HEAD", diff --git a/apps/server/src/routes/worktree/routes/merge.ts b/apps/server/src/routes/worktree/routes/merge.ts index f9499d85..df109bc7 100644 --- a/apps/server/src/routes/worktree/routes/merge.ts +++ b/apps/server/src/routes/worktree/routes/merge.ts @@ -7,6 +7,7 @@ import { exec } from "child_process"; import { promisify } from "util"; import path from "path"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -29,6 +30,20 @@ export function createMergeHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const branchName = `feature/${featureId}`; // Git worktrees are stored in project directory const worktreePath = path.join(projectPath, ".worktrees", featureId); diff --git a/apps/server/src/routes/worktree/routes/open-in-editor.ts b/apps/server/src/routes/worktree/routes/open-in-editor.ts index 04f9815f..7eecaf7d 100644 --- a/apps/server/src/routes/worktree/routes/open-in-editor.ts +++ b/apps/server/src/routes/worktree/routes/open-in-editor.ts @@ -7,6 +7,7 @@ import type { Request, Response } from "express"; import { exec } from "child_process"; import { promisify } from "util"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -108,6 +109,20 @@ export function createOpenInEditorHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(worktreePath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const editor = await detectDefaultEditor(); try { diff --git a/apps/server/src/routes/worktree/routes/pull.ts b/apps/server/src/routes/worktree/routes/pull.ts index 119192d0..5150ae7e 100644 --- a/apps/server/src/routes/worktree/routes/pull.ts +++ b/apps/server/src/routes/worktree/routes/pull.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import { exec } from "child_process"; import { promisify } from "util"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -24,6 +25,20 @@ export function createPullHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(worktreePath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Get current branch name const { stdout: branchOutput } = await execAsync( "git rev-parse --abbrev-ref HEAD", diff --git a/apps/server/src/routes/worktree/routes/push.ts b/apps/server/src/routes/worktree/routes/push.ts index d9447a2b..bf95374c 100644 --- a/apps/server/src/routes/worktree/routes/push.ts +++ b/apps/server/src/routes/worktree/routes/push.ts @@ -6,6 +6,7 @@ import type { Request, Response } from "express"; import { exec } from "child_process"; import { promisify } from "util"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -25,6 +26,20 @@ export function createPushHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(worktreePath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Get branch name const { stdout: branchOutput } = await execAsync( "git rev-parse --abbrev-ref HEAD", diff --git a/apps/server/src/routes/worktree/routes/start-dev.ts b/apps/server/src/routes/worktree/routes/start-dev.ts index fcd0cec7..5a17b3fd 100644 --- a/apps/server/src/routes/worktree/routes/start-dev.ts +++ b/apps/server/src/routes/worktree/routes/start-dev.ts @@ -9,6 +9,7 @@ import type { Request, Response } from "express"; import { getDevServerService } from "../../../services/dev-server-service.js"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; export function createStartDevHandler() { return async (req: Request, res: Response): Promise => { @@ -34,6 +35,21 @@ export function createStartDevHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + validatePath(worktreePath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + const devServerService = getDevServerService(); const result = await devServerService.startDevServer(projectPath, worktreePath); diff --git a/apps/server/src/routes/worktree/routes/status.ts b/apps/server/src/routes/worktree/routes/status.ts index 3f56ef17..d317ad0f 100644 --- a/apps/server/src/routes/worktree/routes/status.ts +++ b/apps/server/src/routes/worktree/routes/status.ts @@ -8,6 +8,7 @@ import { promisify } from "util"; import path from "path"; import fs from "fs/promises"; import { getErrorMessage, logError } from "../common.js"; +import { validatePath, PathNotAllowedError } from "../../../lib/security.js"; const execAsync = promisify(exec); @@ -29,6 +30,20 @@ export function createStatusHandler() { return; } + // Validate paths are within ALLOWED_ROOT_DIRECTORY + try { + validatePath(projectPath); + } catch (error) { + if (error instanceof PathNotAllowedError) { + res.status(403).json({ + success: false, + error: error.message, + }); + return; + } + throw error; + } + // Git worktrees are stored in project directory const worktreePath = path.join(projectPath, ".worktrees", featureId); diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example index cdbc3346..99d7a5dd 100644 --- a/docker-compose.override.yml.example +++ b/docker-compose.override.yml.example @@ -8,7 +8,3 @@ services: # Set root directory for all projects and file operations # Users can only create/open projects within this directory - ALLOWED_ROOT_DIRECTORY=/projects - - # Optional: Set workspace directory for UI project discovery - # Falls back to ALLOWED_ROOT_DIRECTORY if not set - # - WORKSPACE_DIR=/projects