← All issues

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

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

<label> forwarding을 통해 Event.isTrusted를 매번 재현 가능한 방식으로 스푸핑할 수 있는 경로를 수정하여 Medium으로 평가합니다. 직접적인 영향은 <input type=checkbox switch>의 haptic feedback trusted-event gate 우회이며, 더 넓은 영향 범위는 label 연결 컨트롤의 click에 대해 isTrusted를 조건으로 삼는 다른 기능들에 따라 달라집니다.

288403@main에서는 <input type=checkbox switch>의 haptic feedback에 user activation이 필요하도록 했습니다. 이와 함께 haptic이 trusted event에서만 발생해야 한다는 요구사항도 있었습니다. 그러나 input과 연결된 label에 click()을 호출하면 이 조건을 우회할 수 있었습니다. 근본적인 원인은 Element::dispatchSimulatedClick이 무조건 SimulatedClickSource::UserAgent를 설정하는 데 있습니다. 수정 내용은 underlying event가 존재하고 untrusted인 경우 SimulatedClickSource::Bindings를 지정하는 것입니다.

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는 이제 underlying event의 신뢰 상태를 바탕으로 SimulatedClickSource를 결정합니다. underlying event 자체가 trusted인 경우에만 UserAgent를 사용하고, 그렇지 않으면 Bindings를 사용합니다. underlying event가 없는 경우(Element::click() 자체가 사용하는 경로)에는 동작이 변경되지 않습니다. layout test는 label.click()이 연결된 input에 untrusted click을 생성하는 것을 검증하며, WPT expectation이 FAIL에서 PASS로 전환됩니다. 또한 SwitchInputTests.mm은 실제 사용자 click에서는 haptic feedback이 발생하지만, user gesture 이후의 label-forwarded programmatic click에서는 발생하지 않음을 단언합니다.

내부 event-forwarding 경계를 넘을 때 underlying event의 trust bit가 전파되지 않아, untrusted event가 forwarded target에서 trusted로 재레이블링되는 패턴.

Event.isTrusted는 user agent가 사용자 입력에 반응하여 event를 생성한 경우에만 true가 되는 boolean 플래그입니다. autoplay, fullscreen, clipboard, haptics 등 보안 및 프라이버시에 민감한 많은 API는 user activation 외에도, 또는 user activation 대신 이 플래그를 조건으로 삼습니다. SimulatedClickSource는 WebKit 내부 enum으로, UserAgent는 합성된 click을 isTrusted = true로 dispatch하고, BindingsisTrusted = false로 dispatch합니다. Element::dispatchSimulatedClick(Event* underlyingEvent, ...)는 다른 event를 대신하여 합성 click을 전달하는 helper입니다. tree 내에서의 주된 호출 지점은 <label> 요소로, label에 대한 click을 연결된 form 컨트롤로 전달합니다. Element::click()(IDL로 노출된 메서드)은 underlying event 없이 simulateClick을 직접 호출하는 별도의 경로이며, 설계 상 항상 untrusted입니다. user activation과 event trust는 독립적인 신호입니다. activation은 document 수준에서 최근 상호작용을 추적하고, isTrusted는 개별 event 객체의 출처를 추적합니다. trusted-event 검사는 user activation 검사보다 엄격한 조건입니다. activation은 실제 gesture 이후에도 잠시 유지될 수 있기 때문입니다.

Element::dispatchSimulatedClick는 label에서 전달되는 모든 simulated click에 SimulatedClickSource::UserAgent를 하드코딩했습니다. trust laundering 흐름은 다음과 같습니다. JS에서 발생한 untrusted label.click()이 시작점입니다. HTMLLabelElement는 이를 dispatchSimulatedClick(underlyingEvent=<untrusted click>) 형태로 연결된 컨트롤에 전달합니다. 이때 <input type=checkbox switch>에 대한 simulated click이 SimulatedClickSource::UserAgent로 dispatch됩니다. 결과적으로 user activation 외에 trusted event를 요구하는 288403@main의 haptic-feedback gate가 해당 event를 trusted로 수락하게 됩니다.

Web content 관점에서 공격 흐름은 다음과 같습니다. 우선 <input type=checkbox switch><label> 안에 배치하거나 label과 연결합니다. 그런 다음 실제 사용자 gesture를 받는 임의의 element에 handler를 연결합니다. 이 handler는 실제 gesture 이후 동기적으로 실행되므로 fresh user activation 상태를 갖습니다. handler 내부에서 document.querySelector('label').click()을 호출합니다. label의 click()은 label에 untrusted click event를 생성하고, HTMLLabelElement는 이 untrusted click을 underlyingEvent로 전달하면서 연결된 input에 대해 Element::dispatchSimulatedClick을 호출합니다. 패치 이전에는 forwarded click이 isTrusted = true로 재레이블링되었습니다. activation이 유효한 상태에서 haptic-feedback trusted-event gate를 통과하고, UIImpactFeedbackGenerator.impactOccurred가 실행되었습니다. 새로 추가된 HapticFeedbackRequiresUserGestureAndTrustedEvent 테스트가 바로 이 트리거 조건을 정확히 검증합니다.

이 vulnerability는 사용자가 발생시킨 event와 스크립트가 발생시킨 event를 구분하는 Event.isTrusted 신뢰 경계를 약화시켰습니다. 공격자가 확보할 수 있는 primitive는 label과 연결된 form 컨트롤을 대상으로 하는 click event에 isTrusted=true를 부여하는 것입니다. 실제로 어느 정도의 영향을 미칠 수 있는지는, 해당 컨트롤의 click event에 대해 isTrusted를 조건으로 삼는 다른 API가 무엇인지에 따라 달라집니다. 내부 event-forwarding helper 전반에서 isTrusted가 제대로 전파되는지 여부는 반복적으로 놓치기 쉬운 취약 지점입니다. 이 helper는 accessibility/UA 목적(예: spacebar로 버튼 활성화)과 스크립트가 발생시킨 event를 laundering하는 binding-forwarded 목적 모두에 사용되기 때문입니다. 수정된 패턴은 underlying event의 trust bit에서 simulated source를 결정하고, underlying event가 없을 때만 UserAgent로 fallback하는 방식입니다. WebKit이 한 event에서 다른 DOM event를 합성하는 모든 곳에 일반적인 원칙으로 적용할 가치가 있습니다.