fix(tool): Windows UNC 路径规范 — PathValidator 路径比较一致性
Some checks are pending
CI / Lint & TypeCheck (push) Waiting to run
CI / Unit Tests (push) Waiting to run
CI / Build Frontend (push) Waiting to run
CI / Rust Check (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / E2E Tests (push) Blocked by required conditions

- with_workspace() 对 workspace_root 做 canonicalize,确保与
  resolve_and_validate 产出的 canonical 路径格式一致
- 新增 normalize_windows_path() 剥离 \?\ 前缀,解决 Windows 上
  starts_with 比较失败问题
- check_blocked/check_allowed 统一使用规范化路径比较
This commit is contained in:
iven
2026-04-24 17:02:24 +08:00
parent 855c89e8fb
commit 7b0d452845

View File

@@ -97,6 +97,17 @@ fn default_blocked_paths() -> Vec<PathBuf> {
] ]
} }
/// Normalize Windows UNC path prefix for consistent comparison.
/// `\\?\C:\Users\...` → `C:\Users\...`
fn normalize_windows_path(path: &Path) -> std::borrow::Cow<'_, Path> {
let s = path.to_string_lossy();
if s.starts_with(r"\\?\") {
std::borrow::Cow::Owned(PathBuf::from(&s[4..]))
} else {
std::borrow::Cow::Borrowed(path)
}
}
/// Expand tilde in path to home directory /// Expand tilde in path to home directory
fn expand_tilde(path: &str) -> PathBuf { fn expand_tilde(path: &str) -> PathBuf {
if path.starts_with('~') { if path.starts_with('~') {
@@ -154,9 +165,16 @@ impl PathValidator {
} }
} }
/// Set the workspace root directory /// Set the workspace root directory.
/// Canonicalizes the path to ensure consistent comparison on Windows
/// (where canonicalize() returns `\\?\C:\...` UNC paths).
pub fn with_workspace(mut self, workspace: PathBuf) -> Self { pub fn with_workspace(mut self, workspace: PathBuf) -> Self {
self.workspace_root = Some(workspace); let canonical = if workspace.exists() {
workspace.canonicalize().unwrap_or(workspace)
} else {
workspace
};
self.workspace_root = Some(canonical);
self self
} }
@@ -287,10 +305,14 @@ impl PathValidator {
Ok(()) Ok(())
} }
/// Check if path is in blocked list /// Check if path is in blocked list.
/// Normalizes Windows UNC prefix (`\\?\`) for consistent comparison.
fn check_blocked(&self, path: &Path) -> Result<()> { fn check_blocked(&self, path: &Path) -> Result<()> {
// Strip Windows UNC prefix for consistent matching
let normalized = normalize_windows_path(path);
for blocked in &self.config.blocked_paths { for blocked in &self.config.blocked_paths {
if path.starts_with(blocked) || path == blocked { let blocked_norm = normalize_windows_path(blocked);
if normalized.starts_with(&*blocked_norm) || normalized == blocked_norm {
return Err(ZclawError::InvalidInput(format!( return Err(ZclawError::InvalidInput(format!(
"Access to this path is blocked: {}", "Access to this path is blocked: {}",
path.display() path.display()
@@ -310,11 +332,15 @@ impl PathValidator {
/// - This prevents accidental exposure of the entire filesystem /// - This prevents accidental exposure of the entire filesystem
/// when the validator is misconfigured or used without setup /// when the validator is misconfigured or used without setup
fn check_allowed(&self, path: &Path) -> Result<()> { fn check_allowed(&self, path: &Path) -> Result<()> {
let path_norm = normalize_windows_path(path);
// If no allowed paths specified, check workspace // If no allowed paths specified, check workspace
if self.config.allowed_paths.is_empty() { if self.config.allowed_paths.is_empty() {
if let Some(ref workspace) = self.workspace_root { if let Some(ref workspace) = self.workspace_root {
// Workspace is configured - validate path is within it // Workspace is configured - validate path is within it
if !path.starts_with(workspace) { // Both sides are canonicalized (workspace via with_workspace, path via resolve_and_validate)
let ws_norm = normalize_windows_path(workspace);
if !path_norm.starts_with(&*ws_norm) {
return Err(ZclawError::InvalidInput(format!( return Err(ZclawError::InvalidInput(format!(
"Path outside workspace: {} (workspace: {})", "Path outside workspace: {} (workspace: {})",
path.display(), path.display(),
@@ -336,7 +362,8 @@ impl PathValidator {
// Check against allowed paths // Check against allowed paths
for allowed in &self.config.allowed_paths { for allowed in &self.config.allowed_paths {
if path.starts_with(allowed) { let allowed_norm = normalize_windows_path(allowed);
if path_norm.starts_with(&*allowed_norm) {
return Ok(()); return Ok(());
} }
} }