From 060c48c48dea6fdeb974c787207c2d88dfffcfea Mon Sep 17 00:00:00 2001 From: Tw93 Date: Sat, 17 Jan 2026 09:49:42 +0800 Subject: [PATCH] refactor: enhance uninstall safety and fix dock removal - Add symlink/bundle_id/BOM validation to prevent injection attacks - Fix Dock removal for /Applications symlink (use pwd not pwd -P) - Fix brew uninstall test hanging (skip sudo in non-interactive mode) - Remove unused SENSITIVE_DATA_REGEX --- .gitignore | 1 + lib/core/app_protection.sh | 2 ++ lib/core/common.sh | 2 +- lib/core/file_ops.sh | 16 ++++++++++++---- lib/uninstall/batch.sh | 19 ------------------- lib/uninstall/brew.sh | 11 ++++++----- tests/uninstall.bats | 4 ---- 7 files changed, 22 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index 2c2eb10..a00769c 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ tests/tmp-* CLAUDE.md GEMINI.md ANTIGRAVITY.md +WARP.md .cursorrules # Go build artifacts (development) diff --git a/lib/core/app_protection.sh b/lib/core/app_protection.sh index bdd0147..05955cd 100755 --- a/lib/core/app_protection.sh +++ b/lib/core/app_protection.sh @@ -749,6 +749,8 @@ find_app_files() { # Launch Agents by name (special handling) # Note: LaunchDaemons are system-level and handled in find_app_system_files() + # Minimum 5-char threshold prevents false positives (e.g., "Time" matching system agents) + # Short-name apps (e.g., Zoom, Arc) are still cleaned via bundle_id matching above if [[ ${#app_name} -ge 5 ]] && [[ -d ~/Library/LaunchAgents ]]; then while IFS= read -r -d '' plist; do local plist_name=$(basename "$plist") diff --git a/lib/core/common.sh b/lib/core/common.sh index 99e66ca..ed3a225 100755 --- a/lib/core/common.sh +++ b/lib/core/common.sh @@ -133,7 +133,7 @@ remove_apps_from_dock() { fi if [[ -e "$app_path" ]]; then - if full_path=$(cd "$(dirname "$app_path")" 2>/dev/null && pwd -P); then + if full_path=$(cd "$(dirname "$app_path")" 2>/dev/null && pwd); then full_path="$full_path/$(basename "$app_path")" else continue diff --git a/lib/core/file_ops.sh b/lib/core/file_ops.sh index 7909cef..cc6ca8e 100644 --- a/lib/core/file_ops.sh +++ b/lib/core/file_ops.sh @@ -47,11 +47,19 @@ validate_path_for_deletion() { return 1 } - # If symlink points to absolute path, validate target - if [[ "$link_target" == /* ]]; then - case "$link_target" in + # Resolve relative symlinks to absolute paths for validation + local resolved_target="$link_target" + if [[ "$link_target" != /* ]]; then + local link_dir + link_dir=$(dirname "$path") + resolved_target=$(cd "$link_dir" 2>/dev/null && cd "$(dirname "$link_target")" 2>/dev/null && pwd)/$(basename "$link_target") || resolved_target="" + fi + + # Validate resolved target against protected paths + if [[ -n "$resolved_target" ]]; then + case "$resolved_target" in /System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*) - log_error "Symlink points to protected system path: $path -> $link_target" + log_error "Symlink points to protected system path: $path -> $resolved_target" return 1 ;; esac diff --git a/lib/uninstall/batch.sh b/lib/uninstall/batch.sh index 9d558bd..9a0951e 100755 --- a/lib/uninstall/batch.sh +++ b/lib/uninstall/batch.sh @@ -11,25 +11,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # Batch uninstall with a single confirmation. -# User data detection patterns (prompt user to backup if found). -readonly SENSITIVE_DATA_PATTERNS=( - "\.warp" # Warp terminal configs/themes - "/\.config/" # Standard Unix config directory - "/themes/" # Theme customizations - "/settings/" # Settings directories - "/Application Support/[^/]+/User Data" # Chrome/Electron user data - "/Preferences/[^/]+\.plist" # User preference files - "/Documents/" # User documents - "/\.ssh/" # SSH keys and configs (critical) - "/\.gnupg/" # GPG keys (critical) -) - -# Join patterns into a single regex for grep. -SENSITIVE_DATA_REGEX=$( - IFS='|' - echo "${SENSITIVE_DATA_PATTERNS[*]}" -) - # High-performance sensitive data detection (pure Bash, no subprocess) # Faster than grep for batch operations, especially when processing many apps has_sensitive_data() { diff --git a/lib/uninstall/brew.sh b/lib/uninstall/brew.sh index d7f2d01..2906e16 100644 --- a/lib/uninstall/brew.sh +++ b/lib/uninstall/brew.sh @@ -174,17 +174,18 @@ brew_uninstall_cask() { debug_log "Attempting brew uninstall --cask $cask_name" # Ensure we have sudo access if needed, to prevent brew from hanging on password prompt - if ! sudo -n true 2>/dev/null; then - sudo -v + if [[ "${NONINTERACTIVE:-}" != "1" && -t 0 && -t 1 ]]; then + if ! sudo -n true 2>/dev/null; then + sudo -v + fi fi local uninstall_ok=false local brew_exit=0 # Run with timeout to prevent hangs from problematic cask scripts - if run_with_timeout 300 \ - env HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_AUTO_UPDATE=1 NONINTERACTIVE=1 \ - brew uninstall --cask "$cask_name" 2>&1; then + if HOMEBREW_NO_ENV_HINTS=1 HOMEBREW_NO_AUTO_UPDATE=1 NONINTERACTIVE=1 \ + run_with_timeout 300 brew uninstall --cask "$cask_name" 2>&1; then uninstall_ok=true else brew_exit=$? diff --git a/tests/uninstall.bats b/tests/uninstall.bats index 59652d3..cd6b24e 100644 --- a/tests/uninstall.bats +++ b/tests/uninstall.bats @@ -39,8 +39,6 @@ create_app_artifacts() { mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState" mkdir -p "$HOME/Library/LaunchAgents" touch "$HOME/Library/LaunchAgents/com.example.TestApp.plist" - mkdir -p "$HOME/Library/LaunchDaemons" - touch "$HOME/Library/LaunchDaemons/com.example.TestApp.plist" } @test "find_app_files discovers user-level leftovers" { @@ -60,7 +58,6 @@ EOF [[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]] [[ "$result" == *"Containers/com.example.TestApp"* ]] [[ "$result" == *"LaunchAgents/com.example.TestApp.plist"* ]] - [[ "$result" == *"LaunchDaemons/com.example.TestApp.plist"* ]] } @test "calculate_total_size returns aggregate kilobytes" { @@ -121,7 +118,6 @@ batch_uninstall_applications [[ ! -d "$HOME/Library/Caches/TestApp" ]] || exit 1 [[ ! -f "$HOME/Library/Preferences/com.example.TestApp.plist" ]] || exit 1 [[ ! -f "$HOME/Library/LaunchAgents/com.example.TestApp.plist" ]] || exit 1 -[[ ! -f "$HOME/Library/LaunchDaemons/com.example.TestApp.plist" ]] || exit 1 EOF [ "$status" -eq 0 ]