From d446da97178c4c5be377b7c4873ff697734140cc Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Fri, 13 Feb 2026 17:33:06 +0800 Subject: [PATCH] fix: allow file tree access on Windows cross-drive project paths FileTree component was not passing baseDir to the file API, causing it to fall back to restricting access to the home directory. On Windows, projects on a different drive (e.g., D:\) were blocked with 403. Changes: - FileTree now passes workingDirectory as baseDir parameter - File API routes no longer require baseDir to be under homeDir (allows cross-drive access on Windows) - Still prevents using filesystem root as baseDir for safety - Add isRootPath() utility for root path detection Co-Authored-By: Claude Opus 4.6 --- src/app/api/files/preview/route.ts | 13 +++++-------- src/app/api/files/route.ts | 16 ++++++++-------- src/components/project/FileTree.tsx | 2 +- src/lib/files.ts | 9 +++++++++ 4 files changed, 23 insertions(+), 17 deletions(-) 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);