mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void> => {
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<void> => {
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user