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
Tracker500541413
Fix commitf78a31ef686a (chromium/src) +42/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
remoting/protocol/pairing_registry_unittest.cc
modified

Files Changed

  • remoting/protocol/negotiating_authenticator_unittest.cc
  • remoting/protocol/pairing_registry.cc
  • remoting/protocol/pairing_registry_unittest.cc
From f78a31ef686a9196dfa8ea871096c395654aae0c Mon Sep 17 00:00:00 2001
From: Jamie Walch <[email protected]>
Date: Wed, 08 Apr 2026 15:21:16 -0700
Subject: [PATCH] Check client id validity before passing it to delegate.

BUG=500541413

Change-Id: I483e1b2f6f7f6c22808de756071877e01c8bf6b4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7739645
Commit-Queue: Jamie Walch <[email protected]>
Auto-Submit: Jamie Walch <[email protected]>
Reviewed-by: Joe Downing <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1611814}
---

diff --git a/remoting/protocol/negotiating_authenticator_unittest.cc b/remoting/protocol/negotiating_authenticator_unittest.cc
index 92de9901..441b6601 100644
--- a/remoting/protocol/negotiating_authenticator_unittest.cc
+++ b/remoting/protocol/negotiating_authenticator_unittest.cc
@@ -41,8 +41,8 @@
 const char kNoClientId[] = "";
 const char kNoPairedSecret[] = "";
 const char kTestClientName[] = "client-name";
-const char kTestClientId[] = "client-id";
-const char kTestHostId[] = "12345678910123456";
+const char kTestClientId[] = "a1b2c3d4-e5f6-7890-1234-567890abcdef";
+const char kTestHostId[] = "fedcba09-8765-4321-abcd-ef0987654321";
 
 const char kClientJid[] = "[email protected]/abc";
 const char kHostJid[] = "[email protected]/123";
diff --git a/remoting/protocol/pairing_registry.cc b/remoting/protocol/pairing_registry.cc
index 677deaf..1ae002c3 100644
--- a/remoting/protocol/pairing_registry.cc
+++ b/remoting/protocol/pairing_registry.cc
@@ -122,6 +122,13 @@
                                  GetPairingCallback callback) {
   DCHECK(caller_task_runner_->BelongsToCurrentThread());
 
+  if (!base::Uuid::ParseCaseInsensitive(client_id).is_valid()) {
+    LOG(ERROR) << "Invalid client_id: " << client_id;
+    PostTask(caller_task_runner_, FROM_HERE,
+             base::BindOnce(std::move(callback), Pairing()));
+    return;
+  }
+
   GetPairingCallback wrapped_callback =
       base::BindOnce(&PairingRegistry::InvokeGetPairingCallbackAndScheduleNext,
                      this, std::move(callback));
@@ -145,6 +152,13 @@
                                     DoneCallback callback) {
   DCHECK(caller_task_runner_->BelongsToCurrentThread());
 
+  if (!base::Uuid::ParseCaseInsensitive(client_id).is_valid()) {
+    LOG(ERROR) << "Invalid client_id: " << client_id;
+    PostTask(caller_task_runner_, FROM_HERE,
+             base::BindOnce(std::move(callback), false));
+    return;
+  }
+
   DoneCallback wrapped_callback =
       base::BindOnce(&PairingRegistry::InvokeDoneCallbackAndScheduleNext, this,
                      std::move(callback));
diff --git a/remoting/protocol/pairing_registry_unittest.cc b/remoting/protocol/pairing_registry_unittest.cc
index f37ea464..fa8ee1b 100644
--- a/remoting/protocol/pairing_registry_unittest.cc
+++ b/remoting/protocol/pairing_registry_unittest.cc
@@ -77,6 +77,16 @@
     ++callback_count_;
   }
 
+  void ExpectSaveResult(bool expected, bool success) {
+    EXPECT_EQ(success, expected);
+    ++callback_count_;
+  }
+
+  void ExpectInvalidPairing(PairingRegistry::Pairing actual) {
+    EXPECT_FALSE(actual.is_valid());
+    ++callback_count_;
+  }
+
  protected:
   base::test::SingleThreadTaskEnvironment task_environment_;
   base::RunLoop run_loop_;
