mirror of
https://github.com/crabwise-ai/crabwalk.git
synced 2026-08-14 00:57:52 +00:00
fix(workspace): improve cross-platform path handling
Normalize path separators for consistent comparison across Windows and Unix systems. Fixes path traversal validation to handle backslash separators and corrects extension detection for dotfiles like .gitignore. - Add normalizeForComparison helper to convert backslashes to forward slashes - Fix isTextFile to properly handle files starting with dots - Add getParentDirPath utility for cross-platform directory navigation - Prevent false positives in path traversal detection on Windows
This commit is contained in:
+13
-4
@@ -30,10 +30,15 @@ export function validatePath(workspaceRoot: string, targetPath: string): string
|
||||
const resolvedRoot = path.resolve(workspaceRoot)
|
||||
const resolvedTarget = path.resolve(targetPath)
|
||||
|
||||
// Normalize paths for cross-platform comparison
|
||||
// Convert backslashes to forward slashes and ensure consistent formatting
|
||||
const normalizeForComparison = (p: string) => p.replace(/\\/g, '/').replace(/\/$/, '')
|
||||
const normalizedRoot = normalizeForComparison(resolvedRoot) + '/'
|
||||
const normalizedTarget = normalizeForComparison(resolvedTarget)
|
||||
|
||||
// Ensure target path is within root path by checking with trailing separator
|
||||
// This prevents bypasses like /home/user/workspace-evil matching /home/user/workspace
|
||||
const normalizedRoot = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep
|
||||
if (!resolvedTarget.startsWith(normalizedRoot) && resolvedTarget !== resolvedRoot) {
|
||||
if (!normalizedTarget.startsWith(normalizedRoot) && normalizedTarget !== normalizeForComparison(resolvedRoot)) {
|
||||
throw new Error('Path traversal detected: target path is outside workspace root')
|
||||
}
|
||||
|
||||
@@ -212,6 +217,10 @@ export function isTextFile(filename: string): boolean {
|
||||
'.gitignore',
|
||||
'.dockerignore',
|
||||
]
|
||||
const ext = path.extname(filename).toLowerCase()
|
||||
return textExtensions.includes(ext) || !ext.includes('.')
|
||||
// Get extension - handle files starting with dot (like .gitignore)
|
||||
// path.extname returns '' for files like 'Makefile' and '.gitignore'
|
||||
// We need to distinguish between extensionless files and dotfiles
|
||||
const lastDotIndex = filename.lastIndexOf('.')
|
||||
const ext = lastDotIndex > 0 ? path.extname(filename).toLowerCase() : ''
|
||||
return textExtensions.includes(ext) || ext === ''
|
||||
}
|
||||
|
||||
@@ -14,6 +14,19 @@ import { FileTree, MarkdownViewer } from '~/components/workspace'
|
||||
import { CrabIdleAnimation } from '~/components/ani'
|
||||
import type { DirectoryEntry } from '~/lib/workspace-fs'
|
||||
|
||||
// Get parent directory path using path separator logic
|
||||
// Works cross-platform for both / and \ separators
|
||||
function getParentDirPath(filePath: string): string {
|
||||
// Normalize to forward slashes for consistent processing
|
||||
const normalized = filePath.replace(/\\/g, '/')
|
||||
const lastSlashIndex = normalized.lastIndexOf('/')
|
||||
if (lastSlashIndex <= 0) {
|
||||
return filePath
|
||||
}
|
||||
// Return the original path up to the last separator
|
||||
return filePath.substring(0, lastSlashIndex)
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/workspace/')({
|
||||
component: WorkspacePageWrapper,
|
||||
})
|
||||
@@ -184,7 +197,7 @@ function WorkspacePage() {
|
||||
setSelectedFileContent(result.content)
|
||||
setSelectedFileName(result.name)
|
||||
// Get file metadata from the parent directory entry if available
|
||||
const parentDir = pathCache.get(selectedPath?.substring(0, selectedPath.lastIndexOf('/')) || workspacePath)
|
||||
const parentDir = pathCache.get(getParentDirPath(filePath) || workspacePath)
|
||||
const fileEntry = parentDir?.find(e => e.path === filePath)
|
||||
setSelectedFileSize(fileEntry?.size)
|
||||
setSelectedFileModified(fileEntry?.modifiedAt)
|
||||
|
||||
Reference in New Issue
Block a user