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

@@ -46,28 +46,31 @@ test.describe("Spec Editor Persistence", () => {
// Step 4: Click on the Spec Editor in the sidebar
await navigateToSpecEditor(page);
// Step 5: Wait for the spec editor to load
// Step 5: Wait for the spec view to load (not empty state)
await waitForElement(page, "spec-view", { timeout: 10000 });
// Step 6: Wait for the spec editor to load
const specEditor = await getByTestId(page, "spec-editor");
await specEditor.waitFor({ state: "visible", timeout: 10000 });
// Step 6: Wait for CodeMirror to initialize (it has a .cm-content element)
// Step 7: Wait for CodeMirror to initialize (it has a .cm-content element)
await specEditor.locator(".cm-content").waitFor({ state: "visible", timeout: 10000 });
// Step 7: Modify the editor content to "hello world"
// Step 8: Modify the editor content to "hello world"
await setEditorContent(page, "hello world");
// Verify content was set before saving
const contentBeforeSave = await getEditorContent(page);
expect(contentBeforeSave.trim()).toBe("hello world");
// Step 8: Click the save button and wait for save to complete
// Step 9: Click the save button and wait for save to complete
await clickSaveButton(page);
// Step 9: Refresh the page
// Step 10: Refresh the page
await page.reload();
await waitForNetworkIdle(page);
// Step 10: Navigate back to the spec editor
// Step 11: Navigate back to the spec editor
// After reload, we need to wait for the app to initialize
await waitForElement(page, "sidebar", { timeout: 10000 });
@@ -116,7 +119,7 @@ test.describe("Spec Editor Persistence", () => {
);
}
// Step 11: Verify the content was persisted
// Step 12: Verify the content was persisted
const persistedContent = await getEditorContent(page);
expect(persistedContent.trim()).toBe("hello world");
});

View File

@@ -37,8 +37,25 @@ export async function navigateToSpec(page: Page): Promise<void> {
await page.goto("/spec");
await page.waitForLoadState("networkidle");
// Wait for the spec view to be visible
await waitForElement(page, "spec-view", { timeout: 10000 });
// Wait for loading state to complete first (if present)
const loadingElement = page.locator('[data-testid="spec-view-loading"]');
try {
const loadingVisible = await loadingElement.isVisible({ timeout: 2000 });
if (loadingVisible) {
// Wait for loading to disappear (spec view or empty state will appear)
await loadingElement.waitFor({ state: "hidden", timeout: 10000 });
}
} catch {
// Loading element not found or already hidden, continue
}
// Wait for either the main spec view or empty state to be visible
// The spec-view element appears when loading is complete and spec exists
// The spec-view-empty element appears when loading is complete and spec doesn't exist
await Promise.race([
waitForElement(page, "spec-view", { timeout: 10000 }).catch(() => null),
waitForElement(page, "spec-view-empty", { timeout: 10000 }).catch(() => null),
]);
}
/**

View File

@@ -128,7 +128,7 @@ export async function waitForContextFile(
filename: string,
timeout: number = 10000
): Promise<void> {
const locator = await getByTestId(page, `context-file-${filename}`);
const locator = page.locator(`[data-testid="context-file-${filename}"]`);
await locator.waitFor({ state: "visible", timeout });
}

View File

@@ -103,9 +103,10 @@ test.describe("Worktree Integration Tests", () => {
const branchLabel = page.getByText("Branch:");
await expect(branchLabel).toBeVisible({ timeout: 10000 });
// Verify main branch button is displayed
const mainBranchButton = page.getByRole("button", { name: "main" });
await expect(mainBranchButton).toBeVisible({ timeout: 10000 });
// Wait for worktrees to load and main branch button to appear
// Use data-testid for more reliable selection
const mainBranchButton = page.locator('[data-testid="worktree-branch-main"]');
await expect(mainBranchButton).toBeVisible({ timeout: 15000 });
});
test("should select main branch by default when app loads with stale worktree data", async ({