CVE-2026-95346
Overview
Background
- Chromoting / Chrome Remote Desktop host
- The
remotinghost component that shares a local desktop to an authenticated remote client, injecting the client’s mouse and keyboard input into the local session. - Disconnect window
- The always-visible “Stop Sharing” dialog (
DisconnectWindowGtk) that acts as the local user’s primary security indicator and manual disconnect control while a session is shared. - `gtk_window_begin_move_drag`
- A GTK API that initiates an interactive window move so the window follows subsequent pointer motion, invoked here from the dialog’s
button-press-eventhandlerOnButtonPress. - Input injection
- Remote client pointer events are delivered into the host desktop as real mouse input, so anything the local pointer can do (including dragging a window) a remote client can also do.
Root Cause Analysis
The DisconnectWindowGtk dialog registered a button-press-event handler (OnButtonPress) that called gtk_window_begin_move_drag, making the entire dialog surface a freeform drag handle. Because remote clients inject mouse input directly into the host session, an authenticated remote client could press and drag anywhere on the dialog and relocate it, violating the security invariant that the disconnect indicator and “Stop Sharing” control must remain visible and reachable by the local user. By moving the window off-screen the client could hide the sole local signal that a session is active and the sole local means to end it.
The fix removes the OnButtonPress drag handler (and the decorative gripper drawn in OnDraw), replacing freeform positioning with a fixed, monitor-anchored placement computed by SetDialogPosition and constrained by kTopMargin / kBottomMargin. A dedicated toggle_button_ lets the local user flip the anchor between top and bottom via ToggleAlignment, and a kToggleCooldown (3s) cooldown_timer_ prevents a remote client from spamming the toggle to dodge user clicks.
Attack Path
- Establish an authenticated session A remote client connects and is authorized to control the shared desktop, gaining the ability to inject mouse input into the host session.
- Target the disconnect dialog
The client moves the injected pointer over the “Stop Sharing” dialog, whose entire surface is a move handle because
OnButtonPresscallsgtk_window_begin_move_drag. - Initiate a move drag The client issues a button-press-and-drag, causing the dialog window to follow the pointer as GTK performs an interactive move.
- Hide the security indicator The client drags the dialog off-screen (or onto an inactive monitor), removing the local user’s visual indication that sharing is active and the manual disconnect control. (inferred)
- Continue undetected access With the indicator hidden, the local user may not realize the session is active, so the attacker retains control of the desktop. (inferred)
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
DisconnectWindowGtkremoting/host/disconnect_window_linux.cc |
modified | |
current_height_remoting/host/disconnect_window_linux.cc |
modified | |
ifremoting/host/disconnect_window_linux.cc |
modified |
Files Changed
remoting/host/disconnect_window_linux.cc
Audit Directions
- Attacker-controllable input reaching trusted UIAudit remoting host windows and overlays for handlers (drag, resize, close, focus) reachable via injected remote mouse/keyboard input that could hide or move security indicators.
- Security indicators that can be moved or occludedReview any always-on-top or notification surfaces across host platforms (
disconnect_window_*) to confirm they cannot be repositioned off-screen, minimized, or covered by client-triggered actions. - Missing rate limits on UI controlsLook for toggle/move/dismiss controls that lack a cooldown or debounce, since spammable actions let a remote party time interactions to defeat local user clicks.
Patch
From 940324ec1db92ebfb90059e638f73c0ba32ed42e Mon Sep 17 00:00:00 2001 From: Joe Downing <[email protected]> Date: Thu, 27 Aug 2026 15:30:39 -0700 Subject: [PATCH] [remoting] Anchor Linux disconnect dialog to top/bottom edges In Chrome Remote Desktop on Linux, the disconnect ("Stop Sharing") dialog was previously draggable by clicking anywhere on its surface (using gtk_window_begin_move_drag in button-press-event). Because remote mouse input injected into the desktop session can move windows, an authenticated remote client could initiate a move drag to reposition the dialog off-screen, hiding the local user's primary security indicator and disconnect button. This change hardens the disconnect window against remote evasion while preserving usability for local users: 1. Disables freeform window dragging by removing the button-press-event drag handler and the background gripper lines. 2. Adds a dedicated toggle button to the dialog layout displaying "▲" / "▼", allowing users to toggle the dialog between the bottom and top of the active monitor. 3. Implements a 3-second cooldown on the toggle action to prevent a remote client from spamming the toggle button to dodge user clicks. 4. Persists the selected anchor position across dialog instances. 5. Updates SetDialogPosition() to calculate the position using monitor geometry with fixed screen margins to remain static and unobstructed by auto-hiding or resizing panels/docks, and connects to screen change events. TAG=agy CONV=f2e64d7b-06ec-48fc-93c3-e9042357ef4e Bug: 514059630 Change-Id: I46bc955da0ac4e07681c1c770ea1ab19cae437a8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8302253 Reviewed-by: Yuwei Huang <[email protected]> Commit-Queue: Joe Downing <[email protected]> Cr-Commit-Position: refs/heads/main@{#1687607} --- diff --git a/remoting/host/disconnect_window_linux.cc b/remoting/host/disconnect_window_linux.cc index 8ebb804..0041965 100644 --- a/remoting/host/disconnect_window_linux.cc +++ b/remoting/host/disconnect_window_linux.cc @@ -11,9 +11,14 @@ #include "base/check_op.h" #include "base/compiler_specific.h" #include "base/functional/bind.h" +#include "base/location.h" #include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/notimplemented.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "base/timer/timer.h" #include "remoting/base/string_resources.h" #include "remoting/host/client_session_control.h" #include "remoting/host/host_window.h" @@ -24,6 +29,22 @@ namespace { +// The amount of time to wait before allowing another position toggle. +constexpr base::TimeDelta kToggleCooldown = base::Seconds(3); + +// 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; +constexpr int kBottomMargin = 60; + +enum class WindowAnchor { + kBottom, + kTop, +}; + +// Remembers the last selected anchor position across dialog instances. +WindowAnchor g_current_anchor = WindowAnchor::kBottom; + class DisconnectWindowGtk : public HostWindow { public: DisconnectWindowGtk(); @@ -40,14 +61,32 @@ private: gboolean OnDelete(GtkWidget* window, GdkEvent* event); void OnClicked(GtkButton* button); + void OnToggleClicked(GtkButton* button); gboolean OnConfigure(GtkWidget* widget, GdkEventConfigure* event); gboolean OnDraw(GtkWidget* widget, cairo_t* cr); - gboolean OnButtonPress(GtkWidget* widget, GdkEventButton* event); +#if !GTK_CHECK_VERSION(3, 90, 0) + void OnMonitorsChanged(GdkScreen* screen); +#endif + + // Positions the dialog window based on the current anchor. + void SetDialogPosition(); + + // Toggles the dialog anchor between top and bottom. + void ToggleAlignment(); + + // Re-enables the toggle button when cooldown expires. + void OnCooldownExpired(); + + // Updates the toggle button text according to the current anchor. + void UpdateToggleButtonText(); // Used to disconnect the client session. base::WeakPtr<ClientSessionControl> client_session_control_; + base::OneShotTimer cooldown_timer_; + raw_ptr<GtkWidget> disconnect_window_; + raw_ptr<GtkWidget> toggle_button_; raw_ptr<GtkWidget> message_; raw_ptr<GtkWidget> button_; @@ -57,6 +96,8 @@ int current_height_; std::vector<ScopedGSignal> signals_; + + base::WeakPtrFactory<DisconnectWindowGtk> weak_factory_{this}; }; // Helper function for creating a rectangular path with rounded corners, as @@ -102,44 +143,25 @@ cairo_set_line_width(cairo_context, 6); AddRoundRectPath(cairo_context, width, height, kCornerRadius); cairo_stroke(cairo_context); - - // Render the window-gripper. In order for a straight line to light up - // single pixels, Cairo requires the coordinates to have fractional - // components of 0.5 (so the "/ 2" is a deliberate integer division). - double gripper_top = height / 2 - 10.5; - double gripper_bottom = height / 2 + 10.5; - cairo_set_line_width(cairo_context, 1); - - double x = 12.5; - cairo_set_source_rgb(cairo_context, 0.70, 0.70, 0.70); - cairo_move_to(cairo_context, x, gripper_top); - cairo_line_to(cairo_context, x, gripper_bottom); - cairo_stroke(cairo_context); - x += 3; - cairo_move_to(cairo_context, x, gripper_top); - cairo_line_to(cairo_context, x, gripper_bottom); - cairo_stroke(cairo_context); - - x -= 2; - cairo_set_source_rgb(cairo_context, 0.97, 0.97, 0.97); - cairo_move_to(cairo_context, x, gripper_top); - cairo_line_to(cairo_context, x, gripper_bottom); - cairo_stroke(cairo_context); - x += 3; - cairo_move_to(cairo_context, x, gripper_top); - cairo_line_to(cairo_context, x, gripper_bottom); - cairo_stroke(cairo_context); } DisconnectWindowGtk::DisconnectWindowGtk() - : disconnect_window_(nullptr), current_width_(0), current_height_(0) {} + : disconnect_window_(nullptr), + toggle_button_(nullptr), + message_(nullptr), + button_(nullptr), + current_width_(0), + current_height_(0) {} DisconnectWindowGtk::~DisconnectWindowGtk() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); if (disconnect_window_) { - gtk_widget_destroy(disconnect_window_); + gtk_widget_destroy(disconnect_window_.get()); disconnect_window_ = nullptr; + toggle_button_ = nullptr; + message_ = nullptr; + button_ = nullptr; } } @@ -184,7 +206,7 @@ // Allow custom rendering of the background pixmap. #if !GTK_CHECK_VERSION(3, 90, 0) - gtk_widget_set_app_paintable(disconnect_window_, TRUE); + gtk_widget_set_app_paintable(disconnect_window_.get(), TRUE); #endif connect(disconnect_window_.get(), "draw", &DisconnectWindowGtk::OnDraw); @@ -197,22 +219,12 @@ connect(disconnect_window_.get(), "configure-event", &DisconnectWindowGtk::OnConfigure); - // Handle mouse events to allow the user to drag the window around. -#if !GTK_CHECK_VERSION(3, 90, 0) - gtk_widget_set_events(disconnect_window_, GDK_BUTTON_PRESS_MASK); -#endif - connect(disconnect_window_.get(), "button-press-event", - &DisconnectWindowGtk::OnButtonPress); - - // All magic numbers taken from screen shots provided by UX. - // The alignment sets narrow margins at the top and bottom, compared with - // left and right. The left margin is made larger to accommodate the - // window movement gripper. + // Layout contains: toggle button, message label, and disconnect button. GtkWidget* button_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12); gtk_box_set_homogeneous(GTK_BOX(button_row), FALSE); #if GTK_CHECK_VERSION(3, 90, 0) - gtk_widget_set_margin_start(GTK_WIDGET(button_row), 24); + gtk_widget_set_margin_start(GTK_WIDGET(button_row), 12); gtk_widget_set_margin_end(GTK_WIDGET(button_row), 12); gtk_widget_set_margin_top(GTK_WIDGET(button_row), 8); gtk_widget_set_margin_bottom(GTK_WIDGET(button_row), 8); @@ -220,30 +232,41 @@ #else G_GNUC_BEGIN_IGNORE_DEPRECATIONS; GtkWidget* align = gtk_alignment_new(0, 0, 1, 1); - gtk_alignment_set_padding(GTK_ALIGNMENT(align), 8, 8, 24, 12); + gtk_alignment_set_padding(GTK_ALIGNMENT(align), 8, 8, 12, 12); G_GNUC_END_IGNORE_DEPRECATIONS; gtk_container_add(GTK_CONTAINER(window), align); gtk_container_add(GTK_CONTAINER(align), button_row); #endif + toggle_button_ = gtk_button_new(); + UpdateToggleButtonText(); +#if GTK_CHECK_VERSION(3, 90, 0) + gtk_box_pack_start(GTK_BOX(button_row), toggle_button_.get()); +#else + gtk_box_pack_start(GTK_BOX(button_row), toggle_button_.get(), FALSE, FALSE, + 0); +#endif + connect(GTK_BUTTON(toggle_button_.get()), "clicked", + &DisconnectWindowGtk::OnToggleClicked); + + message_ = gtk_label_new(nullptr); +#if GTK_CHECK_VERSION(3, 90, 0) + gtk_box_pack_start(GTK_BOX(button_row), message_.get()); +#else + gtk_box_pack_start(GTK_BOX(button_row), message_.get(), FALSE, FALSE, 0); +#endif + button_ = gtk_button_new_with_label( l10n_util::GetStringUTF8(IDS_STOP_SHARING_BUTTON).c_str()); #if GTK_CHECK_VERSION(3, 90, 0) - gtk_box_pack_end(GTK_BOX(button_row), button_); + gtk_box_pack_end(GTK_BOX(button_row), button_.get()); #else - gtk_box_pack_end(GTK_BOX(button_row), button_, FALSE, FALSE, 0); + gtk_box_pack_end(GTK_BOX(button_row), button_.get(), FALSE, FALSE, 0); #endif connect(GTK_BUTTON(button_.get()), "clicked", &DisconnectWindowGtk::OnClicked); - message_ = gtk_label_new(nullptr); -#if GTK_CHECK_VERSION(3, 90, 0) - gtk_box_pack_end(GTK_BOX(button_row), message_); -#else - gtk_box_pack_end(GTK_BOX(button_row), message_, FALSE, FALSE, 0); -#endif - // Override any theme setting for the text color, so that the text is // readable against the window's background pixmap. PangoAttrList* attributes = pango_attr_list_new(); @@ -254,14 +277,19 @@ #if !GTK_CHECK_VERSION(3, 90, 0) // GTK4 always uses an RGBA visual for windows. - GdkScreen* screen = gtk_widget_get_screen(disconnect_window_); - GdkVisual* visual = gdk_screen_get_rgba_visual(screen); - if (visual) { - gtk_widget_set_visual(disconnect_window_, visual); + GdkScreen* screen = gtk_widget_get_screen(disconnect_window_.get()); + if (screen) { + connect(screen, "monitors-changed", + &DisconnectWindowGtk::OnMonitorsChanged); + connect(screen, "size-changed", &DisconnectWindowGtk::OnMonitorsChanged); + GdkVisual* visual = gdk_screen_get_rgba_visual(screen); + if (visual) { + gtk_widget_set_visual(disconnect_window_.get(), visual); + } } // GTK4 shows windows by default. - gtk_widget_show_all(disconnect_window_); + gtk_widget_show_all(disconnect_window_.get()); #endif // Extract the user name from the JID. @@ -271,6 +299,7 @@ gtk_label_set_text( GTK_LABEL(message_.get()), l10n_util::GetStringFUTF8(IDS_MESSAGE_SHARED, username).c_str()); + SetDialogPosition(); gtk_window_present(window); } @@ -283,6 +312,102 @@ } } +void DisconnectWindowGtk::OnToggleClicked(GtkButton* button) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + ToggleAlignment(); +} + +void DisconnectWindowGtk::ToggleAlignment() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + g_current_anchor = (g_current_anchor == WindowAnchor::kBottom) + ? WindowAnchor::kTop + : WindowAnchor::kBottom; + UpdateToggleButtonText(); + if (toggle_button_) { + gtk_widget_set_sensitive(toggle_button_.get(), FALSE); + cooldown_timer_.Start( + FROM_HERE, kToggleCooldown, + base::BindOnce(&DisconnectWindowGtk::OnCooldownExpired, + weak_factory_.GetWeakPtr())); + } + SetDialogPosition(); +} + +void DisconnectWindowGtk::OnCooldownExpired() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + if (toggle_button_) { + gtk_widget_set_sensitive(toggle_button_.get(), TRUE); + } +} + +void DisconnectWindowGtk::UpdateToggleButtonText() { + if (toggle_button_) { + gtk_button_set_label( + GTK_BUTTON(toggle_button_.get()), + (g_current_anchor == WindowAnchor::kBottom) ? "▲" : "▼"); + } +} + +void DisconnectWindowGtk::SetDialogPosition() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + +#if !GTK_CHECK_VERSION(3, 90, 0) + if (!disconnect_window_) { + return; + } + + GdkDisplay* display = gtk_widget_get_display(disconnect_window_.get()); + if (!display) { + return; + } + + GdkMonitor* monitor = nullptr; + GdkWindow* gdk_window = gtk_widget_get_window(disconnect_window_.get()); + if (gdk_window) { + monitor = gdk_display_get_monitor_at_window(display, gdk_window); + } + if (!monitor) { + monitor = gdk_display_get_primary_monitor(display); + } + if (!monitor) { + monitor = gdk_display_get_monitor(display, 0); + } + if (!monitor) { + return; + } + + GdkRectangle geometry; + gdk_monitor_get_geometry(monitor, &geometry); + + int width = current_width_; + int height = current_height_; + if (width == 0 || height == 0) { + GtkRequisition requisition; + gtk_widget_get_preferred_size(disconnect_window_.get(), nullptr, + &requisition); + width = requisition.width; + height = requisition.height; + } + + int left = geometry.x + (geometry.width - width) / 2; + int top = (g_current_anchor == WindowAnchor::kTop) + ? (geometry.y + kTopMargin) + : (geometry.y + geometry.height - height - kBottomMargin); + + gtk_window_move(GTK_WINDOW(disconnect_window_.get()), left, top); +#else + NOTIMPLEMENTED_LOG_ONCE() + << "Window positioning is not implemented for GTK4/Wayland."; +#endif +} + +#if !GTK_CHECK_VERSION(3, 90, 0) +void DisconnectWindowGtk::OnMonitorsChanged(GdkScreen* screen) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + SetDialogPosition(); +} +#endif + gboolean DisconnectWindowGtk::OnDelete(GtkWidget* window, GdkEvent* event) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); ... (file diff truncated)
Original Bug Report
Potential bypass of Chrome Remote Desktop persistent security indicator
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: Authenticated remote clients can hide the ‘Stop Sharing’ dialog by dragging it off-screen, suppressing the local user’s primary security indicator and disconnect button. This occurs across Windows, macOS, and Linux because the host application lacks window position clamping and allows remote users to interact with the dialog’s draggable surfaces.
Affected files:
remoting/host/disconnect_window_win.ccremoting/host/win/core.rc.jinja2remoting/host/input_injector_win.ccremoting/host/it2me_desktop_environment.ccremoting/host/me2me_desktop_environment.ccremoting/host/input_monitor/local_mouse_input_monitor_win.ccremoting/host/disconnect_window_mac.mmremoting/host/disconnect_window_linux.cc
Estimated timestamp from git blame: 2011-10-24
The Chrome Remote Desktop (CRD) host displays a ‘Stop Sharing’ dialog to satisfy a critical security requirement: notifying the local user that their desktop is being accessed remotely and providing a persistent, one-click mechanism to terminate the session. A defense-in-depth hardening gap allows an authenticated remote client to move this dialog outside the visible screen area, effectively hiding it.
Technical Analysis
On Windows, the disconnect dialog is a borderless WS_POPUP window. Its message handler in remoting/host/disconnect_window_win.cc responds to WM_NCHITTEST by returning HTCAPTION (line 286), which treats the entire dialog surface as a draggable title bar. While the host uses LocalMouseInputMonitorWin to detect local activity and re-center the window, it explicitly ignores injected input (events with a null hDevice handle, as seen in remoting/host/input_monitor/local_mouse_input_monitor_win.cc line 58). This allows a remote attacker to initiate a system move loop and drag the window off-screen without triggering the ’local input’ re-centering logic.
Furthermore, in Me2Me (personal access) sessions, the dialog uses an internal flag local_input_seen_ to manage positioning. Once any genuine local input is detected, this flag is set to true. Subsequent calls to ShowDialog()—which occur after the window auto-hides—will skip the re-centering logic if this flag is set (remoting/host/disconnect_window_win.cc line 370). Consequently, if an attacker drags the window off-screen after the flag is set, it will remain hidden indefinitely.
Similar behavior is reproducible on other platforms:
- macOS: The window is draggable because
movableByWindowBackgroundis set toYESinremoting/host/disconnect_window_mac.mm(line 240). - Linux: The window can be moved via
gtk_window_begin_move_draginremoting/host/disconnect_window_linux.cc(line 324).
Potential Reproduction Steps
- Establish a Chrome Remote Desktop session with a Windows host.
- From the remote client, perform a mouse-down event on the ‘Stop Sharing’ dialog surface.
- Drag the cursor to the far corner of the virtual screen (e.g., coordinate 0,0).
- Release the mouse button.
- Observe that the dialog remains at the off-screen position, removing the visible indicator and the disconnect button for the local user.
Suggested Fix
- Window Clamping: Implement handlers for window movement events (e.g.,
WM_WINDOWPOSCHANGINGon Windows) to clamp the dialog position within the work area of the current monitor. - Restrict Draggability: Disable the ability to drag the window by its background. If draggability is required, it should be limited to a specific, small handle that is less likely to be exploited by remote input.
- Forced Re-centering: Modify the
ShowDialog()logic to verify that the window is within the visible work area before displaying it. If the window is off-screen, it should be re-centered regardless of the state of thelocal_input_seen_flag.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.