CVE-2026-78892
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifremoting/BUILD.gn |
modified | |
ifremoting/host/base/BUILD.gn |
modified | |
ifremoting/host/base/process_util.cc |
modified | |
ScopedStdHandleremoting/host/base/process_util_win_unittest.cc |
modified | |
TESTremoting/host/base/process_util_win_unittest.cc |
modified |
Files Changed
remoting/BUILD.gnremoting/host/base/BUILD.gnremoting/host/base/process_util.ccremoting/host/base/process_util.hremoting/host/base/process_util_win_unittest.cc
Patch
From b0ac65436bc652d8a5d2c91115ba28c60654d4f9 Mon Sep 17 00:00:00 2001 From: Yuwei Huang <[email protected]> Date: Tue, 07 Jul 2026 16:30:56 -0700 Subject: [PATCH] [remoting][win] Identify WebAuthn NMH launcher via stdio pipes We previously tried to identify the real NMH launcher by checking if the parent is CMD then looking at its grandparent. It turns out Chrome actually creates a pair of named pipes, namely `\\.\pipe\chrome.nativeMessaging.in.<token>` and `\\.\pipe\chrome.nativeMessaging.out.<token>`, then passes them to the NMH via the cmd command `cmd.exe /d /s /c $COMMAND < $IN_PIPE > $OUT_PIPE`, which means the named pipes are directly forwarded to the NMH. It is much easier and safer to just identify the server PID of the named pipes than trying to figure out the what the grandparent process is, so this CL implements the new approach. Bug: 518053893 Change-Id: I50fb1cb3731f508d058a63baa4864aacb62bf330 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8057588 Auto-Submit: Yuwei Huang <[email protected]> Reviewed-by: Joe Downing <[email protected]> Commit-Queue: Yuwei Huang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1658366} --- diff --git a/remoting/BUILD.gn b/remoting/BUILD.gn index 0cc19b5..99aca082 100644 --- a/remoting/BUILD.gn +++ b/remoting/BUILD.gn @@ -110,6 +110,7 @@ deps += [ "//remoting/codec:unit_tests", "//remoting/host:unit_tests", + "//remoting/host/base:unit_tests", "//ui/gfx", ] if (is_posix) { diff --git a/remoting/host/base/BUILD.gn b/remoting/host/base/BUILD.gn index e487d2d..f4d6a0b 100644 --- a/remoting/host/base/BUILD.gn +++ b/remoting/host/base/BUILD.gn @@ -54,4 +54,8 @@ "//testing/gtest", "//third_party/webrtc_overrides:webrtc_component", ] + + if (is_win) { + sources += [ "process_util_win_unittest.cc" ] + } } diff --git a/remoting/host/base/process_util.cc b/remoting/host/base/process_util.cc index 3302774..592c742 100644 --- a/remoting/host/base/process_util.cc +++ b/remoting/host/base/process_util.cc @@ -83,4 +83,45 @@ #endif } +#if BUILDFLAG(IS_WIN) +base::ProcessId GetLauncherProcessIdFromPipes(HANDLE stdin_handle, + HANDLE stdout_handle) { + if (stdin_handle == INVALID_HANDLE_VALUE || stdin_handle == nullptr || + stdout_handle == INVALID_HANDLE_VALUE || stdout_handle == nullptr) { + return base::kNullProcessId; + } + + if (::GetFileType(stdin_handle) != FILE_TYPE_PIPE || + ::GetFileType(stdout_handle) != FILE_TYPE_PIPE) { + return base::kNullProcessId; + } + + ULONG stdin_server_pid = 0; + if (!::GetNamedPipeServerProcessId(stdin_handle, &stdin_server_pid)) { + PLOG(ERROR) << "GetNamedPipeServerProcessId failed for stdin"; + return base::kNullProcessId; + } + + ULONG stdout_server_pid = 0; + if (!::GetNamedPipeServerProcessId(stdout_handle, &stdout_server_pid)) { + PLOG(ERROR) << "GetNamedPipeServerProcessId failed for stdout"; + return base::kNullProcessId; + } + + if (stdin_server_pid != stdout_server_pid) { + LOG(ERROR) + << "stdin and stdout pipes belong to different server processes (" + << stdin_server_pid << " vs " << stdout_server_pid << ")"; + return base::kNullProcessId; + } + + return static_cast<base::ProcessId>(stdin_server_pid); +} + +base::ProcessId GetLauncherProcessIdFromStdioPipes() { + return GetLauncherProcessIdFromPipes(::GetStdHandle(STD_INPUT_HANDLE), + ::GetStdHandle(STD_OUTPUT_HANDLE)); +} +#endif + } // namespace remoting diff --git a/remoting/host/base/process_util.h b/remoting/host/base/process_util.h index 8f6367b..86fb48e5 100644 --- a/remoting/host/base/process_util.h +++ b/remoting/host/base/process_util.h @@ -8,6 +8,11 @@ #include "base/files/file_path.h" #include "base/process/process.h" #include "base/process/process_handle.h" +#include "build/build_config.h" + +#if BUILDFLAG(IS_WIN) +#include "base/win/windows_types.h" +#endif namespace remoting { @@ -19,6 +24,19 @@ // Same as above but using an existing process handle. base::FilePath GetProcessImagePath(const base::Process& process); +#if BUILDFLAG(IS_WIN) +// Returns the process ID of the named-pipe server connected to the specified +// input and output pipe handles. Returns base::kNullProcessId if either handle +// is invalid, not a named pipe, or connected to different named-pipe servers. +base::ProcessId GetLauncherProcessIdFromPipes(HANDLE stdin_handle, + HANDLE stdout_handle); + +// Returns the process ID of the named-pipe server connected to standard input +// and standard output. Returns base::kNullProcessId if stdin or stdout is not +// a named pipe, or if they are connected to different named-pipe servers. +base::ProcessId GetLauncherProcessIdFromStdioPipes(); +#endif + } // namespace remoting #endif // REMOTING_HOST_BASE_PROCESS_UTIL_H_ diff --git a/remoting/host/base/process_util_win_unittest.cc b/remoting/host/base/process_util_win_unittest.cc new file mode 100644 index 0000000..2886447d --- /dev/null +++ b/remoting/host/base/process_util_win_unittest.cc @@ -0,0 +1,104 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "remoting/host/base/process_util.h" + +#include <windows.h> + +#include "base/process/process.h" +#include "base/process/process_handle.h" +#include "base/win/scoped_handle.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace remoting { + +namespace { + +class ScopedStdHandle { + public: + ScopedStdHandle(DWORD std_handle, HANDLE new_handle) + : std_handle_(std_handle), old_handle_(::GetStdHandle(std_handle)) { + ::SetStdHandle(std_handle_, new_handle); + } + + ~ScopedStdHandle() { ::SetStdHandle(std_handle_, old_handle_); } + + private: + DWORD std_handle_; + HANDLE old_handle_; +}; + +} // namespace + +TEST(ProcessUtilWinTest, NonPipeStdinReturnsNull) { + base::win::ScopedHandle event(::CreateEvent(nullptr, TRUE, FALSE, nullptr)); + ASSERT_TRUE(event.is_valid()); + ASSERT_EQ(GetLauncherProcessIdFromPipes(event.get(), event.get()), + base::kNullProcessId); + + ScopedStdHandle scoped_stdin(STD_INPUT_HANDLE, event.get()); + ASSERT_EQ(GetLauncherProcessIdFromStdioPipes(), base::kNullProcessId); +} + +TEST(ProcessUtilWinTest, PipeServerPidResolution) { + HANDLE read_pipe = nullptr; + HANDLE write_pipe = nullptr; + SECURITY_ATTRIBUTES sa = {}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + ASSERT_TRUE(::CreatePipe(&read_pipe, &write_pipe, &sa, 0)); + + base::win::ScopedHandle scoped_read(read_pipe); + base::win::ScopedHandle scoped_write(write_pipe); + + ASSERT_EQ(GetLauncherProcessIdFromPipes(read_pipe, write_pipe),
Regression Test / PoC
diff --git a/remoting/host/base/process_util_win_unittest.cc b/remoting/host/base/process_util_win_unittest.cc
new file mode 100644
index 0000000..2886447d
--- /dev/null
+++ b/remoting/host/base/process_util_win_unittest.cc
@@ -0,0 +1,104 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "remoting/host/base/process_util.h"
+
+#include <windows.h>
+
+#include "base/process/process.h"
+#include "base/process/process_handle.h"
+#include "base/win/scoped_handle.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace remoting {
+
+namespace {
+
+class ScopedStdHandle {
+ public:
+ ScopedStdHandle(DWORD std_handle, HANDLE new_handle)
+ : std_handle_(std_handle), old_handle_(::GetStdHandle(std_handle)) {
+ ::SetStdHandle(std_handle_, new_handle);
+ }
+
+ ~ScopedStdHandle() { ::SetStdHandle(std_handle_, old_handle_); }
+
+ private:
+ DWORD std_handle_;
+ HANDLE old_handle_;
+};
+
+} // namespace
+
+TEST(ProcessUtilWinTest, NonPipeStdinReturnsNull) {
+ base::win::ScopedHandle event(::CreateEvent(nullptr, TRUE, FALSE, nullptr));
+ ASSERT_TRUE(event.is_valid());
+ ASSERT_EQ(GetLauncherProcessIdFromPipes(event.get(), event.get()),
+ base::kNullProcessId);
+
+ ScopedStdHandle scoped_stdin(STD_INPUT_HANDLE, event.get());
+ ASSERT_EQ(GetLauncherProcessIdFromStdioPipes(), base::kNullProcessId);
+}
+
+TEST(ProcessUtilWinTest, PipeServerPidResolution) {
+ HANDLE read_pipe = nullptr;
+ HANDLE write_pipe = nullptr;
+ SECURITY_ATTRIBUTES sa = {};
+ sa.nLength = sizeof(sa);
+ sa.bInheritHandle = TRUE;
+ ASSERT_TRUE(::CreatePipe(&read_pipe, &write_pipe, &sa, 0));
+
+ base::win::ScopedHandle scoped_read(read_pipe);
+ base::win::ScopedHandle scoped_write(write_pipe);
+
+ ASSERT_EQ(GetLauncherProcessIdFromPipes(read_pipe, write_pipe),
+ base::GetCurrentProcId());
+
+ ScopedStdHandle scoped_stdin(STD_INPUT_HANDLE, read_pipe);
+ ScopedStdHandle scoped_stdout(STD_OUTPUT_HANDLE, write_pipe);
+ ASSERT_EQ(GetLauncherProcessIdFromStdioPipes(), base::GetCurrentProcId());
+}
+
+TEST(ProcessUtilWinTest, NullOrInvalidHandlesReturnNull) {
+ HANDLE read_pipe = nullptr;
+ HANDLE write_pipe = nullptr;
+ SECURITY_ATTRIBUTES sa = {};
+ sa.nLength = sizeof(sa);
+ sa.bInheritHandle = TRUE;
+ ASSERT_TRUE(::CreatePipe(&read_pipe, &write_pipe, &sa, 0));
+
+ base::win::ScopedHandle scoped_read(read_pipe);
+ base::win::ScopedHandle scoped_write(write_pipe);
+
+ ASSERT_EQ(GetLauncherProcessIdFromPipes(nullptr, write_pipe),
+ base::kNullProcessId);
+ ASSERT_EQ(GetLauncherProcessIdFromPipes(read_pipe, nullptr),
+ base::kNullProcessId);
+ ASSERT_EQ(GetLauncherProcessIdFromPipes(INVALID_HANDLE_VALUE, write_pipe),
+ base::kNullProcessId);
+ ASSERT_EQ(GetLauncherProcessIdFromPipes(read_pipe, INVALID_HANDLE_VALUE),
+ base::kNullProcessId);
+}
+
+TEST(ProcessUtilWinTest, MismatchedOrNonPipeHandlesReturnNull) {
+ HANDLE read_pipe = nullptr;
+ HANDLE write_pipe = nullptr;
+ SECURITY_ATTRIBUTES sa = {};
+ sa.nLength = sizeof(sa);
+ sa.bInheritHandle = TRUE;
+ ASSERT_TRUE(::CreatePipe(&read_pipe, &write_pipe, &sa, 0));
+
+ base::win::ScopedHandle scoped_read(read_pipe);
+ base::win::ScopedHandle scoped_write(write_pipe);
+
+ base::win::ScopedHandle event(::CreateEvent(nullptr, TRUE, FALSE, nullptr));
+ ASSERT_TRUE(event.is_valid());
+
+ ASSERT_EQ(GetLauncherProcessIdFromPipes(read_pipe, event.get()),
+ base::kNullProcessId);
+ ASSERT_EQ(GetLauncherProcessIdFromPipes(event.get(), write_pipe),
+ base::kNullProcessId);
+}
+
+} // namespace remoting
Original Bug Report
Potential security bypass in IsLaunchedByTrustedProcess via parent PID spoofing on Windows
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: On Windows, the Chrome Remote Desktop remote WebAuthn host relies on parent PID verification to ensure it was launched by a trusted browser process. Because parent PIDs can be spoofed during process creation, a local attacker can bypass these path and signature checks to interact with the WebAuthn proxy. This allows unauthorized local processes to drive remote WebAuthn operations over an active session.
Affected files:
remoting/host/webauthn/remote_webauthn_caller_security_utils.ccremoting/host/webauthn/remote_webauthn_main.cc
Estimated timestamp from git blame: 2022-05-09
Potential Vulnerability Analysis
In remoting/host/webauthn/remote_webauthn_caller_security_utils.cc, the Windows implementation of IsLaunchedByTrustedProcess() verifies whether the process was initiated by a trusted browser process. It does this by querying its parent process ID:
base::ProcessId parent_pid =
base::GetParentProcessId(base::GetCurrentProcessHandle());
base::FilePath parent_image_path = GetProcessImagePath(parent_pid);
On Windows, base::GetParentProcessId() retrieves the parent PID by querying ProcessBasicInformation via NtQueryInformationProcess, which returns the InheritedFromUniqueProcessId field from the process’s PEB.
However, this value is not a cryptographically secure or kernel-verified indicator of the actual creator process. During process creation, an attacker can specify an arbitrary parent process using the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute via UpdateProcThreadAttribute(). When this attribute is specified, the newly created child process’s parent PID is set to the designated process, and it inherits handles from that designated process’s handle table instead of the actual creator’s.
By leveraging this mechanism, a local attacker could potentially launch remote_webauthn.exe with its parent spoofed to a genuine, signed chrome.exe instance, thereby bypassing the path and signature checks in IsLaunchedByTrustedProcess().
Potential Step-by-Step Scenario
Note: These are suggested/potential steps derived from code analysis; our tooling does not currently run active proof-of-concept exploits.
- A local attacker running at Medium Integrity Level (Medium-IL) opens a handle to an official, running or suspended instance of
chrome.exewithPROCESS_CREATE_PROCESSandPROCESS_DUP_HANDLEpermissions. - The attacker creates standard anonymous pipes and uses
DuplicateHandleto place the read/write ends into the targetchrome.exe’s handle table as inheritable handles. - The attacker calls
CreateProcessto launchremote_webauthn.exe(from the Chrome Remote Desktop installation directory), specifying thePROC_THREAD_ATTRIBUTE_PARENT_PROCESSattribute pointing to thechrome.exehandle, and redirecting the standard input/output handles to the duplicated pipe handles. - When
remote_webauthn.exestarts,base::GetParentProcessId()returns the PID of the spoofedchrome.exeparent. GetProcessImagePathresolves this PID to the authentic pathC:\Program Files\Google\Chrome\Application\chrome.exe.IsBinaryTrustedverifies the signature of the genuinechrome.exebinary, which succeeds.- The validation passes, allowing the attacker to communicate directly with the verified
remote_webauthn.exeinstance over the duplicated pipes and issue arbitrary WebAuthn requests over the active CRD connection.
Suggested Remediation
To address this security boundary weakness, avoid relying on Parent Process IDs (PPID) for authorization on Windows, as they are inherently spoofable by same-integrity processes.
Instead, consider the following design changes:
- Direct Mojo IPC: Rather than launching a standalone executable via standard I/O redirection and performing parental checks on startup, establish a secure Mojo IPC connection directly initiated from the browser process via a trusted broker.
- Job Objects or Process Mitigation Policies: If process tree verification is required, utilize robust sandboxing, job objects, or restricted tokens to ensure that child processes cannot be spawned with arbitrary parent assignments within the security context of the Chrome Remote Desktop host services.
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.