Skip to content

Uninstaller: validate app-bundle deletion + refuse protected apps#113

Open
iliyami wants to merge 1 commit into
mainfrom
fix/uninstaller-validation
Open

Uninstaller: validate app-bundle deletion + refuse protected apps#113
iliyami wants to merge 1 commit into
mainfrom
fix/uninstaller-validation

Conversation

@iliyami

@iliyami iliyami commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Second finding from the security review. The uninstaller's app-bundle deletion was the one delete path that skipped the app's own safety rails.

Findings (deep dive)

  • UninstallerView.uninstall() trashed the bundle with try? FileManager.default.trashItem(at: app.path, ...): no validatePath, no protected-app re-check, error swallowed. (The associated files already went through the validated engine; only the bundle was raw.)
  • Key insight: SafetyGuard.validatePath is path-based (protected paths / SIP / symlink), while isProtectedApp is bundle-id based. So neither validatePath nor routing through the engine protects a protected app, that check must be explicit. Today it was only the UI hiding the button.

Fix

  • New pure, testable AppUninstaller.plan(): refuses protected apps (bundle-id) and returns a clean plan that includes the bundle as a FileItem.
  • uninstall() routes the bundle through the same validated/logged/trash-first CleaningEngine as its leftovers, and surfaces failures in an alert instead of discarding the result with _ =.

Testing (TDD)

  • Failing-first tests: protected app (com.apple.finder) is refused; the bundle is routed through the engine and always selected; leftovers preserved.
  • check-version-sync (1.16.2), swift build clean, swift test 612 passed / 3 skipped / 0 failures.

Both security-review findings (this + the shredder #112) are now addressed.

Security review found the uninstaller trashed the app bundle with a raw
`try? FileManager.default.trashItem(...)` that skipped SafetyGuard, never
re-checked protected apps (only the UI hid the button), and swallowed
errors. Note SafetyGuard.validatePath is path-based and can't catch a
protected *app* (protection is by bundle id), so that guard has to be
explicit.

- Extract AppUninstaller.plan(): refuses protected apps (bundle-id check)
  and builds a clean plan that includes the bundle as a FileItem.
- uninstall() now routes the bundle through the same validated, logged,
  trash-first CleaningEngine as its leftovers (no more raw trashItem), and
  surfaces failures in an alert instead of discarding the result.
- Regression tests: protected app is refused; the bundle is routed through
  the engine and always selected; leftovers are preserved.

Bump to 1.16.2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant