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

@@ -13,6 +13,7 @@ import { readImageAsBase64 } from "../lib/image-handler.js";
import { buildPromptWithImages } from "../lib/prompt-builder.js";
import { createChatOptions } from "../lib/sdk-options.js";
import { isAbortError } from "../lib/error-handler.js";
import { isPathAllowed, PathNotAllowedError } from "../lib/security.js";
interface Message {
id: string;
@@ -80,11 +81,20 @@ export class AgentService {
const metadata = await this.loadMetadata();
const sessionMetadata = metadata[sessionId];
// Determine the effective working directory
const effectiveWorkingDirectory = workingDirectory || process.cwd();
const resolvedWorkingDirectory = path.resolve(effectiveWorkingDirectory);
// Validate that the working directory is allowed
if (!isPathAllowed(resolvedWorkingDirectory)) {
throw new PathNotAllowedError(effectiveWorkingDirectory);
}
this.sessions.set(sessionId, {
messages,
isRunning: false,
abortController: null,
workingDirectory: workingDirectory || process.cwd(),
workingDirectory: resolvedWorkingDirectory,
sdkSessionId: sessionMetadata?.sdkSessionId, // Load persisted SDK session ID
});
}
@@ -461,11 +471,28 @@ export class AgentService {
const sessionId = this.generateId();
const metadata = await this.loadMetadata();
// Determine the effective working directory
const effectiveWorkingDirectory = workingDirectory || projectPath || process.cwd();
const resolvedWorkingDirectory = path.resolve(effectiveWorkingDirectory);
// Validate that the working directory is allowed
if (!isPathAllowed(resolvedWorkingDirectory)) {
throw new PathNotAllowedError(effectiveWorkingDirectory);
}
// Validate that projectPath is allowed if provided
if (projectPath) {
const resolvedProjectPath = path.resolve(projectPath);
if (!isPathAllowed(resolvedProjectPath)) {
throw new PathNotAllowedError(projectPath);
}
}
const session: SessionMetadata = {
id: sessionId,
name,
projectPath,
workingDirectory: workingDirectory || projectPath || process.cwd(),
workingDirectory: resolvedWorkingDirectory,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
model,

View File

@@ -24,6 +24,7 @@ import { resolveDependencies, areDependenciesSatisfied } from "../lib/dependency
import type { Feature } from "./feature-loader.js";
import { FeatureLoader } from "./feature-loader.js";
import { getFeatureDir, getAutomakerDir, getFeaturesDir, getContextDir } from "../lib/automaker-paths.js";
import { isPathAllowed, PathNotAllowedError } from "../lib/security.js";
const execAsync = promisify(exec);
@@ -486,6 +487,11 @@ export class AutoModeService {
this.runningFeatures.set(featureId, tempRunningFeature);
try {
// Validate that project path is allowed
if (!isPathAllowed(projectPath)) {
throw new PathNotAllowedError(projectPath);
}
// Check if feature has existing context - if so, resume instead of starting fresh
// Skip this check if we're already being called with a continuation prompt (from resumeFeature)
if (!options?.continuationPrompt) {
@@ -549,6 +555,11 @@ export class AutoModeService {
? path.resolve(worktreePath)
: path.resolve(projectPath);
// Validate that working directory is allowed
if (!isPathAllowed(workDir)) {
throw new PathNotAllowedError(workDir);
}
// Update running feature with actual worktree info
tempRunningFeature.worktreePath = worktreePath;
tempRunningFeature.branchName = branchName ?? null;