@@ -161,6 +171,22 @@
   EXPECT_EQ(*actual_client_id, pairing_2.client_id());
 }
 
+TEST_F(PairingRegistryTest, InvalidClientId) {
+  scoped_refptr<PairingRegistry> registry = new SynchronousPairingRegistry(
+      std::make_unique<MockPairingRegistryDelegate>());
+
+  registry->DeletePairing("../tmp/target",
+                          base::BindOnce(&PairingRegistryTest::ExpectSaveResult,
+                                         base::Unretained(this), false));
+  EXPECT_EQ(callback_count_, 1);
+
+  registry->GetPairing(
+      "../tmp/target",
+      base::BindOnce(&PairingRegistryTest::ExpectInvalidPairing,
+                     base::Unretained(this)));
+  EXPECT_EQ(callback_count_, 2);
+}
+
 TEST_F(PairingRegistryTest, ClearAllPairings) {
   scoped_refptr<PairingRegistry> registry = new SynchronousPairingRegistry(
       std::make_unique<MockPairingRegistryDelegate>());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/remoting/protocol/negotiating_authenticator_unittest.cc b/remoting/protocol/negotiating_authenticator_unittest.cc
index 92de9901..441b6601 100644
--- a/remoting/protocol/negotiating_authenticator_unittest.cc
+++ b/remoting/protocol/negotiating_authenticator_unittest.cc
@@ -41,8 +41,8 @@
 const char kNoClientId[] = "";
 const char kNoPairedSecret[] = "";
 const char kTestClientName[] = "client-name";
-const char kTestClientId[] = "client-id";
-const char kTestHostId[] = "12345678910123456";
+const char kTestClientId[] = "a1b2c3d4-e5f6-7890-1234-567890abcdef";
+const char kTestHostId[] = "fedcba09-8765-4321-abcd-ef0987654321";
 
 const char kClientJid[] = "[email protected]/abc";
 const char kHostJid[] = "[email protected]/123";
diff --git a/remoting/protocol/pairing_registry_unittest.cc b/remoting/protocol/pairing_registry_unittest.cc
index f37ea464..fa8ee1b 100644
--- a/remoting/protocol/pairing_registry_unittest.cc
+++ b/remoting/protocol/pairing_registry_unittest.cc
@@ -77,6 +77,16 @@
     ++callback_count_;
   }
 
+  void ExpectSaveResult(bool expected, bool success) {
+    EXPECT_EQ(success, expected);
+    ++callback_count_;
+  }
+
+  void ExpectInvalidPairing(PairingRegistry::Pairing actual) {
+    EXPECT_FALSE(actual.is_valid());
+    ++callback_count_;
+  }
+
  protected:
   base::test::SingleThreadTaskEnvironment task_environment_;
   base::RunLoop run_loop_;
@@ -161,6 +171,22 @@
   EXPECT_EQ(*actual_client_id, pairing_2.client_id());
 }
 
