Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Chromoting
DescriptionIncorrect authorization in Chromoting
ComponentChromoting
Bug ClassLogic Error
Tracker518053893
Fix commitb0ac65436bc6 (chromium/src) +179/-60
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
remoting/BUILD.gn
modified
if
remoting/host/base/BUILD.gn
modified
if
remoting/host/base/process_util.cc
modified
ScopedStdHandle
remoting/host/base/process_util_win_unittest.cc
modified
TEST
remoting/host/base/process_util_win_unittest.cc
modified

Files Changed

  • remoting/BUILD.gn
  • remoting/host/base/BUILD.gn
  • remoting/host/base/process_util.cc
  • remoting/host/base/process_util.h
  • remoting/host/base/process_util_win_unittest.cc
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),
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • remoting/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.

  1. A local attacker running at Medium Integrity Level (Medium-IL) opens a handle to an official, running or suspended instance of chrome.exe with PROCESS_CREATE_PROCESS and PROCESS_DUP_HANDLE permissions.
  2. The attacker creates standard anonymous pipes and uses DuplicateHandle to place the read/write ends into the target chrome.exe’s handle table as inheritable handles.
  3. The attacker calls CreateProcess to launch remote_webauthn.exe (from the Chrome Remote Desktop installation directory), specifying the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute pointing to the chrome.exe handle, and redirecting the standard input/output handles to the duplicated pipe handles.
  4. When remote_webauthn.exe starts, base::GetParentProcessId() returns the PID of the spoofed chrome.exe parent.
  5. GetProcessImagePath resolves this PID to the authentic path C:\Program Files\Google\Chrome\Application\chrome.exe.
  6. IsBinaryTrusted verifies the signature of the genuine chrome.exe binary, which succeeds.
  7. The validation passes, allowing the attacker to communicate directly with the verified remote_webauthn.exe instance 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:

  1. 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.
  2. 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.

View on issue tracker