CVE-2026-0901
Overview
Background
- Paint-holding
- after a navigation, the browser keeps showing the previous page’s last frame until the new renderer has produced its first frame, and during that time
RenderWidgetHostkeeps theInputRouterinactive (per the comment added inrender_input_router.cc). - `kDropInputEventsWhilePaintHolding`
- a Blink feature flag whose description reads “Drop input events at the browser process until the process receives the first signal that the renderer has sent a frame to cc (https://crbug.com/40057499)" (
features.cc:612-615). - `InputRouterImpl::FilterAndSendWebInputEvent`
- the browser-side choke point that asks the client to filter an event and, if
WasHandled(...), acks it locally instead of sending it to the renderer (input_router_impl.cc:658-679). - `kNoConsumerExists`
- an
InputEventResultStatethatWasHandledtreats as handled (input_router_impl.cc:55-60), so the event is not dispatched to the renderer.
Root Cause Analysis
e59c87967184. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (f95480a21202).Before the enable commit, kDropInputEventsWhilePaintHolding defaulted to base::FEATURE_DISABLED_BY_DEFAULT (features.cc:614-615). Every early-return that drops input while the router is inactive was gated on that flag: !IsActive() && base::FeatureList::IsEnabled(...) in SendMouseEvent, SendWheelEvent, SendKeyboardEvent, SendGestureEvent and SendTouchEvent (input_router_impl.cc:110-112, 138-139, 152-153, 178-179, 339-340). In the default configuration, input that arrived while the browser was still holding the previous page’s frame therefore went on to FilterAndSendWebInputEvent and was dispatched to the newly navigated page (input_router_impl.cc:681-682; for events that block the event stream, the DispatchEvent call at 726-729). A user could click on what looked like the old page while the click actually landed in the new, not-yet-painted page. That is the click passthrough the reporter describes. The violated invariant is that a page should not receive user input before any of its content has been shown to the user. Mechanism commit 1fda96c7 removes the five per-event checks and moves the check into RenderInputRouter::FilterInputEvent, which now returns kNoConsumerExists when the flag is on and !input_router_->IsActive().
As a result, FilterAndSendWebInputEvent acks the event in the browser through its existing WasHandled branch (input_router_impl.cc:669-678), which according to the commit message also fixes ack handling for touch. Primary commit e59c8796 then flips the flag to base::FEATURE_ENABLED_BY_DEFAULT and removes the now-redundant field-trial entry, so the drop is active by default.
Attack Path
- Delay the first frame of a navigation
A page navigates (for example with
location.hreforwindow.open, per the reporter) while the browser is still showing a previously painted page. The reporter’s heavy-rendering trigger only lengthens this window, and the code fix does not touch rendering. - Paint-holding keeps the old frame
Until the new renderer produces a frame, the user still sees the earlier content. During this period
RenderWidgetHostkeeps theInputRouterinactive (per the addedrender_input_router.cccomment). The inactive router does not cause the stale display; it only marks the window in which input should be dropped. - User input reaches the new page
Before the fix, with the flag off by default, the
!IsActive()guards did not fire, and taps or clicks went throughFilterAndSendWebInputEventto the new renderer (input_router_impl.cc:681-682). - Clickjacking
The user’s interaction meant for the visible content triggers actions in the hidden target page, for example a permission prompt (inferred from the reporter’s choice of
permission.site).
Impact Assessment
Files Changed
testing/variations/fieldtrial_testing_config.jsonthird_party/blink/common/features.cc
Audit Directions
- Security guards behind disabled-by-default flagsLook for
BASE_FEATURE(..., FEATURE_DISABLED_BY_DEFAULT)declarations whose comments cite a security bug. A mitigation that only ships through field trials leaves the default build exposed. - Input delivery vs. visible contentCheck other states in which the displayed frame and the input target differ, such as other navigation or activation transitions (inferred). Confirm that input is gated on whether the target has actually painted.
- Per-event-type duplicated checksWhen a filter is copied into each
Send*Eventmethod, check that every type, including queued or deferred gestures and touch acks, goes through the same path and gets the same ack handling.
Patch
diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index 9ca53071..76c1756a 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -9462,27 +9462,6 @@
]
}
],
- "DropInputEventsWhilePaintHolding": [
- {
- "platforms": [
- "android",
- "android_weblayer",
- "android_webview",
- "chromeos",
- "linux",
- "mac",
- "windows"
- ],
- "experiments": [
- {
- "name": "Enabled",
- "enable_features": [
- "DropInputEventsWhilePaintHolding"
- ]
- }
- ]
- }
- ],
"DropMetricsFromNonProducedFramesOnlyIfTheyHadNoDamage": [
{
"platforms": [
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index 6a6eb14..e40bad84 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -612,7 +612,7 @@
// Drop input events at the browser process until the process receives the first
// signal that the renderer has sent a frame to cc (https://crbug.com/40057499).
BASE_FEATURE(kDropInputEventsWhilePaintHolding,
- base::FEATURE_DISABLED_BY_DEFAULT);
+ base::FEATURE_ENABLED_BY_DEFAULT);
// Extends console.timestamp to support adding deep-links into the DevTools
// Performance Panel, which (when clicked) call into a DevTools extension.
Original Bug Report
Security: Busy Rendering Cause Layer to Persist on Target Website Allow Address Bar Spoofing & Clickjacking Attack
VULNERABILITY DETAILS
When using huge CSS font-size, line height then combined with large border radius, Chromium will appear sluggish when rendering the page. After first-paint rendering, zooming in and scroling the page will also prolong busy rendering times.
While Google Chrome busy rendering the page layer, then subsequently visit another website (e.g. example.com, permission.site, etc.) the busy layer will persist on target website, more interestingly sometimes the target page DOM is interactable while page shows the spoofed busy page layers which leads to clickjacking attack.
I can only reproduce this on Chrome for Android, is it possible because of “Multiple Raster Threads: disabled” on Android? I think it’s a regression, because on older versions of Chrome it won’t feel sluggish when rendering the page so the layer will not persist.
TESTED WORKING ON
- Chrome 94.0.4606.71 on Android 11; Mi 9T
- Chrome Dev 96.0.4655.4 on Android 11; Mi 9T
- Chrome Canary 96.0.4659.3 on Android 11; Mi 9T
- Chrome 94.0.4606.61 on Android 11; Redmi Note 9 Pro
- Chrome Dev 96.0.4655.4 on Android 11; Redmi Note 9 Pro
- Vivaldi Snapshot (UA Chrome/94.0.4606.47) on Android Emulator Pixel_2_API_29 (Play Store edition)
Graphics Feature Status (chrome://gpu) on Mi 9T
- Canvas: Hardware accelerated
- Canvas out-of-process rasterization: Enabled
- Compositing: Hardware accelerated
- Multiple Raster Threads: Disabled
- Out-of-process Rasterization: Hardware accelerated
- OpenGL: Enabled
- Rasterization: Hardware accelerated
- Skia Renderer: Enabled
- Surface Control: Enabled
- Video Decode: Hardware accelerated
- Vulkan: Disabled
- WebGL: Hardware accelerated
- WebGL2: Hardware accelerated
REPRODUCTION CASE
A) Steps to reproduce (location.href)
- Visit attached spoof-locationhref.html
- After few seconds the address bar changed to https://permission.site while page layer still on spoof-locationhref.html.
- Try clicking anywhere on the page, sometimes the click will passthrough to permission.site while page layer still on spoof page.
B) Steps to reproduce (window.open)
- Visit attached spoof-windowopen.html
- After couple of seconds, tap the page to trigger the window.open
- While layer still on spoof page, try clicking anywhere on the page, sometimes the click will passthrough to permission.site while page layer still on spoof page.
(If the spoof layer not persist on permission.site as on PoC video, try zooming-in or scrolling the page (to prolong the busy rendering) before tap the page or location.href changed to permission.site)
CREDIT INFORMATION
Irvan Kurniawan (sourc7)
Busy Rendering Spoof Layer with window.open (interactable clickjack to GitHub).mp4Facebook_Canva_Request_Access_Chrome.jpgFacebook_Overlay_Page_One_Tap_Authorize_App_Chrome_Samsung_S23.mp4Facebook_Tiktok_Request_Access_Chrome.jpgGitHub_Authorize_Apps.jpgGitLab_Authorize_Apps.jpgScreen_Recording_20260123_034711_Chrome_Samsung_S23.mp4Spoof Layer reproducible on Android Emulator Pixel_2_API_29 with Vivaldi UA Chrome 94.0.4606.47.mp4Spoof layer and click passthrough on security_interstitials page.mp4android-stable.pngcrbug40057499-uma-affected-by-preloading.webmspoof layer click passthrough to googlechrome.github.io.mp4spoof-locationhref-badssl-fps.htmlspoof-locationhref-fps.htmlspoof-locationhref.htmlspoof-windowopen-badssl.htmlspoof-windowopen.htmlspoof4-tweaked-rescale1.htmlspoof4-tweaked-timed-facebook-authorize.htmlspoof4-tweaked-timed-touchscreentest.htmltouch_interactable_passthrough_test_Chrome_Samsung_S23.mp4trace_android.json.gz