Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Chromoting
DescriptionInsufficient validation of untrusted input in Chromoting
ComponentChromoting
Bug ClassLogic Error
Tracker501709220
Fix commit91bc80321849 (chromium/src) +49/-21
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
GetUnprivilegedConfigKeys
remoting/host/setup/daemon_controller.cc
modified
for
remoting/host/setup/daemon_controller.cc
modified
for
remoting/host/setup/daemon_controller_delegate_win.cc
modified

Files Changed

  • remoting/host/setup/daemon_controller.cc
  • remoting/host/setup/daemon_controller.h
  • remoting/host/setup/daemon_controller_delegate_win.cc
From 91bc80321849517953b62ef1f7b92481cdec3578 Mon Sep 17 00:00:00 2001
From: Yuwei Huang <[email protected]>
Date: Thu, 16 Apr 2026 21:12:44 -0700
Subject: [PATCH] [crd host] Refactor readonly/unprivileged config keys check

Move the read-only key filtering logic from the Windows delegate into
DaemonController, so that it also applies to Mac and Linux. Also add a
check to DoGetConfig() to remove privileged keys in case they slip
through the cracks.

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

diff --git a/remoting/host/setup/daemon_controller.cc b/remoting/host/setup/daemon_controller.cc
index e72fb14f..0b0020c 100644
--- a/remoting/host/setup/daemon_controller.cc
+++ b/remoting/host/setup/daemon_controller.cc
@@ -7,20 +7,42 @@
 #include <memory>
 #include <utility>
 
+#include "base/containers/flat_set.h"
 #include "base/functional/bind.h"
 #include "base/location.h"
+#include "base/logging.h"
 #include "base/message_loop/message_pump_type.h"
+#include "base/no_destructor.h"
 #include "base/task/single_thread_task_runner.h"
 #include "base/values.h"
 #include "build/build_config.h"
 #include "remoting/base/auto_thread.h"
 #include "remoting/base/auto_thread_task_runner.h"
+#include "remoting/host/host_config.h"
 
 namespace remoting {
 
+namespace {
+
 // Name of the Daemon Controller's worker thread.
 const char kDaemonControllerThreadName[] = "Daemon Controller thread";
 
+// The configuration keys that cannot be specified in UpdateConfig().
+const char* const kReadonlyKeys[] = {
+    kHostIdConfigPath, kHostOwnerConfigPath, kServiceAccountConfigPath,
+    kDeprecatedXmppLoginConfigPath, kDeprecatedHostOwnerEmailConfigPath};
+
+}  // namespace
+
+// static
+const base::flat_set<std::string_view>&
+DaemonController::GetUnprivilegedConfigKeys() {
+  static base::NoDestructor<base::flat_set<std::string_view>> unprivileged_keys(
+      {kHostIdConfigPath, kServiceAccountConfigPath,
+       kDeprecatedXmppLoginConfigPath, kUsageStatsConsentConfigPath});
+  return *unprivileged_keys;
+}
+
 DaemonController::DaemonController(std::unique_ptr<Delegate> delegate)
     : caller_task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()),
       delegate_(std::move(delegate)) {
@@ -75,6 +97,14 @@
                                     CompletionCallback done) {
   DCHECK(caller_task_runner_->BelongsToCurrentThread());
 
+  for (const char* key : kReadonlyKeys) {
+    if (config.Find(key)) {
+      LOG(ERROR) << "Cannot update config: '" << key << "' is read-only.";
+      std::move(done).Run(RESULT_FAILED);
+      return;
+    }
+  }
+
   CompletionCallback wrapped_done =
       base::BindOnce(&DaemonController::InvokeCompletionCallbackAndScheduleNext,
                      this, std::move(done));
@@ -119,6 +149,17 @@
   DCHECK(delegate_task_runner_->BelongsToCurrentThread());
 
   std::optional<base::DictValue> config = delegate_->GetConfig();
+  if (config.has_value()) {
+    for (auto it = config->begin(); it != config->end();) {
+      // Do not include other keys since they may contain sensitive information.
+      if (!GetUnprivilegedConfigKeys().contains(it->first)) {
+        LOG(ERROR) << "Removed unknown key: " << it->first;
+        it = config->erase(it);
+      } else {
+        ++it;
+      }
+    }
+  }
   caller_task_runner_->PostTask(
       FROM_HERE, base::BindOnce(std::move(done), std::move(config)));
 }
