Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Chromoting
DescriptionIncorrect authorization in Chromoting
ComponentChromoting
Bug ClassLogic Error
Tracker517798926
Fix commit8941f7c9070f (chromium/src) +82/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-01

Background

Chromoting
Chrome Remote Desktop’s host component, which exposes host services to local clients over a named Mojo IPC pipe.
Named Mojo Pipe Server
A Windows named-pipe endpoint (named_mojo_ipc_server) that brokers Mojo IPC connections between the Chromoting host process and connecting client processes.
`WTSQueryUserToken`
A Windows Terminal Services API that retrieves the access token of the user logged into a given session, requiring SE_TCB_NAME privilege held by LocalSystem.
User SID
The Security Identifier uniquely naming a Windows user account, used here to confirm a connecting process belongs to the legitimate session owner.

Root Cause Analysis

On Windows, IsTrustedMojoEndpoint in remoting/host/mojo_caller_security_checker.cc authorized a connecting Mojo client solely by verifying that the caller’s process image was code-signed and trusted via IsBinaryTrusted(caller_process_image_path). This check confirmed what binary connected but never confirmed who owned the connecting process, so any local user running a trusted, code-signed Chromoting binary could connect to a host service running in another user’s session, violating the invariant that a client must belong to the same session user the host is serving. Additionally, the Named Mojo Pipe Server did not mark client invitations as untrusted, leaving the Mojo broker reachable to a connecting process that should not have full channel-broker capabilities.

The fix adds IsWinCallerUserSidValid, which computes the expected session-owner SID (via WTSQueryUserToken when running as LocalSystem, or the current process user as a developer fallback) and rejects any caller whose process-token User SID does not match. It also sets MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS on invitations so the broker restricts what the client can do. Together these restore the requirement that a client be both a trusted binary and owned by the correct session user.

Key insight
The core mistake was authorizing the Mojo peer by binary trust alone while omitting any identity check on the caller’s owning user, so a low-privileged local user could impersonate a legitimate client to a host serving a different session. The fix closes this by enforcing a session-aware User SID match (IsWinCallerUserSidValid) in addition to the binary-trust check, and by marking invitations MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS.

Attack Path

  1. Local foothold An attacker runs an unprivileged process in their own Windows logon session on a machine where the Chromoting host service runs as LocalSystem.
  2. Locate the pipe The attacker connects to the host’s named Mojo pipe exposed by the Named Mojo Pipe Server, which serves another session’s user.
  3. Pass the weak check Because authorization only required IsBinaryTrusted, the attacker leverages a trusted, code-signed Chromoting binary so the endpoint accepts the connection despite the caller belonging to a different user.
  4. Cross-session access The host treats the attacker’s process as a legitimate client and grants access to host-services IPC that should have been scoped to the active session user.
  5. Privilege escalation By interacting with services brokered by the LocalSystem-owned host, the attacker gains capabilities beyond their own account, escalating local privilege.

Impact Assessment

An attacker with a low-privileged local account gains unauthorized access to Chromoting host services running in a different Windows session, potentially served by a LocalSystem host process, enabling local privilege escalation. This occurs on Windows in the host-services IPC channel exposed by the Named Mojo Pipe Server. Preconditions are local code execution on the machine, a running Chromoting host, and possession of (or ability to run) a code-signed trusted Chromoting binary to satisfy the pre-fix binary check.

Changed Functions

FunctionChangeNotes
if
remoting/host/BUILD.gn
modified
if
remoting/host/mojo_caller_security_checker.cc
modified

Files Changed

  • remoting/host/BUILD.gn
  • remoting/host/chromoting_host_services_server.cc
  • remoting/host/mojo_caller_security_checker.cc

Audit Directions

  • Binary trust without identity
    Flag any IPC authorization that trusts a caller based on process image signing (IsBinaryTrusted) without also verifying the caller’s owning user or session SID.
  • Cross-session Windows IPC
    Review named-pipe and Mojo endpoints reachable across Windows sessions to ensure they validate the connecting process token against the intended session owner, e.g. via WTSQueryUserToken and base::win::AccessToken::FromProcess.
  • Unrestricted Mojo invitations
    Check that Mojo invitations sent to less-privileged clients set MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS to limit broker capabilities granted to the peer.
From 8941f7c9070f1e9e6c7cd465f1072e1700630e36 Mon Sep 17 00:00:00 2001
From: Yuwei Huang <[email protected]>
Date: Mon, 01 Jun 2026 13:25:44 -0700
Subject: [PATCH] Harden Windows Named Mojo Pipe Server for Chromoting Host Services

Harden the Named Mojo Pipe Server on Windows against potential local
privilege escalation.

Mitigations:
*   Set MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS on client
    invitations to restrict Mojo channel broker access.
*   Enforce session-aware User SID verification of the client process.
*   Add a safe fallback for local user-mode developer environments.
*   Use move semantics for token extraction and explicitly link
    wtsapi32.lib.

Bug: 517798926
Change-Id: I406538b61990fd35084904f433cfc7f37cbbb171
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7886655
Auto-Submit: Yuwei Huang <[email protected]>
Reviewed-by: Joe Downing <[email protected]>
Commit-Queue: Yuwei Huang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1639627}
---

