From 974a71ad25af93c5a7caa23d5cf9a1a2c295c58b Mon Sep 17 00:00:00 2001 From: Matias Palma Date: Mon, 20 Apr 2026 22:24:29 -0400 Subject: [PATCH] fix: validate safe.directory path before writing to global git config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit git_add_safe_directory forwarded the caller-supplied path straight to `git config --global --add safe.directory`. Because the target config is global and persistent, any junk that made it through — most dangerously the `*` wildcard — auto-whitelisted every repository on disk, meaning any repo opened afterwards could run its pre/post hooks without the usual dubious-ownership prompt. Validation added before shelling out: - Reject empty input, `*`, and any value starting with `-` (so it can't be reparsed as a git flag like `--global --get`). - `Path::canonicalize` the path and require it to be an existing directory. The frontend only ever calls this with the current workspace root, which is a real directory on disk, so legitimate usage is unchanged. - Strip the `\\?\` verbatim prefix on Windows before handing the value to `git config`, since git stores entries in plain form and doesn't recognize the UNC-verbatim variant. The user-facing confirmation dialog in GitExplorerComponent already shows the path being added; canonicalization now makes that dialog match what actually lands in the global config. --- apps/desktop/src-tauri/src/lib.rs | 45 ++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index bff02176..fc2e4e5b 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -882,8 +882,51 @@ async fn get_recursive_file_list(root_path: String) -> Result, Strin #[tauri::command] async fn git_add_safe_directory(path: String) -> Result { + // `git config --global --add safe.directory ` permanently whitelists + // the value for any future git invocation on this machine. If the frontend + // can be tricked into supplying `*` or an unrelated directory, the entry + // lands in the user's global config and auto-executes hooks in every repo + // opened from that point on. So we validate before touching `git`. + + // Reject the wildcard and any obvious git option syntax up front — these + // never correspond to a real directory on disk and exist only to bypass + // safe.directory's repo-ownership check globally. + let trimmed = path.trim(); + if trimmed.is_empty() { + return Err("safe.directory path is empty".to_string()); + } + if trimmed == "*" { + return Err( + "Refusing to add `*` to safe.directory; that disables git ownership checks for every repository".to_string(), + ); + } + if trimmed.starts_with('-') { + return Err( + "safe.directory path cannot start with `-` (would be parsed as a git flag)".to_string(), + ); + } + + // Canonicalize so the caller can only whitelist a real directory on disk. + // Also collapses `..`/symlinks into a single absolute form before we hand + // it to `git config`, so the entry written to the user's config file is + // the one they actually saw in the UI confirmation dialog. + let canonical = std::path::Path::new(trimmed) + .canonicalize() + .map_err(|e| format!("Failed to resolve safe.directory path: {}", e))?; + + if !canonical.is_dir() { + return Err("safe.directory target must be a directory".to_string()); + } + + // Strip the `\\?\` verbatim prefix on Windows; git stores entries in + // forward-slash form and doesn't recognize the UNC-verbatim variant. + let canonical_str = canonical.to_string_lossy(); + let cleaned = canonical_str + .strip_prefix(r"\\?\") + .unwrap_or(&canonical_str); + let output = silent_command("git") - .args(["config", "--global", "--add", "safe.directory", &path]) + .args(["config", "--global", "--add", "safe.directory", cleaned]) .output() .map_err(|e| e.to_string())?;