CVE-2026-84334
Overview
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_NAMEprivilege held byLocalSystem. - 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.
IsWinCallerUserSidValid) in addition to the binary-trust check, and by marking invitations MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESS.Attack Path
- 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. - 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. - 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. - 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.
- 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
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
| Function | Change | Notes |
|---|---|---|
ifremoting/host/BUILD.gn |
modified | |
ifremoting/host/mojo_caller_security_checker.cc |
modified |
Files Changed
remoting/host/BUILD.gnremoting/host/chromoting_host_services_server.ccremoting/host/mojo_caller_security_checker.cc
Audit Directions
- Binary trust without identityFlag 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 IPCReview 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
WTSQueryUserTokenandbase::win::AccessToken::FromProcess. - Unrestricted Mojo invitationsCheck that Mojo invitations sent to less-privileged clients set
MOJO_SEND_INVITATION_FLAG_UNTRUSTED_PROCESSto limit broker capabilities granted to the peer.
Patch
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;