← 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.

🔒

Walks through how a script-initiated event can cross an internal forwarding boundary inside WebCore and emerge with elevated trust — and what realistic policy gates that lets a page bypass from ordinary web content.

Subscribe to read more

🔒

Several reusable audit patterns for event-trust laundering across WebKit's synthesized-event helpers, with concrete grep targets and subsystems to start with.

Subscribe to read more