diff --git a/BrewBar/BrewBarApp.swift b/BrewBar/BrewBarApp.swift index 2f76d0c..c43e4e0 100644 --- a/BrewBar/BrewBarApp.swift +++ b/BrewBar/BrewBarApp.swift @@ -145,22 +145,29 @@ class AppDelegate: NSObject, NSApplicationDelegate { return "brew" } - /// Written once per launch; sudo runs it to show a GUI password dialog - /// whenever brew needs admin rights (no terminal is attached to Process). - static let askpassURL: URL = { + /// Rewritten before each brew command so the sudo password dialog can say + /// which command needs it. Sudo runs this script to show a GUI password + /// prompt whenever brew needs admin rights (no terminal is attached to + /// Process). If two brew commands overlap, the later one's text wins — + /// harmless, since prompts realistically only appear during upgrades. + static func writeAskpassScript(for command: String) -> URL { let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] .appendingPathComponent("BrewBar", isDirectory: true) try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + // the text lands inside shell single quotes AND an AppleScript string; + // whitelist characters that cannot break out of either + let safeCommand = command.filter { $0.isLetter || $0.isNumber || " ._@+=:/-".contains($0) } + let url = dir.appendingPathComponent("askpass.sh") let script = """ #!/bin/zsh - osascript -e 'display dialog "BrewBar needs your administrator password to continue:" default answer "" with hidden answer with title "BrewBar" with icon caution buttons {"Cancel", "OK"} default button "OK"' -e 'text returned of result' + osascript -e 'display dialog "BrewBar needs your administrator password to run:\\n\\nbrew \(safeCommand)" default answer "" with hidden answer with title "BrewBar" with icon caution buttons {"Cancel", "OK"} default button "OK"' -e 'text returned of result' """ try? script.write(to: url, atomically: true, encoding: .utf8) try? FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: url.path) return url - }() + } func runBrew(_ command: String, completion: @escaping (String) -> Void = { _ in }) { DispatchQueue.global().async { @@ -177,7 +184,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { process.arguments = arguments var environment = ProcessInfo.processInfo.environment - environment["SUDO_ASKPASS"] = Self.askpassURL.path + environment["SUDO_ASKPASS"] = Self.writeAskpassScript(for: cleaned).path process.environment = environment let pipe = Pipe()