fix: enforce ALLOWED_ROOT_DIRECTORY path validation across all routes

This fixes a critical security issue where path parameters from client requests
were not validated against ALLOWED_ROOT_DIRECTORY, allowing attackers to access
files and directories outside the configured root directory.

Changes:
- Add validatePath() checks to 29 route handlers that accept path parameters
- Validate paths in agent routes (workingDirectory, imagePaths)
- Validate paths in feature routes (projectPath)
- Validate paths in worktree routes (projectPath, worktreePath)
- Validate paths in git routes (projectPath, filePath)
- Validate paths in auto-mode routes (projectPath, worktreePath)
- Validate paths in settings/suggestions routes (projectPath)
- Return 403 Forbidden for paths outside ALLOWED_ROOT_DIRECTORY
- Maintain backward compatibility (unrestricted when env var not set)

Security Impact:
- Prevents directory traversal attacks
- Prevents unauthorized file access
- Prevents arbitrary code execution via unvalidated paths

All validation follows the existing pattern in fs routes and session creation,
using the validatePath() function from lib/security.ts which checks against
both ALLOWED_ROOT_DIRECTORY and DATA_DIR (appData).

Tests: All 653 unit tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Test User
2025-12-20 18:13:34 -05:00
parent 873429db19
commit ade80484bb
30 changed files with 449 additions and 10 deletions

View File

@@ -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 });

View File

@@ -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<void> => {
@@ -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) {

View File

@@ -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<void> => {
@@ -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" });

View File

@@ -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 });

View File

@@ -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<void> => {
@@ -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,