CVE-2026-11112
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fremoting/protocol/pairing_registry_unittest.cc |
modified |
Files Changed
remoting/protocol/negotiating_authenticator_unittest.ccremoting/protocol/pairing_registry.ccremoting/protocol/pairing_registry_unittest.cc
Patch
From f78a31ef686a9196dfa8ea871096c395654aae0c Mon Sep 17 00:00:00 2001
From: Jamie Walch <jamiewalch@chromium.org>
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 <jamiewalch@chromium.org>
Auto-Submit: Jamie Walch <jamiewalch@chromium.org>
Reviewed-by: Joe Downing <joedow@chromium.org>
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[] = "alice@gmail.com/abc";
const char kHostJid[] = "alice@gmail.com/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>());
Regression Test / PoC
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[] = "alice@gmail.com/abc";
const char kHostJid[] = "alice@gmail.com/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>());
Original Bug Report
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.ccremoting/host/setup/me2me_native_messaging_host.ccremoting/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
Me2MeNativeMessagingHost::OnMessagereceives a native messaging payload from the browser and parses it into a dictionary (remoting/host/setup/me2me_native_messaging_host.cc).- When the
typeisdeletePairedClient, it callsMe2MeNativeMessagingHost::ProcessDeletePairedClient. - The
clientIdstring is extracted directly from the message dictionary without any format validation or character filtering. - The
clientIdis passed viaPairingRegistry::DeletePairingto the platform delegate. - On Linux,
PairingRegistryDelegateLinux::Deleteis invoked (remoting/host/pairing_registry_delegate_linux.cc). - The method constructs a filename by formatting the string
"%s.json"with the attacker-controlledclientId. - This filename is appended to the base registry directory (
~/.config/chrome-remote-desktop/paired-clients) usingbase::FilePath::Append().Appendconcatenates paths verbatim, failing to neutralize../components. - The resulting
base::FilePathis passed tobase::DeleteFile(), which invokes the POSIXunlink/unlinkatsyscall. 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.
- An attacker compromises an extension allowlisted to communicate with the
com.google.chrome.remote_desktopnative messaging host (e.g., via a Cross-Site Scripting vulnerability in the CRD extension). - The attacker uses the
chrome.runtime.sendNativeMessageAPI to send a malicious JSON payload to the Native Messaging Host:{ "type": "deletePairedClient", "clientId": "../../../../../../../../tmp/target" } - The host process constructs the deletion path as
~/.config/chrome-remote-desktop/paired-clients/../../../../../../../../tmp/target.json. - The host OS deletes
/tmp/target.json. The attacker can target any.jsonfile 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.