winget-cli icon indicating copy to clipboard operation
winget-cli copied to clipboard

Automatically drop elevation for "ElevationRequirement: elevationProhibited" pkgs

Open agowa opened this issue 1 year ago • 0 comments

Description of the new feature / enhancement

Winget should automatically drop privileges when trying to install a package that has "ElevationRequirement: elevationProhibited" specified (example package Spotify.Spotify). Currently winget just fails. This is bad from a UX perspective in two ways.

  1. winget currently requests elevation for every single app it tries to install and taht request them. This normally would be mitigated by just starting it from an elevated prompt or when ran from a script starting its process using the elevation flag set.
  2. When the approach from 1 is used winget will fail the installation without any way for a caller to prevent this. Also a caller has no way to know upfront which packages need and prohibit elevation. Therefore a caller only has the option to start winget without elevation, which will cause individual UAC prompts. However this can easily cause "security fatigue", e.g. users disabling the UAC prompts entirely and using silent elevation.

Proposed technical implementation details

Use ImpersonateLoggedOnUser to impersonate our own liked (LUA) access token (which we can acquire e.g. by calling CreateRestrictedToken with the LUA_TOKEN flag set as well as our own token from GetCurrentProcessToken). And from within that LUA context, we can call CreateProcessAsUser (instead of just CreateProcess, we however could just switch to always using CreateProcessAsUser and keep our code simple) and it will inherit the LUA token instead of the full one (avoiding the undesired "feature" of it that would allow the new process to access the executable and its directory using our full token).

Interestingly we cannot use the CreateProcessWithToken function, as it mandates the SeImpersonatePrivilege even for our case. Which is probably the only difference between it and the combination of CreateProcessAsUser and ImpersonateLoggedOnUser.

Note: Technically as we're running using the full token, we should have the SeImpersonatePrivilege, but

  1. edge cases exist using the e.g. "PowerUsers" group (does anything that supports winget still allow its usage?)
  2. security hardening measures within SOHO and enterprise may have removed Administrators from the allow list within secpol.
  3. It generates unnecessary and misleading audit events.

Edit: Technically using the above approach we could also solve the reverse use case of wanting to call multiple installers but only prompting once. We can just switch into the full token context (which will cause an UAC prompt) then install everything that needs (or basically anything that doesn't prohibit) full privileges and then switch back and destroy the privileged token (But we probably should adjust the ACLs to our own process first then, as otherwise any LUA process of the same user would be able to do stuff like e.g. copy our privileged token (which technically should be useless because of it's own ACLs) or try to inject code into our process/thread to get control over the execution...). HOWEVER these concerns are mostly irrelevant for now, as they do not apply to the full => LUA case.

Edit2: After looking into the source code of winget a bit more, it is currently using ShellExecuteExW and not CreateProcess as I assumed initially. This detail however shouldn't change the overall proposed technical implementation much.

Edit3: Relevant code sections:

  1. Where it currently fails: https://github.com/microsoft/winget-cli/blob/master/src/AppInstallerCLICore/Workflows/InstallFlow.cpp#L616
  2. Where the ShellExecuteExW Win32-API call would happen: https://github.com/microsoft/winget-cli/blob/master/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp#L38

agowa avatar Jan 19 '24 12:01 agowa