CVE-2026-43721
Overview
Background
- Storage Access API
- A Web API (document.requestStorageAccess()) by which an embedded third-party frame asks for access to its first-party cookies/storage, subject to a permission decision that may or may not show a prompt.
- Transient user activation
- A short-lived per-frame state set by a genuine user interaction that gates powerful APIs (clipboard, popups, autoplay) to prevent abuse by unattended script.
- StorageAccessPromptWasShown
- A result flag indicating whether the storage-access decision involved a user-facing prompt; a No value can mean the request was auto-decided without any interaction.
- Prevalent resource
- In WebKit’s Intelligent Tracking Prevention, a domain classified as a cross-site tracker whose storage access is restricted unless prior user interaction exists.
- Completion handler capture
- The async lambda that runs when the cross-process storage-access decision returns; state it needs (like the original gesture status) must be captured at call time to be available later.
Root Cause Analysis
The vulnerable path is DocumentStorageAccess::requestStorageAccess in Source/WebCore/dom/DocumentStorageAccess.cpp. When the Storage Access API request completed with StorageAccessWasGranted::No, the completion handler decided whether to keep (preserve) the calling frame’s transient user activation with the single test ‘shouldPreserveUserGesture = result.promptWasShown == StorageAccessPromptWasShown::No’. The intended invariant is that user activation may only be preserved/consumed across the async round-trip when the caller actually had a user gesture (or the request legitimately did not require one). But the old test keyed only on whether a UI prompt was shown, ignoring whether the original call carried a gesture. A no-prompt, no-gesture rejection therefore left the frame with transient user activation it never legitimately earned. The diff shows the request now captures the gesture state at call time in a renamed enum value ‘hasUserGestureOrNoUserGestureRequired’ (previously the ambiguous ‘HasOrShouldIgnoreUserGesture’), and that value is captured into the completion lambda.
The fix replaces the test with ‘promptNotShownButMayHaveUserGesture = result.promptWasShown == StorageAccessPromptWasShown::No && hasUserGestureOrNoUserGestureRequired == HasUserGestureOrNoUserGestureRequired::Yes’, so activation is only preserved when a gesture (or no-gesture-required condition) was actually present. The enum rename is threaded end-to-end through ChromeClient, WebChromeClient, WebPage, the NetworkConnectionToWebProcess IPC message, WebResourceLoadStatisticsStore, the serialization list and messages.py, but those are mechanical renames; the behavioral fix is the added conjunct. The new layout test confirms the intended behavior: an iframe calls document.requestStorageAccess() with no user gesture, the request is rejected, and in the rejection handler window.open must be blocked (no synthesized activation). Per the CVE text the consequence is that spuriously preserved activation lets a page perform gesture-gated actions such as writing the clipboard without a real user interaction, hence ‘silently hijack clipboard data’.
Attack Path
- Set up a rejectable request The attacker page (or an embedded third-party iframe) is a prevalent resource with no prior user interaction, so a Storage Access API request will be denied and, critically, no permission prompt will be shown.
- Call requestStorageAccess without a gesture Script calls document.requestStorageAccess() outside any user-gesture context (e.g. from a timer or onload), so hasUserGesture is No.
- Receive a no-prompt rejection The network process returns StorageAccessWasGranted::No with StorageAccessPromptWasShown::No; the pre-patch completion handler sets shouldPreserveUserGesture = true purely because no prompt was shown.
- Inherit transient activation in the rejection handler The promise’s rejection callback now runs with a preserved/synthesized transient user activation that the frame never earned from a real interaction.
- Invoke a gesture-gated API Within that handler the page performs an action normally requiring activation — e.g. navigator.clipboard writes/reads or window.open — silently, without the user having clicked anything, achieving the clipboard-hijack impact described.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
DocumentStorageAccess::requestStorageAccessSource/WebCore/dom/DocumentStorageAccess.cpp |
modified | Renames the gesture flag, captures it into the completion lambda, and gates shouldPreserveUserGesture on both promptWasShown==No AND a real/na gesture; this is the core fix. |
DocumentStorageAccess::requestStorageAccessQuirkSource/WebCore/dom/DocumentStorageAccess.cpp |
modified | Passes the renamed enum value HasUserGestureOrNoUserGestureRequired::Yes; mechanical rename only. |
WebResourceLoadStatisticsStore::requestStorageAccessSource/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp |
modified | Signature/parameter renamed and the No-branch check updated to the new enum name; behavior unchanged. |
NetworkConnectionToWebProcess::requestStorageAccessSource/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp |
modified | Forwards the renamed enum to resourceLoadStatistics; mechanical rename. |
WebChromeClient::requestStorageAccessSource/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp |
modified | Renamed parameter forwarded to WebPage; mechanical rename. |
WebPage::requestStorageAccessSource/WebKit/WebProcess/WebPage/WebPage.cpp |
modified | Sends the renamed enum over the RequestStorageAccess IPC message; mechanical rename. |
ChromeClient::requestStorageAccess (declaration)Source/WebCore/page/ChromeClient.h |
modified | Virtual signature updated to the renamed enum type. |
HasOrShouldIgnoreUserGesture -> HasUserGestureOrNoUserGestureRequired (enum)Source/WebCore/dom/DocumentStorageAccess.h |
modified | Enum renamed to make its true meaning (caller had a gesture / none required) explicit rather than 'ignore gesture'. |
Files Changed
LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txtLayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.htmlLayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.htmlSource/WebCore/dom/DocumentStorageAccess.cppSource/WebCore/dom/DocumentStorageAccess.hSource/WebCore/page/ChromeClient.hSource/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cppSource/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.hSource/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cppSource/WebKit/NetworkProcess/NetworkConnectionToWebProcess.hSource/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.inSource/WebKit/Scripts/webkit/messages.pySource/WebKit/Shared/WebCoreArgumentCoders.serialization.inSource/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cppSource/WebKit/WebProcess/WebCoreSupport/WebChromeClient.hSource/WebKit/WebProcess/WebPage/WebPage.cppSource/WebKit/WebProcess/WebPage/WebPage.h
Audit Directions
- Other branches of the same completion handlerIn DocumentStorageAccess.cpp re-check every assignment to shouldPreserveUserGesture (the StorageAccessWasGranted::Yes/YesWithException cases) to confirm none preserve activation without a genuine gesture; grep for ‘shouldPreserveUserGesture’ and ‘promptWasShown’.
- Callers of the renamed enumAudit all sites still passing HasUserGestureOrNoUserGestureRequired::Yes unconditionally (e.g. requestStorageAccessQuirk) to ensure ‘Yes’ is justified and not masking a missing gesture check; grep ‘HasUserGestureOrNoUserGestureRequired::Yes’.
- Gesture-gated APIs reachable from async rejection handlersLook for promise rejection/resolution paths that resume with preserved activation and reach powerful APIs; grep for ‘UserGestureIndicator’, ‘processingUserGesture’, and clipboard/window.open entry points to find similar activation-preservation logic.
- Prompt-vs-gesture conflation across permission flowsSearch other permission subsystems (getUserMedia, notifications, geolocation) for decisions keyed on whether a prompt was shown rather than on actual user activation; grep ‘PromptWasShown’ and ‘wasGranted’ patterns.