CVE-2025-12726
Overview
Background
- `EnvInputStateController`
- the Aura helper in
ui/aura/env_input_state_controller.ccthat tracks global input state (mouse buttons, touch points) and mirrors it into the process-wideaura::Env. - `touch_ids_down_`
- a bitmask with one bit per active touch pointer id; a bit is set on
ui::EventType::kTouchPressedand cleared on release, andenv_->SetTouchDown(touch_ids_down_ != 0)publishes whether any finger is down. - `ui::EventType::kTouchCancelled`
- an event that ends a touch sequence without a normal release; it can come from the OS (a native event) or be synthesized inside Chrome, in which case
event.HasNativeEvent()is false. - ChromeOS exo touch drag
- a ChromeOS-only path (Wayland/exo) that cancels touch-downs with synthetic events when handing a drag to a new consumer, and needs the global touch-down state to survive that hand-off.
Root Cause Analysis
In the touch-event switch in ui/aura/env_input_state_controller.cc, the kTouchCancelled case used to break out early whenever the cancel had no native event (if (!event.HasNativeEvent()) break;). Only native cancels fell through to the kTouchReleased code that clears the pointer’s bit in touch_ids_down_. This applied on every Aura platform. So when Chrome itself synthesized a touch cancel, the pointer’s bit stayed set, and the controller went on reporting a touch as down after the touch sequence had ended. That breaks the rule that a touch sequence ended by any terminal event (release or cancel) is removed from global touch state.
The fix wraps the native-event filter in #if BUILDFLAG(IS_CHROMEOS) and adds #include "build/build_config.h". On every other platform, every kTouchCancelled (synthetic or native) now falls through to the release path, and the bit is cleared. ChromeOS keeps the old filter on purpose, because exo touch drag depends on it.
Attack Path
- Touch sequence begins
The user touches the screen on a non-ChromeOS Aura build (for example Windows or Linux with a touchscreen).
kTouchPressedsets the pointer’s bit intouch_ids_down_, andenv_->SetTouchDown(true)is published. - Chrome synthesizes a cancel
Some internal Chrome logic ends that touch with a synthetic
kTouchCancelledthat has no native event. The diff does not show which producers emit these synthetic cancels. - State is not cleared
Before the patch, the
!event.HasNativeEvent()check sent the event tobreak. The release-path bit clear never ran, so the global touch-down state stayed asserted after the finger was logically gone. - Downstream UI acts on stale state (inferred) Views/Aura code that checks the global touch-down state could treat later interactions as happening during an active touch. A page that can arrange for touches to be cancelled might use this to make browser UI behave inappropriately. The specific consumer and its security effect are not shown in the diff.
Impact Assessment
(inferred), because the patch does not show which consumer misbehaves. Reaching it requires an Aura platform other than ChromeOS, real touch input from the user, and a code path that synthesizes a touch cancel. Ordinary web content does not drive this state directly, and nothing in the diff indicates that a compromised renderer, a flag or an extension is needed. The vendor rates this high (“Inappropriate implementation in Views”). The provided metadata does not say whether it was exploited in the wild.Files Changed
ui/aura/env_input_state_controller.cc
Audit Directions
- Event filters that skip synthetic eventsSearch input-state trackers (
ui/aura,ui/events,ui/views) forHasNativeEvent()checks that skip terminal events (cancel, release, capture-lost). Skipping a terminal event leaves “pressed” or “down” bookkeeping stuck. - Platform-specific workarounds applied globallyLook for behaviour added for one platform (exo, Wayland, ChromeOS) without a
BUILDFLAGguard, which then changes state handling on Windows and Linux. - Consumers of global pointer stateCheck which Views/Aura code uses
Env’s touch-down or mouse-button state to gate security-relevant UI (dialogs, drag-and-drop, prompts). Confirm that a stale value cannot weaken a gesture or input-protection decision.
Patch
From 083cac2c44a50242c7ac100f8fed5fc66ead9958 Mon Sep 17 00:00:00 2001 From: David Bienvenu <[email protected]> Date: Thu, 16 Oct 2025 14:29:16 -0700 Subject: [PATCH] Fix handling of touch state in EnvInputStateController Change-Id: I7357dff83de347207e6f0016429b3314daf2d5a1 Bug: 447172715 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7032454 Reviewed-by: Nico Weber <[email protected]> Commit-Queue: David Bienvenu <[email protected]> Reviewed-by: Mustaq Ahmed <[email protected]> Cr-Commit-Position: refs/heads/main@{#1531080} --- diff --git a/ui/aura/env_input_state_controller.cc b/ui/aura/env_input_state_controller.cc index 75da86584..9371488 100644 --- a/ui/aura/env_input_state_controller.cc +++ b/ui/aura/env_input_state_controller.cc @@ -4,6 +4,7 @@ #include "ui/aura/env_input_state_controller.h" +#include "build/build_config.h" #include "ui/aura/client/screen_position_client.h" #include "ui/aura/env.h" #include "ui/events/event.h" @@ -48,11 +49,17 @@ env_->SetTouchDown(touch_ids_down_ != 0); break; - // Handle EventType::kTouchCancelled only if it has a native event. case ui::EventType::kTouchCancelled: - if (!event.HasNativeEvent()) +#if BUILDFLAG(IS_CHROMEOS) + // Handle EventType::kTouchCancelled only if it has a native event. + // ChromeOS exo touch drag relies on the ability to cancel touch + // downs with synthetic events when handing off to the new consumer, + // without losing the global env touch down state. + if (!event.HasNativeEvent()) { break; + } [[fallthrough]]; +#endif // BUILDFLAG(IS_CHROMEOS) case ui::EventType::kTouchReleased: touch_ids_down_ = (touch_ids_down_ | (1 << event.pointer_details().id)) ^ (1 << event.pointer_details().id);
Original Bug Report
Security: Compromised renderer can control mouse after single tap (UXSS, sandbox escape, and more)
SUMMARY
Similar to issue 370856871, on Windows a compromised renderer can call StartDragging() to control the mouse and perform clicks. The existing mitigations can be bypassed after a single user tap until browser shutdown, allowing for many of the same impacts as the referenced issue. After the initial tap, all attacks can be performed an infinite amount of times, in any combination, at any time (such as when user is away from computer).
Standalone, we have a one-tap sandbox escape, UXSS, and other impacts.
When chained with other vulnerabilities, we have a one-tap sandbox escape with Mark of the Web (MOTW) bypass. I’ll post the chain in comments.
VULNERABILITY DETAILS
Background
A compromised renderer can call RFHI::StartDragging(), which eventually calls WebContentsViewAura::StartDragging() which ultimately calls DesktopDragDropClientWin::StartDragAndDrop().
StartDragAndDrop() performs mouse clicks through OS API calls in DesktopWindowTreeHostWin::StartTouchDrag() + ::FinishTouchDrag() and also calls the OS ::DoDragDrop() method.
Important concept: An aura::Window “represents virtual windows, including tabs, bubbles and menus” (source: crbug comment). It doesn’t always correspond to what a user typically considers a window. As a mental shortcut, in the main browser window, any UI that is typically visible immediately after navigating to a web page is likely within the same window as the web page content.
Bugs in existing mitigations
The fix for issue 370856871 added two checks within StartDragAndDrop():
touch_downmitigation: Checks if Aurais_touch_down()returns true. This will return true if on anyaura::Window, a touch was started (kTouchPressed) but has not finished (kTouchReleased) or cancelled (kTouchCancelled). Aura only tracks touches initiated by physical touch, privileged CDP client, or OS input APIs.touch_over_other_windowmitigation: Checks if cursor is over the currentaura::Window. This fails if it’s over a non-aura::Windowor a differentaura::Window.
Unfortunately, bugs prevent both of these mitigations from working as intended. There are also no restrictions on drag/clicks within the same aura::Window, which can still have serious impacts.
if (source == ui::mojom::DragEventSource::kTouch) {
// ...
aura::Window* window =
screen->GetWindowAtScreenPoint(screen->GetCursorScreenPoint()); // <-- Gets window at cursor (cursor is not necessarily at touch_screen_point)
// ...
bool touch_down = aura::Env::GetInstance()->is_touch_down();
bool touch_over_other_window =
!window || window->GetRootWindow() != root_window;
// Check that the cursor is over the window being dragged from. If not,
// don't start the drag because ::DoDragDrop will not do the drag.
if (!touch_down || touch_over_other_window) { // <-- Checks safe states before starting drag
return ui::PreferredDragOperation(
ui::DragDropTypes::DropEffectToDragOperation(DROPEFFECT_NONE));
}
desktop_host_->StartTouchDrag(touch_screen_point); // <-- Sends mouse left down and mouse move events
}
// ...
::DoDragDrop(...) // <-- Starts OS drag. Waits until drag finishes before continuing with funciton.
// ...
desktop_host_->FinishTouchDrag(touch_screen_point); // <-- Sends mouse left up event (to finish click) after drag completed
touch_down bypass
Due to an Aura bug, is_touch_down() can persistently return true and bypass the check after a single tap by user. The bypass persists until a full browser restart.
In our PoC, to reach the persistent touch down state, the user only needs to make a single tap. During the tap, the page opens a tab in the same window (and optionally closes the tab) within the touchstart event handler. After this, the touch down state is persistently true even after user finishes the tap, and attacks can be launched at any time until browser restart.
See Root Cause Analysis in comment for details, including other (potentially natural) ways to reach persistent touch down state.
touch_over_other_window bypass
This is a generally solid mitigation that mitigates most impacts against bubbles, menus, other browser windows, and non-browser windows. However, a race condition allows menus (e.g. browser menu, context menu) to bypass this check, despite being a different aura::Window.
If timed correctly, touch_over_other_window will be false when calling StartDragging() over a menu that is opening. The menu item will then be clicked as it opens. I am not sure exactly why this occurs, but it’s reliable enough for an attacker to use (usually works the first time, but in case of failure we can repeat infinitely with adjusted timing until we succeed). This means top-level menu options (i.e. not nested menu options) can be clicked on using StartDragging().
We can open and click on the browser menu (three dots on top-right of browser window) and context menu (renderer can call ShowContextMenu() to open it).
Same aura::Window drag/click allowed
As mentioned earlier, an aura::Window is conceptually different than what a user typically considers a window. If an interesting target exists within the same aura::Window, it can be clicked on because there is no mitigation against this.
We can click on any page contents, bookmarks bar, address bar, docked DevTools, and anything else within same aura::Window. We can also drag to bookmarks bar and address bar. We may also be able to drag to other UI elements.
IMPACTS
After the initial tap to trigger persistent touch down state, all attacks can be performed an infinite amount of times, in any combination, at any time (such as when user is away from computer).
Roughly divided by cause, or prerequsite of extension or chained vuln:
Impacts when chained with unfixed issue 443255991
- Sandbox escape with MOTW bypass: We can remove all user interaction requirements for issue 443255991’s PoCs. I’ll post details and chained PoCs in comments. Current PoC requires an extension.
Impacts due to same aura::Window allowed
- Create, click JS bookmarks (universal XSS): Web pages can create drags with
javascript:URLs, and the bookmarks bar will create/run bookmarks withjavascript:URLs (bookmarklets). Therefore, attacker can drag ajavascript:URL to the bookmarks bar, open a tab to the target website, and then click the attacker-created bookmark in the bookmarks bar. This gives us Universal XSS (UXSS) on anyhttp(s)://page and non-componentchrome-extension://page. WebUI pages and componentchrome-extension://pages are not allowed to run bookmarklets therefore they are not impacted. - Click on other page: We can open a tab to any web page and click anywhere on it. We can also click on any visible tab content, such as WebUI pages and docked DevTools.
- Click on side panel: We can click on any open side panel, such as Bookmarks side panel, GLIC side panel, extension side panels, etc.
Impacts due to touch_over_other_window bypass (menu race condition)
- Open Downloads page, run/open download (sandbox escape): We can click the browser menu button, then click “Downloads” to open the WebUI page. We can then click on downloaded items to run/open them and escape sandbox, bypassing user interaction requirements within browser.
- Open, click on split tab: Renderer can call
ShowContextMenu()to open context menu, click on “Open link in Split View”, and then click on the other side of the split tab. - Open Settings, change most settings: We can click the browser menu button, then click “Settings” to open the WebUI page. Most settings can be be changed, if they are buttons, toggles, dropdown menus, or otherwise clickable. This includes disabling Safe Browsing, and showing/hiding bookmarks bar (useful for UXSS impact).
- Open, click on DevTools: Renderer can call
ShowContextMenu()to open context menu, and then click on “Inspect” to open DevTools.
Impacts with malicious extension
An extension with no permissions can open any chrome:// URL that web pages cannot open. We can also enable internal debugging URLs in chrome://chrome-urls and open them.
- Open Extensions page, change extension settings (enable file access, incognito access): We can navigate to
chrome://extensions/?id={extId}, then click on the page to enable file access and incognito access. - Open site permissions, allow permissions: We can navigate to
chrome://settings/content/siteDetails?site={origin}and click to allow any listed permission, notably: camera, microphone, location, clipboard read, and local network access. - Toggle any chrome://flags: We can navigate to
chrome://flags, change any flags, and then restart the browser. This could be used to further exploit vulnerabilities in experimental Web/JS/GPU features or relax some security features. - Open DevTools for any target: We can open DevTools for any target through
chrome://inspect. We can then interact with the DevTools instance if it is docked to the same Aura window. With additional user interaction, there are significant impacts here that I’m still exploring (similar to issue 402791076: unrestricted CDP access, sandbox escape).
Prior impacts when chained with fixed issue 404000989
Prior to issue 404000989 being fixed in March 2025, a web page could drag/drop restricted URLs such as chrome:// or devtools:// URLs. The standalone vuln required drag and click user interactions to create bookmarks with restricted URLs and then navigate to them. With this drag/click vuln we can do both with one tap (or zero clicks after persistent touch down state). This would allow web pages to launch attacks that currently require extensions.
Prior impacts when chained with fixed issue 402791076
Prior to issue 402791076 being fixed in March 2025, we could gain XSS on DevTools. The standalone vuln required user clicking on a specific part of a page and then opening DevTools manually, but with this drag/click vuln we can do both with one tap (or zero clicks after persistent touch down state). This would allow further impacts, such as unrestricted CDP access, sandbox escape, and UXSS on any page (including WebUI pages).
BISECT
Can’t do proper bisect with custom build, but the initial fix was introduced in r1371525 (Oct 21, 2024). I started testing in ~r1508700 (August 29th, 2025) where it reproduced, although it likely reproduced since the fix on October 2024.
PROPOSED FIXES
One or more of these should mitigate most impacts:
-
Check position of renderer-provided drag start position in
StartDragAndDrop()to ensure it is within web page boundaries. Currently, code checks if the cursor position is within theaura::Window, which also allows parentaura::Windows such as the main browser window. The cursor check should remain for the functional reasons documented in code, but a security check should be added to validate the drag position itself. This does not mitigate all attacks (such as clicking on page context menu or on web page content), but mitigates most impacts. -
Ensure renderer that initiates drag is visible when calling
StartDragging(). This would prevent attacks on other tabs in the same window. -
Remove
event.HasNativeEvent()early break inEnvInputStateController::UpdateStateForTouchEvent. After doing some archeology on theHasNativeEvent()check, it was added in January 2013 as a fix for an X11 bug. The functionTouchEventIsGeneratedHack()is still present and used in X11 code, so it may still be needed to not regress the bug. If it’s still needed, we can add theHasNativeEvent()check only when using X11 (only Linux?) as a short-term fix. In theory the persistentis_touch_down()state may also occur in Linux, but should not have any security impacts there. -
Ensure only one drag is occuring at a time, and/or throttling drag starts. Some of the behaviors currently depend on multiple drags happening simultaneously or in close succession, so restricting active drags and throttling drags may make exploitation more difficult.
Additional mitigation:
- Prevent drag start from prerendered pages. I noticed that when we are in the persistent touch down state, we can initiate drags from a prerendered page. You can test this by triggering persistent touch down state, closing the page, and then typing/pasting the URL into address bar without navigating to the page. This may be useful in some attack scenarios.
VERSION
Chrome version: Verified with custom build based on 0ecd794f626107687c4844683a828953f49fb2b3 (Sept 11th, 2025)
Operating System: Windows 10
REPRODUCTION CASE
Patch to simulate compromised renderer
For all scenarios, apply attached patch and build Chromium.
The patch adds exploit logic to renderer:
- Adds calls to
StartDragging()andShowContextMenu()inthird_party/blink/renderer/core/dom/document.ccfor use by the exploit page. (This is loosely based on patch from issue 370856871.) - Bypasses popup blocker in
content/renderer/render_frame_impl.cc(this is same as earlier issue’s patch). - Disables multiple context menu check in
third_party/blink/renderer/core/page/context_menu_controller.ccwhich helps with context menu attacks.
The patch also adds logging and disables a DCHECK, only for development/debugging purposes:
- Disables DCHECK in
base/message_loop/message_pump_win.ccthat we hit during PoCs. You can alternatively build with DCHECKs disabled. - Adds logging in
content/browser/renderer_host/render_widget_host_view_aura.ccto show whenHide()is called (which would trigger persistent touch down state if tap is in progress). - Adds logging in
ui/aura/env.ccto showSetTouchDown(bool)calls. - Adds logging in
ui/aura/env_input_state_controller.ccto show state of touch tracking and processed touch events. - Adds logging in
ui/views/widget/desktop_aura/desktop_drag_drop_client_win.ccto show when operations occur, and iftouch_over_other_windowmitigation is hit. - Adds logging in
ui/views/widget/desktop_aura/desktop_window_tree_host_win.ccto show when mouse events are generated due to touch/drag operations.
Prerequisites
- A touch-enabled device or another way for OS/Aura to receive touch events (CDP should work).
- Compromised renderer (apply attached patch to simulate).
Web page scenario: Universal XSS (UXSS)
- Navigate to https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=uxss
- Tap anywhere, or do nothing if using autorun.
Observed:
- If bookmarks bar is hidden: Renderer opens Settings page through browser menu, clicks on Appearance, and enables “Show bookmarks bar”. Then continues below:
- If bookmarks bar is shown: Renderer opens tab to target website, drags a
javascript:URL to bookmarks bar to create bookmarklet, then clicks bookmarklet to run the JS payload on target site.
Expected: Renderer cannot click on bookmarks bar. Drag cannot start from background tab. Renderer can still start a javascript: URL drag to create a bookmarklet, since this is supported functionality.
Web page scenario: Click on page
- Navigate to https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=click-page
- Tap anywhere, or do nothing if using autorun.
Observed: Renderer opens tab to target website, and performs multiple clicks on target page. In this case, the clicks trigger navigations, but any action triggerable with clicks is possible.
Expected: Renderer cannot click on page on another tab. Drag cannot start from background tab.
Web page scenario: Open and click on split tab
- Navigate to https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=click-split-tab
- Tap anywhere, or do nothing if using autorun.
Observed: Renderer opens split tab to target website by opening and clicking on context menu (this may take several attempts). After opening split tab, renderer performs multiple clicks on the other split tab. In this case, the clicks trigger navigations, but any action triggerable with clicks is possible.
Expected: Renderer cannot click on context menu. Renderer cannot click on the other split tab.
Web page scenario: Open Settings and disable Safe Browsing
- Navigate to https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=open-settings-disable-safebrowsing
- Tap anywhere, or do nothing if using autorun.
Observed: Renderer opens Settings page by clicking on browser menu button, and then clicking “Settings” menu item. Renderer then performs multiple clicks on Settings page to disable Safe Browsing.
Expected: Renderer cannot click on browser menu button nor on browser menu items. Renderer cannot click on page on another tab. Drag cannot start from background tab.
Web page scenario: pwn all the things! 💥
Runs all web page scenarios. Use autorun for maximum pwnage.
- Navigate to https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=combo&autorun=1
- Do nothing. :)
Observed: After one tap, renderer can make multiple StartDragging() calls anytime until browser restart. This means attacker can run any scenario with no user interaction.
Expected: After one tap, renderer can only make a single StartDragging() call if drag isn’t currently occurring. is_touch_down() is true only when a physical or otherwise safe touch event is in progress.
Credit Information
Reporter credit: Alesandro Ortiz https://AlesandroOrtiz.com
basic-click-demo.htmlbasic-repro.logclick-monitor.htmldrag_diffs.txtdragclick-patch.diffdragclick-pwn-all-the-things.mp4dragclick-uxss-open-settings-show-bookmarks-bar.mp4dragclick-uxss.mp4dragclick-web-contents-bounds-bypass.mp4manifest.jsonmitigations.txtpage.htmlpersistent-touch-state.htmlself-closing.htmlself-closing.jstouch-drag-click-cr.htmltouch_diffs.txt
- https://AlesandroOrtiz.com
- https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=click-page
- https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=click-split-tab
- https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=combo&autorun=1
- https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=open-settings-disable-safebrowsing
- https://alesandroortiz.com/security/chromium/touch-drag-click-cr.html?mode=uxss
- https://chromium-review.googlesource.com/c/chromium/src/+/5943487
- https://crbug.com/169177
- https://crrev.com/ea7f83787239ae91db4ec297225905b0c8a5a595
- https://issuetracker.google.com/issues/370856871
- https://issuetracker.google.com/issues/402791076
- https://issuetracker.google.com/issues/404000989
- https://issuetracker.google.com/issues/443255991
- https://source.chromium.org/chromium/chromium/src/+/main:content/browser/web_contents/web_contents_view_aura.cc;l=1108;drc=181e7a80687c55d02e2e74d7d5db3d75ad53c0de
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/mojom/frame/frame.mojom;l=567;drc=f98c3dd3dc0193d10d5cfe549c98e2651244d567
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/mojom/frame/frame.mojom;l=797;drc=f98c3dd3dc0193d10d5cfe549c98e2651244d567
- https://source.chromium.org/chromium/chromium/src/+/main:ui/aura/env.h;l=97;drc=89f6321d4c72ccc4b16de1d3e700e66b878e624b
- https://source.chromium.org/chromium/chromium/src/+/main:ui/aura/env_input_state_controller.cc;l=46-60;drc=181e7a80687c55d02e2e74d7d5db3d75ad53c0de
- https://source.chromium.org/chromium/chromium/src/+/main:ui/aura/env_input_state_controller.cc;l=53;drc=e4f1aef5f3ec30a28950d766612cc2c04c822c71
- https://source.chromium.org/chromium/chromium/src/+/main:ui/events/x/events_x_utils.cc;l=109;drc=ffec0aed60ca494f2a228d0deba095d43ad8748b
- https://source.chromium.org/chromium/chromium/src/+/main:ui/views/widget/desktop_aura/desktop_drag_drop_client_win.cc;l=68;drc=181e7a80687c55d02e2e74d7d5db3d75ad53c0de
- https://source.chromium.org/chromium/chromium/src/+/main:ui/views/widget/desktop_aura/desktop_window_tree_host_win.cc;l=151-167;drc=0540bffebe0ced2a9ad02793b159a8af1a930b31