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 <noreply@anthropic.com>
This commit is contained in:
1 parent
6aca8c3870
commit
d446da9717
4 files changed
+23
-17
No files matched your search
@@ -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<ErrorResponse>(
|
||||
{ error: 'Base directory is outside the allowed scope' },
|
||||
{ error: 'Cannot use filesystem root as base directory' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<ErrorResponse>(
|
||||
{ error: 'Base directory is outside the allowed scope' },
|
||||
{ error: 'Cannot use filesystem root as base directory' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<FileTreeNode[]> {
|
||||
const resolvedDir = path.resolve(dir);
|
||||
|
||||
|
||||
Reference in new issue
Block a user