* @brief Resolve the given raw command into a proper command string for CreateProcess(). * @details This converts URLs and non-executable file paths into a runnable command like ShellExecute(). * @param raw_cmd The raw command provided by the user. * @param working_dir The working directory for the new process. * @param token The user token currently being impersonated or `nullptr` if
| 742 | * @return A command string suitable for use by CreateProcess(). |
| 743 | */ |
| 744 | std::wstring resolve_command_string(const std::string &raw_cmd, const std::wstring &working_dir, HANDLE token, DWORD &creation_flags) { |
| 745 | std::wstring raw_cmd_w = from_utf8(raw_cmd); |
| 746 | |
| 747 | // First, convert the given command into parts so we can get the executable/file/URL without parameters |
| 748 | auto raw_cmd_parts = boost::program_options::split_winmain(raw_cmd_w); |
| 749 | if (raw_cmd_parts.empty()) { |
| 750 | // This is highly unexpected, but we'll just return the raw string and hope for the best. |
| 751 | BOOST_LOG(warning) << "Failed to split command string: "sv << raw_cmd; |
| 752 | return from_utf8(raw_cmd); |
| 753 | } |
| 754 | |
| 755 | auto raw_target = raw_cmd_parts.at(0); |
| 756 | if (PathIsURLW(raw_target.c_str())) { |
| 757 | // If the target is a URL, handle it directly with rundll32.exe |
| 758 | std::wstring cmd = L"rundll32.exe url.dll,FileProtocolHandler " + raw_target; |
| 759 | return cmd; |
| 760 | } |
| 761 | |
| 762 | // If the target is not a URL, assume it's a regular file path |
| 763 | auto extension = PathFindExtensionW(raw_target.c_str()); |
| 764 | if (extension == nullptr || *extension == 0) { |
| 765 | // If the file has no extension, assume it's a command and allow CreateProcess() |
| 766 | // to try to find it via PATH |
| 767 | return from_utf8(raw_cmd); |
| 768 | } |
| 769 | else if (boost::iequals(extension, L".exe")) { |
| 770 | // If the file has an .exe extension, we will bypass the resolution here and |
| 771 | // directly pass the unmodified command string to CreateProcess(). The argument |
| 772 | // escaping rules are subtly different between CreateProcess() and ShellExecute(), |
| 773 | // and we want to preserve backwards compatibility with older configs. |
| 774 | return from_utf8(raw_cmd); |
| 775 | } |
| 776 | |
| 777 | // For regular files, the class is found using the file extension (including the dot) |
| 778 | std::wstring lookup_string = extension; |
| 779 | HRESULT res; |
| 780 | |
| 781 | std::array<WCHAR, MAX_PATH> shell_command_string; |
| 782 | bool needs_cmd_escaping = false; |
| 783 | { |
| 784 | // Overriding these predefined keys affects process-wide state, so serialize all calls |
| 785 | // to ensure the handle state is consistent while we perform the command query. |
| 786 | static std::mutex per_user_key_mutex; |
| 787 | auto lg = std::lock_guard(per_user_key_mutex); |
| 788 | |
| 789 | // Override HKEY_CLASSES_ROOT and HKEY_CURRENT_USER to ensure we query the correct class info |
| 790 | if (!override_per_user_predefined_keys(token)) { |
| 791 | return from_utf8(raw_cmd); |
| 792 | } |
| 793 | |
| 794 | // Find the command string for the specified class |
| 795 | DWORD out_len = shell_command_string.size(); |
| 796 | res = AssocQueryStringW(ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND, lookup_string.c_str(), L"open", shell_command_string.data(), &out_len); |
| 797 | |
| 798 | // In some cases (UWP apps), we might not have a command for this target. If that happens, |
| 799 | // we'll have to launch via cmd.exe. This prevents proper job tracking, but that was already |
| 800 | // broken for UWP apps anyway due to how they are started by Windows. Even 'start /wait' |
| 801 | // doesn't work properly for UWP, so really no termination tracking seems to work at all. |
no test coverage detected