Overview

Low
Severity
4.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Chromoting
DescriptionUI misrepresentation in Chromoting
ComponentChromoting
Sub-componentRemoting
Bug ClassSpoofing
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker553921181
Fix commit368bc15964d9 (chromium/src) +206/-5
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-08-28 (per vendor advisory)
Fix landed2026-08-31 (public git)
Disclosed2026-09-22
Patch gap22 days — fix public in source before the advisory

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 GtkLabel named message_.
`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.

Key insight
The single mistake is trusting an attacker-supplied identity string to size a piece of security UI, letting the remote username dictate window geometry. The fix removes that leverage by bounding the string length, ellipsizing the label, and clamping the window position on screen.

Attack Path

  1. 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.
  2. Username flows into the label unbounded The host builds username from client_jid.substr(0, client_jid.find('/')) and formats it into IDS_MESSAGE_SHARED, setting it as the text of the un-ellipsized message_ label.
  3. 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.
  4. 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

An authenticated remote client gains the ability to render the host’s disconnect UI unusable by hiding the disconnect button off-screen, undermining the local user’s ability to see or end the session in the host process’s UI context. The precondition is an established, authenticated remote connection to a Linux Chrome Remote Desktop host with an attacker-controlled JID/email. The advisory classifies this as UI misrepresentation in Chromoting (a LogicError) and states no exploited-in-the-wild status; any further escalation beyond obscuring the control is (inferred).

Changed Functions

FunctionChangeNotes
FakeClientSessionControl
remoting/host/disconnect_window_linux_unittest.cc
modified
TestResourceBundleDelegate
remoting/host/disconnect_window_linux_unittest.cc
modified
if
remoting/host/disconnect_window_linux_unittest.cc
modified
DisconnectWindowLinuxTest
remoting/host/disconnect_window_linux_unittest.cc
modified
resource_swapper_
remoting/host/disconnect_window_linux_unittest.cc
modified
TEST_F
remoting/host/disconnect_window_linux_unittest.cc
modified
if
remoting/host/BUILD.gn
modified

Files Changed

  • remoting/host/disconnect_window_linux.cc
  • remoting/host/BUILD.gn
  • remoting/host/disconnect_window_linux_unittest.cc

Audit Directions

  • Attacker-controlled strings sizing UI
    Audit host/security dialogs that render remote-supplied identities (JIDs, emails, device names) in labels without ellipsization or length bounds.
  • Unclamped window placement
    Review dialog positioning math that centers on screen geometry without clamping to non-negative on-screen coordinates.
  • Unicode-unsafe truncation
    Check other truncation sites to ensure they cut at character boundaries (e.g. base::i18n::UTF16CharIterator) rather than splitting surrogate pairs.
From 368bc15964d9580eb6f6a875c25280d8a6f4c504 Mon Sep 17 00:00:00 2001
From: Joe Downing <[email protected]>
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 <[email protected]>
Commit-Queue: Joe Downing <[email protected]>
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(
+      "[email protected]/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",
     ]
Loading diff…

Regression Test / PoC

shipped with the fix
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(
+      "[email protected]/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
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.