diff --git a/remoting/host/BUILD.gn b/remoting/host/BUILD.gn
index 302f780a..5a78f0fb 100644
--- a/remoting/host/BUILD.gn
+++ b/remoting/host/BUILD.gn
@@ -356,6 +356,7 @@
   }
   if (is_win) {
     deps += [ "//remoting/host/win:trust_util" ]
+    libs = [ "wtsapi32.lib" ]
   }
 }
 
diff --git a/remoting/host/chromoting_host_services_server.cc b/remoting/host/chromoting_host_services_server.cc
index 77c6ce0..8cc4dce 100644
--- a/remoting/host/chromoting_host_services_server.cc
+++ b/remoting/host/chromoting_host_services_server.cc
@@ -23,6 +23,7 @@
 #if BUILDFLAG(IS_WIN)
 #include "base/strings/strcat_win.h"
 #include "base/win/win_util.h"
+#include "mojo/public/c/system/invitation.h"
 #endif
 
 namespace remoting {
@@ -44,6 +45,8 @@
   options.security_descriptor =
       base::StrCat({L"O:", user_sid, L"G:", user_sid, L"D:(A;;GA;;;AU)"});
   options.include_peer_process_info = true;
+  options.extra_send_invitation_flags =
+      MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS;
 #elif BUILDFLAG(IS_LINUX)
   // Allow the endpoint to be connected by any users iff the server is run as
   // root.
diff --git a/remoting/host/mojo_caller_security_checker.cc b/remoting/host/mojo_caller_security_checker.cc
index b0080bb..20bddec 100644
--- a/remoting/host/mojo_caller_security_checker.cc
+++ b/remoting/host/mojo_caller_security_checker.cc
@@ -26,6 +26,14 @@
 #endif
 
 #if BUILDFLAG(IS_WIN)
+#include <windows.h>
+
+#include <wtsapi32.h>
+
+#include "base/strings/utf_string_conversions.h"
+#include "base/win/access_token.h"
+#include "base/win/scoped_handle.h"
+#include "base/win/sid.h"
 #include "remoting/host/win/trust_util.h"
 #endif
 
@@ -56,6 +64,72 @@
     std::to_array<const std::string_view>({kBundleId, "remote_webauthn"});
 #endif
 
+#if BUILDFLAG(IS_WIN)
+bool IsWinCallerUserSidValid(
+    const named_mojo_ipc_server::ConnectionInfo& caller) {
+  std::optional<base::win::AccessToken> current_token =
+      base::win::AccessToken::FromCurrentProcess();
+  if (!current_token.has_value()) {
+    PLOG(ERROR) << "Failed to open current process token.";
+    return false;
+  }
+
+  std::optional<base::win::Sid> expected_sid;
+
+  // Verify the client's identity depending on the host service context:
+  // - If running as `LocalSystem` (standard service/production mode), the
+  //   host has the necessary TCB privileges (`SE_TCB_NAME`) to query the
+  //   legitimate active session user's token via `WTSQueryUserToken`.
+  // - If running as a standard user process (developer diagnostics/testing
+  //   environments or developer unit tests), the host lacks TCB privileges
+  //   to query other session tokens. In this case, the active remote session's
+  //   owner is simply the current process owner itself, so we securely fall
+  //   back to validating the client against the current host process token.
+  if (current_token->User() ==
+      base::win::Sid::FromKnownSid(base::win::WellKnownSid::kLocalSystem)) {
+    // SYSTEM Mode: Retrieve the target Windows session owner's User SID.
+    HANDLE session_token_raw = nullptr;
+    if (!::WTSQueryUserToken(caller.session_id, &session_token_raw)) {
+      PLOG(ERROR) << "WTSQueryUserToken failed for session ID "
+                  << caller.session_id;
+      return false;
+    }
+    base::win::ScopedHandle session_token_handle(session_token_raw);
+
+    std::optional<base::win::AccessToken> session_token =
+        base::win::AccessToken::FromToken(std::move(session_token_handle));
+    if (!session_token.has_value()) {
+      PLOG(ERROR) << "Failed to get access token from session token handle.";
+      return false;
+    }
+    expected_sid = session_token->User();
+  } else {
+    // User/Developer Mode Fallback: Use current process User SID as the
+    // expected value.
+    expected_sid = current_token->User();
+  }
+
+  std::optional<base::win::AccessToken> client_token =
+      base::win::AccessToken::FromProcess(caller.process.Handle());
+  if (!client_token.has_value()) {
+    PLOG(ERROR) << "Failed to open client process token for PID " << caller.pid;
+    return false;
+  }
+  base::win::Sid client_sid = client_token->User();
+
+  if (client_sid != *expected_sid) {
+    LOG(ERROR) << "Client user SID ("
+               << base::WideToUTF8(client_sid.ToSddlString().value_or(L""))
+               << ") does not match expected user SID ("
+               << base::WideToUTF8(expected_sid->ToSddlString().value_or(L""))
+               << ")";
+    return false;
+  }
+
+  return true;
+}
+#endif  // BUILDFLAG(IS_WIN)
+
 }  // namespace
 
 bool IsTrustedMojoEndpoint(
@@ -114,7 +188,10 @@
     return false;
   }
 #if BUILDFLAG(IS_WIN)
-  return IsBinaryTrusted(caller_process_image_path);
+  if (!IsBinaryTrusted(caller_process_image_path)) {
+    return false;
+  }
+  return IsWinCallerUserSidValid(caller);
 #else
   // Linux binaries are not code-signed, so we just return true.
   return true;
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.