refactor: implement ALLOWED_ROOT_DIRECTORY security and fix path validation

This commit consolidates directory security from two environment variables
(WORKSPACE_DIR, ALLOWED_PROJECT_DIRS) into a single ALLOWED_ROOT_DIRECTORY variable
while maintaining backward compatibility.

Changes:
- Re-enabled path validation in security.ts (was previously disabled)
- Implemented isPathAllowed() to check ALLOWED_ROOT_DIRECTORY with DATA_DIR exception
- Added backward compatibility for legacy ALLOWED_PROJECT_DIRS and WORKSPACE_DIR
- Implemented path traversal protection via isPathWithinDirectory() helper
- Added PathNotAllowedError custom exception for security violations
- Updated all FS route endpoints to validate paths and return 403 on violation
- Updated template clone endpoint to validate project paths
- Updated workspace config endpoints to use ALLOWED_ROOT_DIRECTORY
- Fixed stat() response property access bug in project-init.ts
- Updated security tests to expect actual validation behavior

Security improvements:
- Path validation now enforced at all layers (routes, project init, agent services)
- appData directory (DATA_DIR) always allowed for settings/credentials
- Backward compatible with existing ALLOWED_PROJECT_DIRS/WORKSPACE_DIR configurations
- Protection against path traversal attacks

Backend test results: 654/654 passing 

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Test User
2025-12-20 15:59:32 -05:00
parent 7d0656bb14
commit 8ff4b5912a
25 changed files with 424 additions and 72 deletions

View File

@@ -4,13 +4,16 @@
import type { Request, Response } from "express";
import fs from "fs/promises";
import { addAllowedPath } from "../../../lib/security.js";
import path from "path";
import { addAllowedPath, getAllowedRootDirectory } from "../../../lib/security.js";
import { getErrorMessage, logError } from "../common.js";
export function createConfigHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
const workspaceDir = process.env.WORKSPACE_DIR;
// Prefer ALLOWED_ROOT_DIRECTORY, fall back to WORKSPACE_DIR for backward compatibility
const allowedRootDirectory = getAllowedRootDirectory();
const workspaceDir = process.env.WORKSPACE_DIR || allowedRootDirectory;
if (!workspaceDir) {
res.json({
@@ -22,29 +25,30 @@ export function createConfigHandler() {
// Check if the directory exists
try {
const stats = await fs.stat(workspaceDir);
const resolvedWorkspaceDir = path.resolve(workspaceDir);
const stats = await fs.stat(resolvedWorkspaceDir);
if (!stats.isDirectory()) {
res.json({
success: true,
configured: false,
error: "WORKSPACE_DIR is not a valid directory",
error: "Configured workspace directory is not a valid directory",
});
return;
}
// Add workspace dir to allowed paths
addAllowedPath(workspaceDir);
addAllowedPath(resolvedWorkspaceDir);
res.json({
success: true,
configured: true,
workspaceDir,
workspaceDir: resolvedWorkspaceDir,
});
} catch {
res.json({
success: true,
configured: false,
error: "WORKSPACE_DIR path does not exist",
error: "Configured workspace directory path does not exist",
});
}
} catch (error) {

View File

@@ -5,45 +5,49 @@
import type { Request, Response } from "express";
import fs from "fs/promises";
import path from "path";
import { addAllowedPath } from "../../../lib/security.js";
import { addAllowedPath, getAllowedRootDirectory } from "../../../lib/security.js";
import { getErrorMessage, logError } from "../common.js";
export function createDirectoriesHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
const workspaceDir = process.env.WORKSPACE_DIR;
// Prefer ALLOWED_ROOT_DIRECTORY, fall back to WORKSPACE_DIR for backward compatibility
const allowedRootDirectory = getAllowedRootDirectory();
const workspaceDir = process.env.WORKSPACE_DIR || allowedRootDirectory;
if (!workspaceDir) {
res.status(400).json({
success: false,
error: "WORKSPACE_DIR is not configured",
error: "Workspace directory is not configured (set ALLOWED_ROOT_DIRECTORY or WORKSPACE_DIR)",
});
return;
}
const resolvedWorkspaceDir = path.resolve(workspaceDir);
// Check if directory exists
try {
await fs.stat(workspaceDir);
await fs.stat(resolvedWorkspaceDir);
} catch {
res.status(400).json({
success: false,
error: "WORKSPACE_DIR path does not exist",
error: "Workspace directory path does not exist",
});
return;
}
// Add workspace dir to allowed paths
addAllowedPath(workspaceDir);
addAllowedPath(resolvedWorkspaceDir);
// Read directory contents
const entries = await fs.readdir(workspaceDir, { withFileTypes: true });
const entries = await fs.readdir(resolvedWorkspaceDir, { withFileTypes: true });
// Filter to directories only and map to result format
const directories = entries
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
.map((entry) => ({
name: entry.name,
path: path.join(workspaceDir, entry.name),
path: path.join(resolvedWorkspaceDir, entry.name),
}))
.sort((a, b) => a.name.localeCompare(b.name));