← All issues

[7] label-forwarded clicks promoted to isTrusted=true via dispatchSimulatedClick

Severity: Medium | Component: WebCore DOM event dispatch | fc1ef83

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

bool Element::dispatchSimulatedClick(Event* underlyingEvent, SimulatedClickMouseEventOptions eventOptions, SimulatedClickVisualOptions visualOptions)
{
- return simulateClick(*this, underlyingEvent, eventOptions, visualOptions, SimulatedClickSource::UserAgent);
+ auto simulatedClickSource = [&] {
+ if (!underlyingEvent)
+ return SimulatedClickSource::UserAgent;
+
+ return underlyingEvent->isTrusted() ? SimulatedClickSource::UserAgent : SimulatedClickSource::Bindings;
+ }();
+
+ return simulateClick(*this, underlyingEvent, eventOptions, visualOptions, simulatedClickSource);
}

LayoutTests/fast/forms/label/label-click-event-dispatch-untrusted.html

+<input id="input" type="checkbox">
+<label id="label" for="input"></label>
+<script>
+input.addEventListener("click", (event) => {
+ shouldBeFalse("event.isTrusted");
+});
+label.click();
+</script>

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.

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.

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.