CVE-2025-43356
Overview
Background
- WebPageProxy (UIProcess)
- The trusted UIProcess-side proxy for a web page that brokers privileged decisions such as media capture and muting on behalf of the sandboxed WebContent process.
- MediaProducerMutedStateFlags
- A bitfield describing which media kinds (e.g. audio capture, video capture) are muted for a page.
- applyWebAppDesiredMutedKinds
- A reconciliation helper that combines a requested muted state with the web app’s own desired muted capture kinds (m_mutedCaptureKindsDesiredByWebApp) to yield the authoritative muted state.
- pageMutedStateChanged
- A UIProcess-to-WebContent notification informing content processes of the page’s current muted state; it must carry the authoritative value.
- getUserMedia capture muting
- The mechanism by which active camera/microphone tracks are suppressed; if the muted state is not enforced, capture continues delivering sensor samples to the page.
Root Cause Analysis
The bug is an ordering/consistency defect in WebPageProxy::setMuted in the UIProcess, the trusted broker that decides whether camera/microphone capture is muted. setMuted receives a requested MediaProducerMutedStateFlags and must reconcile it with the web app’s own desired muted kinds via applyWebAppDesiredMutedKinds(state, m_mutedCaptureKindsDesiredByWebApp) to produce the authoritative newState that is then pushed to the WebContent process(es). In the vulnerable version the reconciliation was performed too late: the function first stored internals().mutedState = state (the raw, un-reconciled request) and first notified every web content process with process.pageMutedStateChanged(pageID, state) using the raw state, and only AFTER those side effects computed newState = applyWebAppDesiredMutedKinds(...). Consequently the UIProcess’s cached muted state and the pageMutedStateChanged notification reflected state rather than the web-app-adjusted newState, while the later async Messages::WebPage::SetMuted used newState — an internal inconsistency. The violated invariant is that all consumers of the page’s muted state (the cached internals().mutedState and every content-process notification) must observe the single authoritative, web-app-reconciled muted state; here two different values were propagated. In practice this meant capture could remain effectively unmuted/active when the reconciled policy should have muted it, allowing a website to access camera/microphone (sensor) data without proper consent — matching the advisory ‘a website may be able to access sensor information without user consent.’
The fix computes newState (via applyWebAppDesiredMutedKinds under ENABLE(MEDIA_STREAM), else newState = state) BEFORE any side effects, stores internals().mutedState = newState, and sends pageMutedStateChanged(pageID, newState), so the cache, the state-changed notification, and the SetMuted message all carry the same reconciled value. The added API test GetUserMediaAfterMuting and the startAudioCapture/startVideoCapture test helpers exercise muting camera and microphone and then re-requesting capture, asserting prompt counts, which validates that muting is applied consistently.
Attack Path
- Obtain capture access A website requests camera/microphone via navigator.mediaDevices.getUserMedia and the user grants it, establishing active capture tracks.
- Induce a setMuted transition A mute state change is triggered (e.g. app/embedder mutes capture, or a state transition flows through WebPageProxy::setMuted) so the reconciliation between requested state and the web app’s desired muted kinds is exercised.
- Exploit the stale propagation
Because pre-patch the UIProcess cached and notified content processes with the raw
stateinstead of the web-app-reconcilednewState, the muted policy is not authoritatively enforced and capture can remain active/unmuted contrary to the intended muted state. - Access sensor data without consent With capture effectively left unmuted, the website continues to receive camera/microphone samples that should have been suppressed, obtaining sensor information without proper user consent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
WebPageProxy::setMutedSource/WebKit/UIProcess/WebPageProxy.cpp |
modified | Computes the web-app-reconciled newState before any side effects, and now stores internals().mutedState = newState and calls pageMutedStateChanged with newState, so the cache and all content-process notifications carry the single authoritative reconciled muted state instead of the raw request. |
GetUserMediaAfterMuting (API test)Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm |
added | New test that starts audio and video capture, mutes camera and microphone, re-requests capture, and asserts capture-state transitions and prompt counts to verify muting is consistently applied. |
startAudioCapture / startVideoCapture (test helpers)Tools/TestWebKitAPI/Tests/WebKitCocoa/media-session-capture.html |
added | JS helpers that request audio-only and video-only getUserMedia and report PASS/FAIL, used by the new test to drive per-kind capture. |
Files Changed
Source/WebKit/UIProcess/WebPageProxy.cppTools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mmTools/TestWebKitAPI/Tests/WebKitCocoa/media-session-capture.html
Audit Directions
- Ordering of state computation vs. propagation in setMutedIn WebPageProxy.cpp, verify every consumer of muted state uses the reconciled value; grep for
internals().mutedState,pageMutedStateChanged, andapplyWebAppDesiredMutedKindsto confirm the reconciled newState is computed before any assignment or notification. - Other capture/permission state settersAudit sibling setters that reconcile a requested value with app/user policy then notify processes; grep in UIProcess for
forEachWebContentProcess,SetMuted, andMediaProducerMutedStateFlagsto find places that might notify with the pre-reconciliation value. - Raw-vs-derived value drift patternsLook across UIProcess for functions that store or send a parameter and only later transform it; grep for a computed
newState/finalStatelocal used in one message but where the originalstateis used in an earlier cache write or notification within the same function.