High watch ✔︎ verified CVSS 5.4 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Blink
DescriptionInappropriate implementation in Blink
ComponentBlink
Sub-componentUI>Input
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — factors set by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: Citations accurate. Minor: Attack step 2's 'so the user still sees the earlier content' wrongly implies the inactive InputRouter causes the stale frame; the fix addresses input delivery only, not the reporter's address-bar spoof. The 726-729 DispatchEvent is only the blocking-event path.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker40057499
Fix commite59c87967184 (chromium/src) +1/-22
Related commits1fda96c72f3c mechanism · chromium/src
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedIrvan Kurniawan (sourc7)
Reported2021-10-04 (per vendor advisory)
Fix landed2026-01-07 (public git)
Disclosed2026-01-13
Patch gap6 days — fix public in source before the advisory

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 RenderWidgetHost keeps the InputRouter inactive (per the comment added in render_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 InputEventResultState that WasHandled treats as handled (input_router_impl.cc:55-60), so the event is not dispatched to the renderer.

Root Cause Analysis

Re-analyzed on 2026-10-03 against the actual fix commit 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.

Key insight
During paint-holding, the browser routed input to a page the user could not yet see, because the existing guard was disabled by default. The fix routes the guard through the common input filter and turns it on by default, so input is dropped until the new page has produced a frame.

Attack Path

  1. Delay the first frame of a navigation A page navigates (for example with location.href or window.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.
  2. Paint-holding keeps the old frame Until the new renderer produces a frame, the user still sees the earlier content. During this period RenderWidgetHost keeps the InputRouter inactive (per the added render_input_router.cc comment). The inactive router does not cause the stale display; it only marks the window in which input should be dropped.
  3. 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 through FilterAndSendWebInputEvent to the new renderer (input_router_impl.cc:681-682).
  4. 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

A malicious page can make the user’s clicks or taps land in a different, not-yet-displayed page, which is a UI-mismatch/clickjacking primitive across a navigation. The decision to drop or deliver input is made in the browser process, but the input lands in the target page’s renderer. The attack needs user interaction, and the reporter reproduced it only on Android. The fix only changes input delivery. It does not change the persisting stale frame or the address-bar mismatch that the reporter also describes. The advisory rates it high and reports no in-the-wild exploitation.

Files Changed

  • testing/variations/fieldtrial_testing_config.json
  • third_party/blink/common/features.cc

Audit Directions

  • Security guards behind disabled-by-default flags
    Look 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 content
    Check 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 checks
    When a filter is copied into each Send*Event method, check that every type, including queued or deferred gestures and touch acks, goes through the same path and gets the same ack handling.
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.
Loading diff…

Original Bug Report

reported by su...@gmail.com

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)

  1. Visit attached spoof-locationhref.html
  2. After few seconds the address bar changed to https://permission.site while page layer still on spoof-locationhref.html.
  3. 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)

  1. Visit attached spoof-windowopen.html
  2. After couple of seconds, tap the page to trigger the window.open
  3. 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)

View on issue tracker