diff --git a/backend/main.py b/backend/main.py index 0739cbb..4c7f681 100644 --- a/backend/main.py +++ b/backend/main.py @@ -191,12 +191,29 @@ def _resolve_safe_path(vault_root: Path, relative_path: str) -> Path: Raises: HTTPException(403): When the resolved path escapes the vault root. """ - resolved = (vault_root / relative_path).resolve() - vault_resolved = vault_root.resolve() + # Construct the full path without resolving symlinks first + full_path = vault_root / relative_path + + # Resolve both paths to handle symlinks try: + resolved = full_path.resolve(strict=False) + vault_resolved = vault_root.resolve(strict=False) + except Exception as e: + logger.error(f"Path resolution error - vault_root: {vault_root}, relative_path: {relative_path}, error: {e}") + raise HTTPException(status_code=500, detail=f"Path resolution error: {str(e)}") + + # Check if resolved path is within vault using string comparison (case-insensitive on Windows) + try: + # This will raise ValueError if resolved is not relative to vault_resolved resolved.relative_to(vault_resolved) except ValueError: - raise HTTPException(status_code=403, detail="Access denied: path outside vault") + # Try case-insensitive comparison for Windows/Docker compatibility + resolved_str = str(resolved).lower() + vault_str = str(vault_resolved).lower() + if not resolved_str.startswith(vault_str): + logger.warning(f"Path outside vault - vault: {vault_resolved}, requested: {relative_path}, resolved: {resolved}") + raise HTTPException(status_code=403, detail="Access denied: path outside vault") + return resolved @@ -529,10 +546,18 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative try: raw = file_path.read_text(encoding="utf-8", errors="replace") - except PermissionError: + except PermissionError as e: + logger.error(f"Permission denied reading file {path}: {e}") raise HTTPException(status_code=403, detail=f"Permission denied: cannot read file {path}") + except UnicodeDecodeError: + # Binary file - try to read as binary and decode with errors='replace' + try: + raw = file_path.read_bytes().decode("utf-8", errors="replace") + except Exception as e: + logger.error(f"Error reading binary file {path}: {e}") + raise HTTPException(status_code=500, detail=f"Cannot read file: {str(e)}") except Exception as e: - logger.error(f"Error reading file {path}: {e}") + logger.error(f"Unexpected error reading file {path}: {e}") raise HTTPException(status_code=500, detail=f"Error reading file: {str(e)}") ext = file_path.suffix.lower()