CVE-2026-95305
Overview
Background
- Chromoting
- the Chrome Remote Desktop host/client stack, whose Linux host shows a disconnect dialog identifying the connected remote user.
- `DisconnectWindowGtk`
- the GTK implementation of the host disconnect window that renders the client identity in a
GtkLabelnamedmessage_. - `client_jid`
- the connecting client’s Jabber ID (e.g. an email plus a resource suffix); its local part is used as the displayed username.
- `PANGO_ELLIPSIZE_MIDDLE`
- a Pango/GTK label attribute that clips overflowing text and inserts an ellipsis rather than letting the label grow unbounded.
Root Cause Analysis
The vulnerable path is DisconnectWindowGtk::ShowSharingUI (via SetLabelText/label setup in the diff), which derives username from client_jid and passes it straight into l10n_util::GetStringFUTF8(IDS_MESSAGE_SHARED, username) for display in an unwrapped, un-ellipsized GtkLabel. Because the label had no length bound and no ellipsization, an authenticated remote user connecting with an oversized email/JID caused the label — and therefore the whole window — to grow past the screen boundary, pushing the disconnect button off-screen so the local user could not see or use it. The invariant violated is that host-controlled security UI must remain fully on-screen and legible regardless of attacker-supplied identity strings.
The fix truncates username to kMaxUsernameLength (50 UTF-16 characters) at a valid character boundary using base::i18n::UTF16CharIterator::LowerBound, collapses whitespace with base::CollapseWhitespace, configures the label with gtk_label_set_ellipsize/gtk_label_set_max_width_chars, and clamps the window’s horizontal placement with std::max(0, ...) so the dialog stays within screen bounds.
Attack Path
- Connect with a crafted identity An authenticated remote user initiates a Chrome Remote Desktop session to a Linux host using a JID whose local part (email) is extremely long.
- Username flows into the label unbounded
The host builds
usernamefromclient_jid.substr(0, client_jid.find('/'))and formats it intoIDS_MESSAGE_SHARED, setting it as the text of the un-ellipsizedmessage_label. - Window grows past screen edge
The oversized label expands the disconnect window horizontally beyond the display geometry, and centered placement (
(geometry.width - width) / 2) yields a negative left offset, shifting the window off-screen. - Disconnect control is hidden The disconnect button is pushed off the visible screen area, so the local host user cannot readily terminate the remote session (UI misrepresentation / spoofing of the security-relevant disconnect affordance). (inferred)
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
FakeClientSessionControlremoting/host/disconnect_window_linux_unittest.cc |
modified | |
TestResourceBundleDelegateremoting/host/disconnect_window_linux_unittest.cc |
modified | |
ifremoting/host/disconnect_window_linux_unittest.cc |
modified | |
DisconnectWindowLinuxTestremoting/host/disconnect_window_linux_unittest.cc |
modified | |
resource_swapper_remoting/host/disconnect_window_linux_unittest.cc |
modified | |
TEST_Fremoting/host/disconnect_window_linux_unittest.cc |
modified | |
ifremoting/host/BUILD.gn |
modified |
Files Changed
remoting/host/disconnect_window_linux.ccremoting/host/BUILD.gnremoting/host/disconnect_window_linux_unittest.cc
Audit Directions
- Attacker-controlled strings sizing UIAudit host/security dialogs that render remote-supplied identities (JIDs, emails, device names) in labels without ellipsization or length bounds.
- Unclamped window placementReview dialog positioning math that centers on screen geometry without clamping to non-negative on-screen coordinates.
- Unicode-unsafe truncationCheck other truncation sites to ensure they cut at character boundaries (e.g.
base::i18n::UTF16CharIterator) rather than splitting surrogate pairs.
Patch
From 368bc15964d9580eb6f6a875c25280d8a6f4c504 Mon Sep 17 00:00:00 2001
From: Joe Downing <joedow@google.com>
Date: Mon, 31 Aug 2026 14:28:54 -0700
Subject: [PATCH] [remoting] Truncate client username in Linux disconnect window
In Chrome Remote Desktop on Linux, the disconnect dialog displays the
connecting client's identity in an unwrapped, un-ellipsized GtkLabel.
When an authenticated remote user connects with an oversized email address,
the window can expand beyond the screen boundary, pushing the disconnect
button off-screen.
This change collapses whitespace and safely truncates the username to
50 UTF-16 characters at a valid character boundary before formatting
IDS_MESSAGE_SHARED, preserving localized punctuation. It also configures
the message label with PANGO_ELLIPSIZE_MIDDLE and max_width_chars, and
clamps window placement within screen boundaries.
Fixed: 553921181
TAG=workflow
TAG=agy
CONV=180927ee-f343-45e4-a0ce-c2b7b7f11b75
Change-Id: Ie6088bc4d1e5d4dc48b9fb87efe3f4547868dc07
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8315589
Reviewed-by: Jamie Walch <jamiewalch@chromium.org>
Commit-Queue: Joe Downing <joedow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1689275}
---
diff --git a/remoting/host/disconnect_window_linux.cc b/remoting/host/disconnect_window_linux.cc
index dd48ea8..687e36a 100644
--- a/remoting/host/disconnect_window_linux.cc
+++ b/remoting/host/disconnect_window_linux.cc
@@ -4,6 +4,7 @@
#include <gtk/gtk.h>
+#include <algorithm>
#include <memory>
#include <numbers>
#include <vector>
@@ -11,6 +12,7 @@
#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/functional/bind.h"
+#include "base/i18n/char_iterator.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
@@ -32,6 +34,9 @@
// The amount of time to wait before allowing another position toggle.
constexpr base::TimeDelta kToggleCooldown = base::Seconds(3);
+// Maximum length of the username / client identity in UTF-16 characters.
+constexpr size_t kMaxUsernameLength = 50;
+
// Margins from screen edges to ensure the dialog is not obscured by the top bar
// or an auto-hiding dock/panel at the bottom.
constexpr int kTopMargin = 40;
@@ -250,10 +255,13 @@
&DisconnectWindowGtk::OnToggleClicked);
message_ = gtk_label_new(nullptr);
+ gtk_label_set_ellipsize(GTK_LABEL(message_.get()), PANGO_ELLIPSIZE_MIDDLE);
+ gtk_label_set_max_width_chars(GTK_LABEL(message_.get()), 30);
#if GTK_CHECK_VERSION(3, 90, 0)
gtk_box_pack_start(GTK_BOX(button_row), message_.get());
+ gtk_widget_set_hexpand(message_.get(), TRUE);
#else
- gtk_box_pack_start(GTK_BOX(button_row), message_.get(), FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(button_row), message_.get(), TRUE, TRUE, 0);
#endif
button_ = gtk_button_new_with_label(
@@ -296,9 +304,21 @@
std::string client_jid = client_session_control_->client_jid();
std::u16string username =
base::UTF8ToUTF16(client_jid.substr(0, client_jid.find('/')));
- gtk_label_set_text(
- GTK_LABEL(message_.get()),
- l10n_util::GetStringFUTF8(IDS_MESSAGE_SHARED, username).c_str());
+ username = base::CollapseWhitespace(username,
+ /*trim_sequences_with_line_breaks=*/true);
+ // Truncate username safely at a Unicode character boundary so that
+ // truncation does not split UTF-16 surrogate pairs. Truncating before
+ // formatting ensures localized punctuation and grammar in IDS_MESSAGE_SHARED
+ // (e.g. trailing periods) are preserved.
+ if (username.length() > kMaxUsernameLength) {
+ username.erase(
+ base::i18n::UTF16CharIterator::LowerBound(username, kMaxUsernameLength)
+ .array_pos());
+ }
+
+ std::string message_text =
+ l10n_util::GetStringFUTF8(IDS_MESSAGE_SHARED, username);
+ gtk_label_set_text(GTK_LABEL(message_.get()), message_text.c_str());
SetDialogPosition();
gtk_window_present(window);
}
@@ -405,7 +425,7 @@
height = requisition.height;
}
- int left = geometry.x + (geometry.width - width) / 2;
+ int left = geometry.x + std::max(0, (geometry.width - width) / 2);
int top = (g_current_anchor == WindowAnchor::kTop)
? (geometry.y + kTopMargin)
: (geometry.y + geometry.height - height - kBottomMargin);
diff --git a/remoting/host/disconnect_window_linux_unittest.cc b/remoting/host/disconnect_window_linux_unittest.cc
new file mode 100644
index 0000000..3b398b4
--- /dev/null
+++ b/remoting/host/disconnect_window_linux_unittest.cc
@@ -0,0 +1,180 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtk/gtk.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/memory/ref_counted_memory.h"
+#include "base/memory/weak_ptr.h"
+#include "base/test/task_environment.h"
+#include "remoting/base/string_resources.h"
+#include "remoting/host/client_session_control.h"
+#include "remoting/host/host_mock_objects.h"
+#include "remoting/host/host_window.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/resource/resource_bundle.h"
+#include "ui/base/resource/resource_scale_factor.h"
+
+namespace remoting {
+
+namespace {
+
+class FakeClientSessionControl : public MockClientSessionControl {
+ public:
+ explicit FakeClientSessionControl(std::string client_jid)
+ : client_jid_(std::move(client_jid)) {
+ EXPECT_CALL(*this, client_jid())
+ .WillRepeatedly(testing::ReturnRef(client_jid_));
+ }
+
+ ~FakeClientSessionControl() override = default;
+
+ base::WeakPtr<ClientSessionControl> GetWeakPtr() {
+ return weak_factory_.GetWeakPtr();
+ }
+
+ private:
+ std::string client_jid_;
+ base::WeakPtrFactory<FakeClientSessionControl> weak_factory_{this};
+};
+
+class TestResourceBundleDelegate : public ui::ResourceBundle::Delegate {
+ public:
+ TestResourceBundleDelegate() = default;
+ ~TestResourceBundleDelegate() override = default;
+
+ base::FilePath GetPathForResourcePack(
+ const base::FilePath& pack_path,
+ ui::ResourceScaleFactor scale_factor) override {
+ return base::FilePath();
+ }
+ gfx::Image GetImageNamed(int resource_id) override { return gfx::Image(); }
+ gfx::Image GetNativeImageNamed(int resource_id) override {
+ return gfx::Image();
+ }
+ bool HasDataResource(int resource_id) const override { return false; }
+ scoped_refptr<base::RefCountedMemory> LoadDataResourceBytes(
+ int resource_id,
+ ui::ResourceScaleFactor scale_factor) override {
+ return nullptr;
+ }
+ std::optional<std::string> LoadDataResourceString(int resource_id) override {
+ return std::nullopt;
+ }
+ bool GetRawDataResource(int resource_id,
+ ui::ResourceScaleFactor scale_factor,
+ std::string_view* value) const override {
+ return false;
+ }
+ bool GetLocalizedString(int message_id,
+ std::u16string* value) const override {
+ if (message_id == IDS_MESSAGE_SHARED) {
+ *value = u"Your desktop is shared with $1.";
+ return true;
+ }
+ if (message_id == IDS_PRODUCT_NAME) {
+ *value = u"Chrome Remote Desktop";
+ return true;
+ }
+ if (message_id == IDS_STOP_SHARING_BUTTON) {
+ *value = u"Stop Sharing";
+ return true;
+ }
+ return false;
+ }
+};
+
+bool InitializeGtk() {
+#if GTK_CHECK_VERSION(3, 90, 0)
+ return gtk_init_check();
+#else
+ return gtk_init_check(nullptr, nullptr);
+#endif
+}
+
+} // namespace
+
+class DisconnectWindowLinuxTest : public testing::Test {
+ public:
+ DisconnectWindowLinuxTest()
+ : task_environment_(base::test::TaskEnvironment::MainThreadType::UI),
+ resource_bundle_(&resource_delegate_),
+ resource_swapper_(&resource_bundle_) {}
+ ~DisconnectWindowLinuxTest() override = default;
+
+ void TearDown() override {
+ // Pump the event loop to ensure GTK widget destruction and cleanup is
+ // processed.
+ task_environment_.RunUntilIdle();
+ }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ TestResourceBundleDelegate resource_delegate_;
+ ui::ResourceBundle resource_bundle_;
+ ui::ResourceBundle::SharedInstanceSwapperForTesting resource_swapper_;
+};
+
+TEST_F(DisconnectWindowLinuxTest, NormalEmailDoesNotCrash) {
+ if (!InitializeGtk()) {
+ GTEST_SKIP() << "No display available for GTK.";
+ }
+
+ FakeClientSessionControl session_control(
+ "john.smith@gmail.com/chromoting_ftl");
+ std::unique_ptr<HostWindow> window = HostWindow::CreateDisconnectWindow();
+ ASSERT_TRUE(window);
+ window->Start(session_control.GetWeakPtr());
+ EXPECT_TRUE(session_control.GetWeakPtr());
+
+ window.reset();
+ task_environment_.RunUntilIdle();
+}
+
+TEST_F(DisconnectWindowLinuxTest, LongEmailDoesNotCrash) {
+ if (!InitializeGtk()) {
+ GTEST_SKIP() << "No display available for GTK.";
+ }
+
+ const std::string kLongEmail =
+ "it-support-desk-remote-assistance-session-verification-operators@"
+ "secure-remote-support-verification-and-customer-protection-desk."
+ "assistance-operations-center-for-workspace-hosted-accounts-team."
+ "example-corporation-global-technical-services-departments.com/"
+ "chromoting_ftl";
+
+ FakeClientSessionControl session_control(kLongEmail);
+ std::unique_ptr<HostWindow> window = HostWindow::CreateDisconnectWindow();
+ ASSERT_TRUE(window);
+ window->Start(session_control.GetWeakPtr());
+ EXPECT_TRUE(session_control.GetWeakPtr());
+
+ window.reset();
+ task_environment_.RunUntilIdle();
+}
+
+TEST_F(DisconnectWindowLinuxTest, WhitespaceAndUnicodeEmailDoesNotCrash) {
+ if (!InitializeGtk()) {
+ GTEST_SKIP() << "No display available for GTK.";
+ }
+
+ // Multi-byte Unicode with whitespace formatting
+ const std::string kUnicodeEmail =
+ " \t\n user_with_unicode_测试_🚀@example.com \n/chromoting_ftl";
+
+ FakeClientSessionControl session_control(kUnicodeEmail);
+ std::unique_ptr<HostWindow> window = HostWindow::CreateDisconnectWindow();
+ ASSERT_TRUE(window);
+ window->Start(session_control.GetWeakPtr());
+ EXPECT_TRUE(session_control.GetWeakPtr());
+
+ window.reset();
+ task_environment_.RunUntilIdle();
+}
+
+} // namespace remoting
diff --git a/remoting/host/BUILD.gn b/remoting/host/BUILD.gn
index a234f50..c9e5a15 100644
--- a/remoting/host/BUILD.gn
+++ b/remoting/host/BUILD.gn
@@ -1031,6 +1031,7 @@
if (is_linux) {
sources += [
+ "disconnect_window_linux_unittest.cc",
"pairing_registry_delegate_linux_unittest.cc",
"terminal_process_monitor_linux_unittest.cc",
]
Regression Test / PoC
diff --git a/remoting/host/disconnect_window_linux_unittest.cc b/remoting/host/disconnect_window_linux_unittest.cc
new file mode 100644
index 0000000..3b398b4
--- /dev/null
+++ b/remoting/host/disconnect_window_linux_unittest.cc
@@ -0,0 +1,180 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtk/gtk.h>
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/memory/ref_counted_memory.h"
+#include "base/memory/weak_ptr.h"
+#include "base/test/task_environment.h"
+#include "remoting/base/string_resources.h"
+#include "remoting/host/client_session_control.h"
+#include "remoting/host/host_mock_objects.h"
+#include "remoting/host/host_window.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/base/resource/resource_bundle.h"
+#include "ui/base/resource/resource_scale_factor.h"
+
+namespace remoting {
+
+namespace {
+
+class FakeClientSessionControl : public MockClientSessionControl {
+ public:
+ explicit FakeClientSessionControl(std::string client_jid)
+ : client_jid_(std::move(client_jid)) {
+ EXPECT_CALL(*this, client_jid())
+ .WillRepeatedly(testing::ReturnRef(client_jid_));
+ }
+
+ ~FakeClientSessionControl() override = default;
+
+ base::WeakPtr<ClientSessionControl> GetWeakPtr() {
+ return weak_factory_.GetWeakPtr();
+ }
+
+ private:
+ std::string client_jid_;
+ base::WeakPtrFactory<FakeClientSessionControl> weak_factory_{this};
+};
+
+class TestResourceBundleDelegate : public ui::ResourceBundle::Delegate {
+ public:
+ TestResourceBundleDelegate() = default;
+ ~TestResourceBundleDelegate() override = default;
+
+ base::FilePath GetPathForResourcePack(
+ const base::FilePath& pack_path,
+ ui::ResourceScaleFactor scale_factor) override {
+ return base::FilePath();
+ }
+ gfx::Image GetImageNamed(int resource_id) override { return gfx::Image(); }
+ gfx::Image GetNativeImageNamed(int resource_id) override {
+ return gfx::Image();
+ }
+ bool HasDataResource(int resource_id) const override { return false; }
+ scoped_refptr<base::RefCountedMemory> LoadDataResourceBytes(
+ int resource_id,
+ ui::ResourceScaleFactor scale_factor) override {
+ return nullptr;
+ }
+ std::optional<std::string> LoadDataResourceString(int resource_id) override {
+ return std::nullopt;
+ }
+ bool GetRawDataResource(int resource_id,
+ ui::ResourceScaleFactor scale_factor,
+ std::string_view* value) const override {
+ return false;
+ }
+ bool GetLocalizedString(int message_id,
+ std::u16string* value) const override {
+ if (message_id == IDS_MESSAGE_SHARED) {
+ *value = u"Your desktop is shared with $1.";
+ return true;
+ }
+ if (message_id == IDS_PRODUCT_NAME) {
+ *value = u"Chrome Remote Desktop";
+ return true;
+ }
+ if (message_id == IDS_STOP_SHARING_BUTTON) {
+ *value = u"Stop Sharing";
+ return true;
+ }
+ return false;
+ }
+};
+
+bool InitializeGtk() {
+#if GTK_CHECK_VERSION(3, 90, 0)
+ return gtk_init_check();
+#else
+ return gtk_init_check(nullptr, nullptr);
+#endif
+}
+
+} // namespace
+
+class DisconnectWindowLinuxTest : public testing::Test {
+ public:
+ DisconnectWindowLinuxTest()
+ : task_environment_(base::test::TaskEnvironment::MainThreadType::UI),
+ resource_bundle_(&resource_delegate_),
+ resource_swapper_(&resource_bundle_) {}
+ ~DisconnectWindowLinuxTest() override = default;
+
+ void TearDown() override {
+ // Pump the event loop to ensure GTK widget destruction and cleanup is
+ // processed.
+ task_environment_.RunUntilIdle();
+ }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ TestResourceBundleDelegate resource_delegate_;
+ ui::ResourceBundle resource_bundle_;
+ ui::ResourceBundle::SharedInstanceSwapperForTesting resource_swapper_;
+};
+
+TEST_F(DisconnectWindowLinuxTest, NormalEmailDoesNotCrash) {
+ if (!InitializeGtk()) {
+ GTEST_SKIP() << "No display available for GTK.";
+ }
+
+ FakeClientSessionControl session_control(
+ "john.smith@gmail.com/chromoting_ftl");
+ std::unique_ptr<HostWindow> window = HostWindow::CreateDisconnectWindow();
+ ASSERT_TRUE(window);
+ window->Start(session_control.GetWeakPtr());
+ EXPECT_TRUE(session_control.GetWeakPtr());
+
+ window.reset();
+ task_environment_.RunUntilIdle();
+}
+
+TEST_F(DisconnectWindowLinuxTest, LongEmailDoesNotCrash) {
+ if (!InitializeGtk()) {
+ GTEST_SKIP() << "No display available for GTK.";
+ }
+
+ const std::string kLongEmail =
+ "it-support-desk-remote-assistance-session-verification-operators@"
+ "secure-remote-support-verification-and-customer-protection-desk."
+ "assistance-operations-center-for-workspace-hosted-accounts-team."
+ "example-corporation-global-technical-services-departments.com/"
+ "chromoting_ftl";
+
+ FakeClientSessionControl session_control(kLongEmail);
+ std::unique_ptr<HostWindow> window = HostWindow::CreateDisconnectWindow();
+ ASSERT_TRUE(window);
+ window->Start(session_control.GetWeakPtr());
+ EXPECT_TRUE(session_control.GetWeakPtr());
+
+ window.reset();
+ task_environment_.RunUntilIdle();
+}
+
+TEST_F(DisconnectWindowLinuxTest, WhitespaceAndUnicodeEmailDoesNotCrash) {
+ if (!InitializeGtk()) {
+ GTEST_SKIP() << "No display available for GTK.";
+ }
+
+ // Multi-byte Unicode with whitespace formatting
+ const std::string kUnicodeEmail =
+ " \t\n user_with_unicode_测试_🚀@example.com \n/chromoting_ftl";
+
+ FakeClientSessionControl session_control(kUnicodeEmail);
+ std::unique_ptr<HostWindow> window = HostWindow::CreateDisconnectWindow();
+ ASSERT_TRUE(window);
+ window->Start(session_control.GetWeakPtr());
+ EXPECT_TRUE(session_control.GetWeakPtr());
+
+ window.reset();
+ task_environment_.RunUntilIdle();
+}
+
+} // namespace remoting