fc1ef83eae Haptic feedback for <input type=checkbox switch> can be triggered with a programmatic click on an associated label
Triage note: Fixes a trusted-event bypass where a scripted click on a label produced a UserAgent-sourced simulated click gating privileged behavior.
Contents
The bug at a glance
This is a trusted-event bypass: an untrusted (script-initiated) click on a <label> was forwarded to the associated form control as a trusted event (isTrusted === true), letting script forge user activation/trust on the control. Trusted-event gating protects sensitive behaviors — here haptic feedback for <input type=checkbox switch>, but the same isTrusted forwarding governs other activation-gated actions — so the bypass has cross-cutting security relevance. The concrete impact in the commit is limited (spoofing trust to trigger haptics after a real gesture), which keeps it below memory-corruption severity, but forging isTrusted on a form control is a real integrity boundary violation.
Element::dispatchSimulatedClick unconditionally passed SimulatedClickSource::UserAgent, so the simulated click it dispatches on the form control is always trusted — even when the label click that triggered it was itself an untrusted, script-generated event via label.click(). The fix makes the simulated click inherit trust from the underlying event: UserAgent only when there is no underlying event or it is trusted, otherwise SimulatedClickSource::Bindings (untrusted).
Root cause
When a <label> is clicked, WebKit forwards the activation to the label’s associated form control by synthesizing a click on that control. This forwarding goes through Element::dispatchSimulatedClick(underlyingEvent, …), which delegates to simulateClick(…) with a SimulatedClickSource. That source determines the isTrusted bit of the dispatched event: SimulatedClickSource::UserAgent produces a trusted event (isTrusted === true), while SimulatedClickSource::Bindings produces an untrusted one (isTrusted === false), matching the DOM semantics that only user-agent-originated events are trusted and script-originated ones are not.
The bug: dispatchSimulatedClick always passed SimulatedClickSource::UserAgent, regardless of whether the click being forwarded originated from a real user gesture or from script. So calling label.click() — a scripted, untrusted click on the label — caused the forwarded click on the associated control to arrive as a trusted event. Commit 288403@main had gated haptic feedback for <input type=checkbox switch> behind user activation, and additionally the intent was to require a trusted event. But because label-forwarded clicks became trusted on the control, script could, immediately after a genuine user gesture (which supplies the transient activation), call label.click() and satisfy both the activation and the trusted-event checks, firing haptics programmatically — the exact bypass the commit closes.
The fix rewrites dispatchSimulatedClick to compute the source from the underlying event’s trust. A lambda returns SimulatedClickSource::UserAgent when there is no underlyingEvent (preserving accessibility/user-agent-driven simulated clicks that legitimately have no originating event), and otherwise returns underlyingEvent->isTrusted() ? SimulatedClickSource::UserAgent : SimulatedClickSource::Bindings. Thus a forwarded click inherits the trust of the event that caused it: a real user click on the label (trusted) still yields a trusted click on the control, while a scripted label.click() (untrusted) now yields an untrusted click on the control (isTrusted === false).
The commit is explicit that this change is deliberately confined to dispatchSimulatedClick and not applied to simulateClick itself, because simulateClick is also used by Element::click(), which has no underlying event and is always untrusted — so the no-underlying-event branch must keep returning UserAgent to preserve existing semantics there. The layout test asserts event.isTrusted is false on the control’s click handler after label.click(), and the imported WPT expectation flips from FAIL (forwarded click was trusted) to PASS.
Key code
Element::dispatchSimulatedClick — inherit trust from the underlying event
bool Element::dispatchSimulatedClick(Event* underlyingEvent, SimulatedClickMouseEventOptions eventOptions, SimulatedClickVisualOptions visualOptions)
{
auto simulatedClickSource = [&] {
if (!underlyingEvent)
return SimulatedClickSource::UserAgent;
return underlyingEvent->isTrusted() ? SimulatedClickSource::UserAgent : SimulatedClickSource::Bindings;
}();
return simulateClick(*this, underlyingEvent, eventOptions, visualOptions, simulatedClickSource);
}
Patch walkthrough
Source/WebCore/dom/Element.cpp— Element::dispatchSimulatedClick replaces the unconditional SimulatedClickSource::UserAgent with a computed source: a lambda returns UserAgent when underlyingEvent is null, else UserAgent if underlyingEvent->isTrusted() and Bindings otherwise. The result is passed to simulateClick. This makes a label-forwarded click untrusted whenever the underlying label click was untrusted (e.g. label.click()).LayoutTests/fast/forms/label/label-click-event-dispatch-untrusted.html— New test: a <label for=input> and a checkbox; the input’s click listener asserts event.isTrusted is false after calling label.click(). Verifies the forwarded click is now untrusted.LayoutTests/imported/w3c/web-platform-tests/html/semantics/forms/the-label-element/label-forwarded-click-pointer-properties-expected.txt— WPT expectation updated: ‘Click forwarded from label.click() is not trusted’ flips from FAIL (got true) to PASS, confirming alignment with the standard that scripted forwarded clicks are untrusted.Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SwitchInputTests.mm— API tests reworked: HapticFeedbackOnClick and a new HapticFeedbackRequiresUserGestureAndTrustedEvent replace the old HapticFeedbackRequiresUserGesture, asserting haptics for the switch require both a user gesture and a trusted event, so a post-gesture label.click() no longer triggers them.
Background
isTrusted and trusted events — The DOM isTrusted attribute is true only for events dispatched by the user agent in response to genuine user interaction, and false for events created or dispatched by script (e.g. dispatchEvent, click()). Security-sensitive behaviors gate on isTrusted to distinguish real user intent from scripted simulation.
Label-to-control click forwarding — HTML specifies that activating a <label> associated with a form control forwards the activation to that control. WebKit implements this by synthesizing a click on the control via Element::dispatchSimulatedClick, passing the label’s click as the underlyingEvent.
SimulatedClickSource UserAgent vs Bindings — simulateClick takes a SimulatedClickSource that sets the synthesized event’s trust: UserAgent yields a trusted event, Bindings yields an untrusted one. dispatchSimulatedClick previously hardcoded UserAgent, so every forwarded click was trusted regardless of origin.
Element::click() vs dispatchSimulatedClick — Element::click() (the scripted API) also routes through simulateClick but has no underlying event and must always be untrusted; the fix therefore keeps the no-underlying-event branch returning UserAgent semantics only within dispatchSimulatedClick and leaves simulateClick unchanged, so click()’s existing untrusted behavior is preserved.
User activation vs trusted-event gating — Transient user activation (a recent real gesture) and event trust are distinct gates. Commit 288403@main required activation for switch haptics; this fix adds the trusted-event requirement. Because a scripted label.click() right after a real gesture satisfied activation and (bug) produced a trusted control click, both gates were bypassable until forwarded-click trust was fixed.
Vulnerability window
- Prior hardening — 288403@main gates <input type=checkbox switch> haptic feedback behind user activation, intending also to require a trusted event.
- Latent flaw — Element::dispatchSimulatedClick always uses SimulatedClickSource::UserAgent, so label-forwarded clicks are trusted on the control even when the label click was scripted.
- Bypass — Script performs label.click() immediately after a genuine user gesture; the control receives a trusted click with activation still live, triggering haptics programmatically.
- Discovery — Recognized that untrusted label clicks become trusted control clicks, violating the trusted-event requirement (WPT expectation was FAIL).
- Fix — dispatchSimulatedClick derives the source from underlyingEvent->isTrusted(), forwarding untrusted clicks as SimulatedClickSource::Bindings; tests assert isTrusted === false and WPT flips to PASS.
Proof of concept
Verbatim core of the added LayoutTest label-click-event-dispatch-untrusted.html. Calling label.click() (an untrusted, scripted click) forwards a click to the associated checkbox; the control’s listener checks event.isTrusted. Pre-patch this was true (bug); post-patch it is false. For the switch-haptics variant, calling label.click() shortly after a real user gesture would fire haptics pre-patch because the forwarded click was trusted and activation was still live.
// <input id="input" type="checkbox">
// <label id="label" for="input"></label>
input.addEventListener("click", (event) => {
shouldBeFalse("event.isTrusted");
});
label.click();
Exploitation
- Setup — Place a <label> associated with a target form control (e.g. <input type=checkbox switch>) and wire script to call label.click() after obtaining transient activation from a genuine user gesture.
- Forge trust — Pre-patch, label.click() delivers a trusted (isTrusted === true) click to the control, spoofing user origin and satisfying trusted-event gates such as switch haptic feedback.
- Impact — Programmatic triggering of activation/trust-gated behavior on the control without the user directly interacting with it. In the demonstrated case this is haptic feedback; the general concern is any behavior keyed on a control receiving a trusted click.
- Reality check — No memory corruption; this is an event-integrity/trust bypass. It requires prior user activation for the haptics case, so it is an escalation of trust within an already-active gesture window rather than fully unattended abuse.
Detection & hunting
For defenders and SOC / detection engineers:
- Scripted label.click patterns —
- isTrusted anomalies —
- Version gating —
Audit directions
- Simulated-click callers —
- isTrusted-gated behaviors —
- Accessibility simulated clicks —
- Cross-element activation forwarding —