diff --git a/src/app/api/files/preview/route.ts b/src/app/api/files/preview/route.ts index 83cdf37..e490025 100644 --- a/src/app/api/files/preview/route.ts +++ b/src/app/api/files/preview/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import path from 'path'; import os from 'os'; -import { readFilePreview, isPathSafe } from '@/lib/files'; +import { readFilePreview, isPathSafe, isRootPath } from '@/lib/files'; import type { FilePreviewResponse, ErrorResponse } from '@/types'; export async function GET(request: NextRequest) { @@ -20,17 +20,14 @@ export async function GET(request: NextRequest) { const homeDir = os.homedir(); // Validate that the file is within the session's working directory. - // The baseDir parameter should be the session's working directory, - // which acts as the trust boundary for file access. - // The baseDir itself must be under the user's home directory to prevent - // attackers from setting baseDir=/ to bypass all restrictions. + // baseDir may be on a different drive than homeDir on Windows. + // Only reject root paths as baseDir to prevent full-disk access. const baseDir = searchParams.get('baseDir'); if (baseDir) { const resolvedBase = path.resolve(baseDir); - // Ensure baseDir is within the home directory (prevent baseDir=/ bypass) - if (!isPathSafe(homeDir, resolvedBase)) { + if (isRootPath(resolvedBase)) { return NextResponse.json( - { error: 'Base directory is outside the allowed scope' }, + { error: 'Cannot use filesystem root as base directory' }, { status: 403 } ); } diff --git a/src/app/api/files/route.ts b/src/app/api/files/route.ts index ff000ab..00aa5da 100644 --- a/src/app/api/files/route.ts +++ b/src/app/api/files/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import path from 'path'; import os from 'os'; -import { scanDirectory, isPathSafe } from '@/lib/files'; +import { scanDirectory, isPathSafe, isRootPath } from '@/lib/files'; import type { FileTreeResponse, ErrorResponse } from '@/types'; export async function GET(request: NextRequest) { @@ -20,17 +20,17 @@ export async function GET(request: NextRequest) { const homeDir = os.homedir(); // Use baseDir (the session's working directory) as the trust boundary. - // The baseDir itself must be under the user's home directory to prevent - // attackers from setting baseDir=/ to bypass all restrictions. - // If no baseDir is provided, fall back to the user's home directory - // to prevent scanning arbitrary system directories. + // baseDir is the project root the user explicitly chose — it may be on + // a different drive than the home directory on Windows (e.g., D:\projects). + // We only reject root paths (/, C:\) as baseDir to prevent full-disk scans. + // If no baseDir is provided, fall back to the user's home directory. const baseDir = searchParams.get('baseDir'); if (baseDir) { const resolvedBase = path.resolve(baseDir); - // Ensure baseDir is within the home directory (prevent baseDir=/ bypass) - if (!isPathSafe(homeDir, resolvedBase)) { + // Prevent using a filesystem root as baseDir (e.g., /, C:\) + if (isRootPath(resolvedBase)) { return NextResponse.json( - { error: 'Base directory is outside the allowed scope' }, + { error: 'Cannot use filesystem root as base directory' }, { status: 403 } ); } diff --git a/src/components/project/FileTree.tsx b/src/components/project/FileTree.tsx index 13c202f..1f23daf 100644 --- a/src/components/project/FileTree.tsx +++ b/src/components/project/FileTree.tsx @@ -118,7 +118,7 @@ export function FileTree({ workingDirectory, onFileSelect, onFileAdd }: FileTree setLoading(true); try { const res = await fetch( - `/api/files?dir=${encodeURIComponent(workingDirectory)}&depth=4` + `/api/files?dir=${encodeURIComponent(workingDirectory)}&baseDir=${encodeURIComponent(workingDirectory)}&depth=4` ); if (res.ok) { const data = await res.json(); diff --git a/src/lib/files.ts b/src/lib/files.ts index f83ea3d..2503218 100644 --- a/src/lib/files.ts +++ b/src/lib/files.ts @@ -74,6 +74,15 @@ export function isPathSafe(basePath: string, targetPath: string): boolean { return resolvedTarget.startsWith(resolvedBase + path.sep) || resolvedTarget === resolvedBase; } +/** + * Check if a path is a filesystem root (e.g., `/`, `C:\`, `D:\`). + * Used to prevent using root as a baseDir for file browsing. + */ +export function isRootPath(p: string): boolean { + const resolved = path.resolve(p); + return resolved === path.parse(resolved).root; +} + export async function scanDirectory(dir: string, depth: number = 3): Promise { const resolvedDir = path.resolve(dir);