[7] label-forwarded clicks promoted to isTrusted=true via dispatchSimulatedClick
Rated Medium because the diff fixes a deterministic Event.isTrusted spoofing path via <label> forwarding; the immediate concrete consequence is bypass of the haptic-feedback trusted-event gate on <input type=checkbox switch>, with broader impact bounded by which other features gate on isTrusted for clicks on label-associated controls.
288403@main ensured that haptic feedback for <input type=checkbox switch> required user activation. However, it is also desired that haptics are only triggered for trusted events. This goal can currently be bypassed by calling click() on a label associated with the input. The underlying issue is that Element::dispatchSimulatedClick unconditionally sets SimulatedClickSource::UserAgent. Fix by specifying SimulatedClickSource::Bindings if there is an underlying event and it is untrusted.
Source/WebCore/dom/Element.cpp
LayoutTests/fast/forms/label/label-click-event-dispatch-untrusted.html
Patch Details
Element::dispatchSimulatedClick now derives the SimulatedClickSource from the trust state of the underlying event: UserAgent only when the underlying event is itself trusted, Bindings otherwise. When no underlying event is present (the path used by Element::click() itself), the behaviour is unchanged. A layout test verifies that label.click() produces an untrusted click on the associated input, a WPT expectation moves from FAIL to PASS, and SwitchInputTests.mm asserts haptic feedback fires on a real user click but not on a label-forwarded programmatic click following a user gesture.
Failure to propagate the trust bit of an underlying event across an internal event-forwarding boundary, allowing an untrusted event to be relabeled as trusted on the forwarded target.
Background
Event.isTrusted is a boolean flag that is true only when the event was created by the user agent in response to user input. Many security- and privacy-sensitive APIs (autoplay, fullscreen, clipboard, haptics) gate on this flag in addition to or instead of user activation. SimulatedClickSource is an internal WebKit enum: UserAgent causes the synthesised click to be dispatched with isTrusted = true, while Bindings causes it to be dispatched with isTrusted = false. Element::dispatchSimulatedClick(Event* underlyingEvent, ...) is the helper used to deliver a synthesised click on behalf of another event — its primary in-tree caller is the <label> element, which forwards clicks on the label to its associated form control. Element::click() (the IDL-exposed method) is a separate path that calls simulateClick directly with no underlying event and is by design always untrusted. User activation and event trust are independent signals: activation tracks recent interaction at the document level, while isTrusted tracks the provenance of an individual event object. A trusted-event check is strictly stronger than a user-activation check, because activation can persist briefly after a real gesture.
Analysis
Element::dispatchSimulatedClick hard-coded SimulatedClickSource::UserAgent for every simulated click forwarded from a label. The trust-laundering chain is: untrusted JS-initiated label.click() → HTMLLabelElement forwards the click to its associated control via dispatchSimulatedClick(underlyingEvent=<untrusted click>) → the simulated click on the <input type=checkbox switch> is dispatched with SimulatedClickSource::UserAgent → downstream consumers (notably the haptic-feedback gate from 288403@main, which requires a trusted event in addition to user activation) accept the event as trusted.
From web content: (1) place an <input type=checkbox switch> inside or associated with a <label>; (2) attach a handler to any element that receives a real user gesture; (3) inside that handler — which runs synchronously after a real gesture and therefore has fresh user activation — call document.querySelector('label').click(). The label's click() produces an untrusted click event on the label; HTMLLabelElement then calls Element::dispatchSimulatedClick on the associated input, passing the untrusted click as underlyingEvent. Before the fix, the forwarded click was relabelled isTrusted = true, satisfying the haptic-feedback trusted-event gate while activation was still live, and a UIImpactFeedbackGenerator.impactOccurred invocation fires. The new HapticFeedbackRequiresUserGestureAndTrustedEvent test encodes exactly this trigger.
This vulnerability weakened the Event.isTrusted trust boundary that distinguishes user-initiated events from script-initiated events. The general primitive is isTrusted=true on attacker-chosen click events targeting label-associated form controls; the upper bound on what an attacker gains depends on which other APIs gate on isTrusted for click events on those controls. isTrusted propagation across internal event-forwarding helpers is a recurring blind spot — the helper has both an accessibility/UA use case (e.g., spacebar activating a button) and a binding-forwarded use case where it is laundering a script-initiated event. The corrected pattern (derive the simulated source from the underlying event's trust bit, fall back to UserAgent only when there is no underlying event) is worth applying as a general rule wherever WebKit synthesises a DOM event from another event.
Audit directions
- Trust-state laundering across internal event-forwarding helpers. Audit every WebCore call site that synthesises a DOM event from another event and selects a
SimulatedClickSourceor equivalent provenance enum. Grep forSimulatedClickSource::UserAgentandsimulateClick(inSource/WebCore— any call that passesUserAgentunconditionally while also having access to anunderlyingEventis a candidate for the same bug. - Features that gate on
Event::isTrusted()for clicks on form controls reachable via<label>forwarding. Audit allisTrusted()checks in event handlers reachable fromHTMLInputElement,HTMLButtonElement,HTMLSelectElement, and verify each is robust against label-forwarded clicks (and other surviving forwarding paths). Start withgit grep -nE 'isTrusted\(\)' Source/WebCorefiltered to input/form/dialog/popover/autofocus/clipboard/haptic call sites. - Synthesised events consumed as if user-originated (accessibility action invocation, default action handlers, spatial navigation activation). Review
Element::dispatchSimulatedClickcallers and similar helpers (dispatchSimulatedMouseEvent, key-event simulations) for silentisTrustedupgrades invoked from a binding-originated path; check accessibility action paths inSource/WebCore/accessibility/that may call into these helpers. - User-activation gates intentionally paired with trusted-event gates. Audit features documented as "requires user activation" to confirm whether they also require
isTrustedand, if so, whether theisTrustedcheck is reachable only via paths that correctly propagate trust. Other recent additions touching UI permission, vibration, fullscreen, and clipboard are worth re-checking under the same lens.