From 5c480ca515b6be2422c367482aac3a97bede13ef Mon Sep 17 00:00:00 2001 From: Nick Bolton Date: Thu, 23 Apr 2026 17:47:45 +0100 Subject: [PATCH] fix(cve): run switch commands as normal user on Windows to prevent privilege escalation CVE-2026-41477 --- src/apps/deskflow-daemon/deskflow-daemon.cpp | 1 + src/lib/deskflow/CMakeLists.txt | 4 ++ src/lib/deskflow/Screen.cpp | 41 +++++++++++++---- src/lib/platform/MSWindowsProcess.cpp | 46 ++++++++++++++++++++ src/lib/platform/MSWindowsProcess.h | 4 ++ src/unittests/deskflow/CMakeLists.txt | 2 +- 6 files changed, 89 insertions(+), 9 deletions(-) diff --git a/src/apps/deskflow-daemon/deskflow-daemon.cpp b/src/apps/deskflow-daemon/deskflow-daemon.cpp index ac0c0e513..c88912d42 100644 --- a/src/apps/deskflow-daemon/deskflow-daemon.cpp +++ b/src/apps/deskflow-daemon/deskflow-daemon.cpp @@ -73,6 +73,7 @@ int main(int argc, char **argv) // It's important to write the version number to the log file so we can be certain the old daemon // was uninstalled, since sometimes Windows services can get stuck and fail to be removed. LOG_PRINT("%s v%s", qPrintable(QCoreApplication::applicationName()), kDisplayVersion); + LOG_INFO("settings file: %s", qPrintable(Settings::settingsFile())); // Default log level to system setting (found in Registry). auto logLevel = Settings::value(Settings::Daemon::LogLevel).toString().toStdString(); diff --git a/src/lib/deskflow/CMakeLists.txt b/src/lib/deskflow/CMakeLists.txt index a0bc4c236..13581adbc 100644 --- a/src/lib/deskflow/CMakeLists.txt +++ b/src/lib/deskflow/CMakeLists.txt @@ -91,6 +91,10 @@ add_library(${lib_name} STATIC ${PLATFORM_CODE} target_link_libraries(${lib_name} PUBLIC common Qt6::Core Qt6::Network) +if(WIN32) + target_link_libraries(${lib_name} PRIVATE platform) +endif() + if(UNIX) target_link_libraries( ${lib_name} diff --git a/src/lib/deskflow/Screen.cpp b/src/lib/deskflow/Screen.cpp index 7524c6d6a..0483fa1ad 100644 --- a/src/lib/deskflow/Screen.cpp +++ b/src/lib/deskflow/Screen.cpp @@ -12,8 +12,35 @@ #include +#ifdef Q_OS_WIN +#include "arch/win32/ArchMiscWindows.h" +#include "platform/MSWindowsProcess.h" +#endif + namespace deskflow { +namespace { + +bool runScreenCommand(const QString &commandLine) +{ +#ifdef Q_OS_WIN + using deskflow::platform::MSWindowsProcess; + if (ArchMiscWindows::isProcessElevated()) { + LOG_DEBUG("current process is elevated, starting detached process as session user"); + return MSWindowsProcess::startDetachedAsSessionUser(commandLine.toStdWString()); + } +#endif + + auto args = QProcess::splitCommand(commandLine); + if (args.isEmpty()) { + return false; + } + const auto program = args.takeFirst(); + return QProcess::startDetached(program, args); +} + +} // namespace + // // Screen // @@ -123,10 +150,9 @@ void Screen::enter(KeyModifierMask toggleMask) } if (Settings::value(Settings::Core::EnableEnterCommand).toBool()) { - auto args = QProcess::splitCommand(Settings::value(Settings::Core::ScreenEnterCommand).toString()); - const auto command = args.takeFirst(); - LOG_DEBUG("running screen enter command: %s %s", qPrintable(command), qPrintable(args.join(" "))); - if (!QProcess::startDetached(command, args)) + const auto commandLine = Settings::value(Settings::Core::ScreenEnterCommand).toString(); + LOG_DEBUG("running screen enter command: %s", qPrintable(commandLine)); + if (!runScreenCommand(commandLine)) LOG_ERR("failed to run screen enter command"); } } @@ -151,10 +177,9 @@ bool Screen::leave() m_screen->leave(); if (Settings::value(Settings::Core::EnableExitCommand).toBool()) { - auto args = QProcess::splitCommand(Settings::value(Settings::Core::ScreenExitCommand).toString()); - const auto command = args.takeFirst(); - LOG_DEBUG("running screen exit command: %s %s", qPrintable(command), qPrintable(args.join(" "))); - if (!QProcess::startDetached(command, args)) + const auto commandLine = Settings::value(Settings::Core::ScreenExitCommand).toString(); + LOG_DEBUG("running screen exit command: %s", qPrintable(commandLine)); + if (!runScreenCommand(commandLine)) LOG_ERR("failed to run screen exit command"); } diff --git a/src/lib/platform/MSWindowsProcess.cpp b/src/lib/platform/MSWindowsProcess.cpp index d676d0ac9..108790175 100644 --- a/src/lib/platform/MSWindowsProcess.cpp +++ b/src/lib/platform/MSWindowsProcess.cpp @@ -11,6 +11,7 @@ #include "base/Log.h" #include "common/Constants.h" #include "common/ExitCodes.h" +#include "platform/MSWindowsSession.h" #define WIN32_LEAN_AND_MEAN #include @@ -188,6 +189,51 @@ void MSWindowsProcess::shutdown(HANDLE handle, DWORD pid, int timeout) } } +bool MSWindowsProcess::startDetachedAsSessionUser(const std::wstring &command) +{ + MSWindowsSession session; + session.updateActiveSession(); + + HANDLE userToken = nullptr; + try { + userToken = session.getUserToken(nullptr); + } catch (const std::runtime_error &e) { + LOG_ERR("could not get session user token: %s", e.what()); + return false; + } + + LPVOID environment = nullptr; + if (!CreateEnvironmentBlock(&environment, userToken, FALSE)) { + LOG_ERR("could not create environment block, error: %s", windowsErrorToString(GetLastError()).c_str()); + CloseHandle(userToken); + return false; + } + + STARTUPINFOW si = {}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi = {}; + + std::wstring mutableCommand = command; + const DWORD flags = CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE; + + LOG_DEBUG("starting detached process as session user, command: %s", command.c_str()); + const BOOL ok = CreateProcessAsUserW( + userToken, nullptr, mutableCommand.data(), nullptr, nullptr, FALSE, flags, environment, nullptr, &si, &pi + ); + + DestroyEnvironmentBlock(environment); + CloseHandle(userToken); + + if (!ok) { + LOG_ERR("could not start process as session user, error: %s", windowsErrorToString(GetLastError()).c_str()); + return false; + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return true; +} + void MSWindowsProcess::createPipes() { SECURITY_ATTRIBUTES saAttr; diff --git a/src/lib/platform/MSWindowsProcess.h b/src/lib/platform/MSWindowsProcess.h index 243dd9a4d..efb8bf205 100644 --- a/src/lib/platform/MSWindowsProcess.h +++ b/src/lib/platform/MSWindowsProcess.h @@ -38,6 +38,10 @@ public: static void shutdown(HANDLE handle, DWORD pid, int timeout = kDefaultShutdownTimeout); + /// Launch @p command as a detached process under the active console user's token, so the + /// child does not inherit the caller's (SYSTEM) privileges. + static bool startDetachedAsSessionUser(const std::wstring &command); + private: void setStartupInfo(STARTUPINFO &si); diff --git a/src/unittests/deskflow/CMakeLists.txt b/src/unittests/deskflow/CMakeLists.txt index 937a3573f..f3514ff99 100644 --- a/src/unittests/deskflow/CMakeLists.txt +++ b/src/unittests/deskflow/CMakeLists.txt @@ -2,7 +2,7 @@ # SPDX-License-Identifier: MIT if(WIN32) - set(extra_libs version) + set(extra_libs platform version) endif() create_test(