diff --git a/remoting/host/setup/daemon_controller.h b/remoting/host/setup/daemon_controller.h
index 2cf676e0..f308dcc 100644
--- a/remoting/host/setup/daemon_controller.h
+++ b/remoting/host/setup/daemon_controller.h
@@ -8,7 +8,9 @@
 #include <memory>
 #include <optional>
 #include <string>
+#include <string_view>
 
+#include "base/containers/flat_set.h"
 #include "base/containers/queue.h"
 #include "base/functional/callback.h"
 #include "base/memory/ref_counted.h"
@@ -92,6 +94,9 @@
   typedef base::OnceCallback<void(const UsageStatsConsent&)>
       GetUsageStatsConsentCallback;
 
+  // The configuration keys whose values may be read by GetConfig().
+  static const base::flat_set<std::string_view>& GetUnprivilegedConfigKeys();
+
   // Interface representing the platform-spacific back-end. Most of its methods
   // are blocking and should be called on a background thread. There are two
   // exceptions:
diff --git a/remoting/host/setup/daemon_controller_delegate_win.cc b/remoting/host/setup/daemon_controller_delegate_win.cc
index 338a6c98..27ddbc1 100644
--- a/remoting/host/setup/daemon_controller_delegate_win.cc
+++ b/remoting/host/setup/daemon_controller_delegate_win.cc
@@ -50,16 +50,6 @@
 
 // Configuration keys.
 
