No clear best practice for privilege escalation in Swift CLI tool

Originator:brandon
Number:rdar://48587226 Date Originated:2019/03/04
Status:Open Resolved:
Product:macOS + SDK, Swift Product Version:
Classification:Suggestion Reproducible:
 
I'm writing a tool in Swift that automates downloading and installing Xcode. It's a reimplementation of https://github.com/xcpretty/xcode-install. The goal is to have a tool that can download and install Xcode, including any additional steps that are needed to be able to use the computer for development purposes. Some of these steps, like accepting Xcode license agreements or enabling developer mode, require superuser privileges. I understand that these specific tasks aren't all public API, but I believe that this problem generalizes to other situations where Swift is used as a scripting language to automate tasks on macOS.

It seems like there’s no good way to escalate privileges on macOS in a command-line app, especially with Swift. Because of this, I worry that the solution many people (including myself) will choose will be worse even than if something like system() or AuthorizationExecuteWithPrivileges were still available to use.

Options that I've considered are:

- sudo in Process: sudo doesn’t read from stdin so this doesn't work
- capture sudoer's password with readpassphrase and pipe to sudo -S in Process: doesn't make me feel great that I'm impersonating sudo and the passphrase is in my memory space for a time. Also requires checking sudo -nv in order to replicate the normal sudo behaviour when a sudoer has cached privileges.
- SMJobBless: requires an app bundle
- AuthorizationExecuteWithPrivileges: deprecated
- system(): unavailable in Swift
- Run the entire tool with sudo: unnecessary and unwanted. Perhaps I could drop the extra privileges when possible, but I haven't explored this.
- Reverse-engineer the privileged process and try to reimplement in a way if relevant API that I can use exists. For example, this could work for `xcodebuild -license agree` because I can use authopen to get a privileged file handle to /Library/Preferences/com.apple.dt.Xcode.plist. This probably won't work in all cases, and is a brittle solution.
- Use another language: In my concrete example, the original Ruby implementation does this trivially, but I'd like to use Swift instead.

I don't have a strong opinion about what the ideal API to solve this problem would be, and I don't have a lot of security experience to guide a suggestion. The API I want probably looks like AuthorizationExecuteWithPrivileges, where I can describe a command or Process that is run with elevated privileges if the user permits. Another option that would align with SMJobBless would be to add API that allows registering a privileged helper that isn't in an app bundle but with the same code signing requirements. At that point perhaps the same launchd on-demand launching and XPC mechanisms could be used. I'd rather not have to ship two executables though, so this loses the convenience SBJobBless has for app bundles.

Comments

Re: No clear best practice for privilege escalation in Swift CLI tool

Regarding the "sudo doesn’t read from stdin so this doesn't work", this is likely happens because Swift's Process uses posix_spawn(2), which has no support for setting terminal process group TPGID, at least on Darwin.

An easy fix would be to call tcsetpgrp(3) after process.run() as follows:

tcsetpgrp(STDIN_FILENO, process.processIdentifier)

See https://stackoverflow.com/questions/76088356/process-cannot-read-from-the-standard-input-in-swift for more details.


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!