+TEST_F(PairingRegistryTest, InvalidClientId) {
+  scoped_refptr<PairingRegistry> registry = new SynchronousPairingRegistry(
+      std::make_unique<MockPairingRegistryDelegate>());
+
+  registry->DeletePairing("../tmp/target",
+                          base::BindOnce(&PairingRegistryTest::ExpectSaveResult,
+                                         base::Unretained(this), false));
+  EXPECT_EQ(callback_count_, 1);
+
+  registry->GetPairing(
+      "../tmp/target",
+      base::BindOnce(&PairingRegistryTest::ExpectInvalidPairing,
+                     base::Unretained(this)));
+  EXPECT_EQ(callback_count_, 2);
+}
+
 TEST_F(PairingRegistryTest, ClearAllPairings) {
   scoped_refptr<PairingRegistry> registry = new SynchronousPairingRegistry(
       std::make_unique<MockPairingRegistryDelegate>());
Loading diff…

Original Bug Report

reported by [email protected]

Path traversal in CRD Linux native host allows arbitrary .json file deletion

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 security team.

Overview: A potential path traversal vulnerability exists in the Chrome Remote Desktop native messaging host for Linux. By compromising an allowlisted extension, an attacker can send a crafted deletePairedClient request containing ../ sequences to delete arbitrary .json files on the host system.

Affected files:

  • remoting/host/pairing_registry_delegate_linux.cc
  • remoting/host/setup/me2me_native_messaging_host.cc
  • remoting/host/pairing_registry.cc

Estimated timestamp from git blame: 2022-07-01

Vulnerability Description

A potential path traversal vulnerability has been identified in the Linux implementation of the Chrome Remote Desktop (CRD) PairingRegistryDelegate.

The CRD Native Messaging Host handles deletePairedClient requests by extracting a clientId from an incoming JSON message. This clientId is directly used to construct a file path for deletion. Because there is no validation or sanitization of the clientId string, an attacker can include directory traversal sequences (e.g., ../../) to navigate outside the intended configuration directory. The underlying POSIX system calls resolve the traversal components, resulting in the deletion of arbitrary .json files accessible to the user running the Native Messaging Host process.

Step-by-step Execution Trace

  1. Me2MeNativeMessagingHost::OnMessage receives a native messaging payload from the browser and parses it into a dictionary (remoting/host/setup/me2me_native_messaging_host.cc).
  2. When the type is deletePairedClient, it calls Me2MeNativeMessagingHost::ProcessDeletePairedClient.
  3. The clientId string is extracted directly from the message dictionary without any format validation or character filtering.
  4. The clientId is passed via PairingRegistry::DeletePairing to the platform delegate.
  5. On Linux, PairingRegistryDelegateLinux::Delete is invoked (remoting/host/pairing_registry_delegate_linux.cc).
  6. The method constructs a filename by formatting the string "%s.json" with the attacker-controlled clientId.
  7. This filename is appended to the base registry directory (~/.config/chrome-remote-desktop/paired-clients) using base::FilePath::Append(). Append concatenates paths verbatim, failing to neutralize ../ components.
  8. The resulting base::FilePath is passed to base::DeleteFile(), which invokes the POSIX unlink/unlinkat syscall. The OS resolves the ../ traversal, deleting the target file.

Potential Attack Scenario

Note: These are suggested/potential steps, as our tooling agent doesn’t yet have the ability to run code and provide a working proof of concept.

  1. An attacker compromises an extension allowlisted to communicate with the com.google.chrome.remote_desktop native messaging host (e.g., via a Cross-Site Scripting vulnerability in the CRD extension).
  2. The attacker uses the chrome.runtime.sendNativeMessage API to send a malicious JSON payload to the Native Messaging Host:
    {
      "type": "deletePairedClient",
      "clientId": "../../../../../../../../tmp/target"
    }
    
  3. The host process constructs the deletion path as ~/.config/chrome-remote-desktop/paired-clients/../../../../../../../../tmp/target.json.
  4. The host OS deletes /tmp/target.json. The attacker can target any .json file that the active user has permissions to delete, potentially leading to denial of service or configuration disruption.

Suggested Fix

To mitigate this vulnerability, the clientId should be strictly validated upon extraction. Legitimate CRD client IDs are standard UUIDs (v4). Enforcing that the clientId is a valid UUID before passing it to the registry will safely reject malicious traversal sequences.

In Me2MeNativeMessagingHost::ProcessDeletePairedClient, validate the clientId:

std::string* client_id =
    message.FindString(protocol::PairingRegistry::kClientIdKey);
if (!client_id || !base::Uuid::ParseLowercase(*client_id).is_valid()) {
  OnError("Invalid or missing '" + std::string(protocol::PairingRegistry::kClientIdKey) + "'.");
  return;
}

Additionally, standardizing path sanitization in PairingRegistryDelegateLinux::Delete (e.g., validating that the final path remains a child of the intended directory) would provide Defense in Depth.

Evaluated with Chrome root at commit: 137d451a126685dd5010e6609db9f6d4a78d8234


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