-// The configuration keys that cannot be specified in UpdateConfig().
-const char* const kReadonlyKeys[] = {
-    kHostIdConfigPath, kHostOwnerConfigPath, kServiceAccountConfigPath,
-    kDeprecatedXmppLoginConfigPath, kDeprecatedHostOwnerEmailConfigPath};
-
-// The configuration keys whose values may be read by GetConfig().
-const char* const kUnprivilegedConfigKeys[] = {kHostIdConfigPath,
-                                               kServiceAccountConfigPath,
-                                               kDeprecatedXmppLoginConfigPath};
-
 // Reads and parses the configuration file up to |kMaxConfigFileSize| in size.
 bool ReadConfig(const base::FilePath& filename, base::DictValue& config_out) {
   // ReadConfig is called in cases where no config file is expected to be
@@ -186,9 +176,9 @@
 
   // Extract the unprivileged fields from the configuration.
   base::DictValue unprivileged_config;
-  for (const char* key : kUnprivilegedConfigKeys) {
-    if (const std::string* value = config.FindString(key)) {
-      unprivileged_config.Set(key, *value);
+  for (const auto& key : DaemonController::GetUnprivilegedConfigKeys()) {
+    if (const base::Value* value = config.Find(key)) {
+      unprivileged_config.Set(key, value->Clone());
     }
   }
 
@@ -360,14 +350,6 @@
 void DaemonControllerDelegateWin::UpdateConfig(
     base::DictValue updated_config,
     DaemonController::CompletionCallback done) {
-  // Check for bad keys.
-  for (const char* key : kReadonlyKeys) {
-    if (updated_config.Find(key)) {
-      LOG(ERROR) << "Cannot update config: '" << key << "' is read only.";
-      InvokeCompletionCallback(std::move(done), false);
-      return;
-    }
-  }
   // Get the old config.
   base::FilePath config_dir = remoting::GetConfigDir();
   base::DictValue config;
Loading diff…

Original Bug Report

reported by [email protected]

CRD Host Hijack via missing config key filtering in UpdateConfig (Linux/macOS)

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 without the Chrome Security team.

Overview: The Chrome Remote Desktop host on Linux and macOS does not filter sensitive configuration keys when receiving updates via Native Messaging. A compromised extension renderer can potentially overwrite keys such as host_id and host_secret_hash to silently hijack the host on Linux. This would grant an attacker persistent remote access and arbitrary input injection capabilities.

Affected files:

  • remoting/host/setup/daemon_controller_delegate_linux.cc
  • remoting/host/setup/daemon_controller_delegate_mac.mm
  • remoting/host/setup/me2me_native_messaging_host.cc

Estimated timestamp from git blame: 2022-09-16

Description

When updating the Chrome Remote Desktop configuration via the native messaging host, the Windows implementation (DaemonControllerDelegateWin::UpdateConfig) explicitly checks for and rejects modifications to a set of kReadonlyKeys (which includes host_id, host_owner, service_account, xmpp_login, and host_owner_email).

However, the implementations for Linux (DaemonControllerDelegateLinux::UpdateConfig) and macOS (DaemonControllerDelegateMac::UpdateConfig) lack this validation. When an updateDaemonConfig message is processed by Me2MeNativeMessagingHost, the provided JSON dictionary is passed directly to the delegate. On Linux and macOS, the code blindly merges this untrusted dictionary into the existing configuration file using base::DictValue::Merge.

Because needs_elevation_ defaults to false on POSIX platforms within Me2MeNativeMessagingHost, this configuration merge and subsequent host reload on Linux occur entirely silently without prompting the user for elevation.

Potential Attack Scenario (Linux)

(Note: These are suggested/potential steps, as our tooling agent does not have the ability to run code or functional exploits to verify them interactively.)

  1. An attacker compromises the CRD companion extension renderer (e.g., via a UXSS or exploiting an externally connectable web interface).
  2. The compromised renderer connects to the native messaging host (com.google.chrome.remote_desktop) and sends an updateDaemonConfig message.
  3. The message contains a malicious config payload with an attacker-controlled host_id and host_secret_hash.
  4. Me2MeNativeMessagingHost::ProcessUpdateDaemonConfig processes the message. Because needs_elevation_ is false, it skips the DelegateToElevatedHost path.
  5. DaemonControllerDelegateLinux::UpdateConfig merges the unvalidated dictionary into the user’s existing configuration file (e.g., ~/.config/chrome-remote-desktop/host#<hash>.json) and saves it to disk.
  6. The delegate triggers a reload by running /opt/google/chrome-remote-desktop/chrome-remote-desktop --reload as the current user.
  7. The Python script finds the running CRD daemon process and sends it a SIGHUP signal.
  8. The daemon cleanly catches the SIGHUP, reloads the poisoned configuration from disk, and restarts the active host process.
  9. The host authenticates to the CRD backend using the attacker’s credentials, establishing persistent remote access and allowing arbitrary input injection at the user’s privilege level.

Secondary Vector (SetConfigAndStart)

A secondary issue exists in DaemonControllerDelegateLinux::SetConfigAndStart. This function writes the configuration to disk before executing the script with --enable-and-start (which prompts for elevation via pkexec).

An attacker could send a startDaemon message with a malicious configuration. The configuration is written to disk immediately. If the user cancels the resulting pkexec prompt (or if the attacker programmatically suppresses it), the malicious configuration remains on disk. The attacker can then send an updateDaemonConfig message to trigger the SIGHUP reload, achieving the same silent hijack.

macOS Impact

On macOS, the UpdateConfig implementation similarly lacks key filtering. However, the configuration is passed to a root helper via ElevateAndSetConfig, which triggers a macOS authorization prompt. While this user prompt prevents a purely silent exploit, the lack of filtering remains a significant defense-in-depth failure.

Suggested Fix

  1. Implement Key Filtering: Apply the kReadonlyKeys filtering logic currently present in DaemonControllerDelegateWin::UpdateConfig to the Linux and macOS implementations. Any update request attempting to modify these keys should be rejected.
  2. Defer Disk Writes on Linux: In DaemonControllerDelegateLinux::SetConfigAndStart, do not write the configuration directly to disk. Instead, pass the configuration securely to the elevated execution context so it is only written if elevation succeeds, preventing poisoned states on disk when elevation fails or is cancelled.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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