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 |
|---|---|---|
ifSource/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp |
modified | |
ifSource/WebKit/WebProcess/WebPage/WebPage.cpp |
modified | |
switchSource/WebKit/WebProcess/WebPage/WebPage.cpp |
modified | |
ifLayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html |
modified | |
setEnableFeatureLayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html |
modified | |
functionLayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html |
modified | |
ifLayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html |
modified |
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.
Patch
diff --git a/Source/WebCore/dom/DocumentStorageAccess.cpp b/Source/WebCore/dom/DocumentStorageAccess.cpp
index 3ba8a7e499a5..54adf96d04c6 100644
--- a/Source/WebCore/dom/DocumentStorageAccess.cpp
+++ b/Source/WebCore/dom/DocumentStorageAccess.cpp
@@ -238,8 +238,8 @@ void DocumentStorageAccess::requestStorageAccess(Ref<DeferredPromise>&& promise)
if (!page->settings().storageAccessAPIPerPageScopeEnabled())
m_storageAccessScope = StorageAccessScope::PerFrame;
- auto hasOrShouldIgnoreUserGesture = frame->requestSkipUserActivationCheckForStorageAccess(RegistrableDomain { document->url() }) || UserGestureIndicator::processingUserGesture() ? HasOrShouldIgnoreUserGesture::Yes : HasOrShouldIgnoreUserGesture::No;
- page->chrome().client().requestStorageAccess(RegistrableDomain::uncheckedCreateFromHost(protect(document->securityOrigin())->host()), RegistrableDomain::uncheckedCreateFromHost(protect(document->topOrigin())->host()), *frame, m_storageAccessScope, hasOrShouldIgnoreUserGesture, [weakThis = WeakPtr { *this }, promise = WTF::move(promise)] (RequestStorageAccessResult result) mutable {
+ auto hasUserGestureOrNoUserGestureRequired = frame->requestSkipUserActivationCheckForStorageAccess(RegistrableDomain { document->url() }) || UserGestureIndicator::processingUserGesture() ? HasUserGestureOrNoUserGestureRequired::Yes : HasUserGestureOrNoUserGestureRequired::No;
+ page->chrome().client().requestStorageAccess(RegistrableDomain::uncheckedCreateFromHost(protect(document->securityOrigin())->host()), RegistrableDomain::uncheckedCreateFromHost(protect(document->topOrigin())->host()), *frame, m_storageAccessScope, hasUserGestureOrNoUserGestureRequired, [weakThis = WeakPtr { *this }, promise = WTF::move(promise), hasUserGestureOrNoUserGestureRequired] (RequestStorageAccessResult result) mutable {
RefPtr protectedThis = weakThis.get();
if (!protectedThis)
return;
@@ -252,7 +252,8 @@ void DocumentStorageAccess::requestStorageAccess(Ref<DeferredPromise>&& promise)
shouldPreserveUserGesture = true;
break;
case StorageAccessWasGranted::No:
- shouldPreserveUserGesture = result.promptWasShown == StorageAccessPromptWasShown::No;
+ const bool promptNotShownButMayHaveUserGesture = result.promptWasShown == StorageAccessPromptWasShown::No && hasUserGestureOrNoUserGestureRequired == HasUserGestureOrNoUserGestureRequired::Yes;
+ shouldPreserveUserGesture = promptNotShownButMayHaveUserGesture;
}
Ref document = protectedThis->m_document.get();
@@ -349,7 +350,7 @@ void DocumentStorageAccess::requestStorageAccessQuirk(RegistrableDomain&& reques
auto topFrameDomain = RegistrableDomain(page->mainFrameURL());
RefPtr frame = document->frame();
- page->chrome().client().requestStorageAccess(WTF::move(requestingDomain), WTF::move(topFrameDomain), *frame, m_storageAccessScope, HasOrShouldIgnoreUserGesture::Yes, [weakThis = WeakPtr { *this }, completionHandler = WTF::move(completionHandler)] (RequestStorageAccessResult result) mutable {
+ page->chrome().client().requestStorageAccess(WTF::move(requestingDomain), WTF::move(topFrameDomain), *frame, m_storageAccessScope, HasUserGestureOrNoUserGestureRequired::Yes, [weakThis = WeakPtr { *this }, completionHandler = WTF::move(completionHandler)] (RequestStorageAccessResult result) mutable {
RefPtr protectedThis = weakThis.get();
if (!protectedThis)
return;
diff --git a/Source/WebCore/dom/DocumentStorageAccess.h b/Source/WebCore/dom/DocumentStorageAccess.h
index 65417557af76..6b46f8105710 100644
--- a/Source/WebCore/dom/DocumentStorageAccess.h
+++ b/Source/WebCore/dom/DocumentStorageAccess.h
@@ -41,7 +41,7 @@ enum class StorageAccessWasGranted : uint8_t { No, Yes, YesWithException };
enum class StorageAccessPromptWasShown : bool { No, Yes };
-enum class HasOrShouldIgnoreUserGesture : bool { No, Yes };
+enum class HasUserGestureOrNoUserGestureRequired : bool { No, Yes };
enum class StorageAccessScope : bool {
PerFrame,
diff --git a/Source/WebCore/page/ChromeClient.h b/Source/WebCore/page/ChromeClient.h
index ed67709d049b..8bd12ba1ab6a 100644
--- a/Source/WebCore/page/ChromeClient.h
+++ b/Source/WebCore/page/ChromeClient.h
@@ -683,7 +683,7 @@ public:
virtual RefPtr<Icon> createIconForFiles(const Vector<String>& /* filenames */) = 0;
virtual void hasStorageAccess(RegistrableDomain&& /*subFrameDomain*/, RegistrableDomain&& /*topFrameDomain*/, LocalFrame&, CompletionHandler<void(bool)>&& completionHandler) { completionHandler(false); }
- virtual void requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, LocalFrame&, StorageAccessScope scope, HasOrShouldIgnoreUserGesture, CompletionHandler<void(RequestStorageAccessResult)>&& completionHandler) { completionHandler({ StorageAccessWasGranted::No, StorageAccessPromptWasShown::No, scope, WTF::move(topFrameDomain), WTF::move(subFrameDomain) }); }
+ virtual void requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, LocalFrame&, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired, CompletionHandler<void(RequestStorageAccessResult)>&& completionHandler) { completionHandler({ StorageAccessWasGranted::No, StorageAccessPromptWasShown::No, scope, WTF::move(topFrameDomain), WTF::move(subFrameDomain) }); }
virtual bool hasPageLevelStorageAccess(const RegistrableDomain& /*topLevelDomain*/, const RegistrableDomain& /*resourceDomain*/) const { return false; }
virtual void setLoginStatus(RegistrableDomain&&, IsLoggedIn, CompletionHandler<void()>&&) { }
diff --git a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
index df275bb0c5ac..96157f4ae9f9 100644
--- a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
+++ b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
@@ -416,7 +416,7 @@ void WebResourceLoadStatisticsStore::callHasStorageAccessForFrameHandler(const R
callback(false);
}
-void WebResourceLoadStatisticsStore::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, FrameIdentifier frameID, PageIdentifier webPageID, WebPageProxyIdentifier webPageProxyID, StorageAccessScope scope, HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture, CompletionHandler<void(RequestStorageAccessResult)>&& completionHandler)
+void WebResourceLoadStatisticsStore::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, FrameIdentifier frameID, PageIdentifier webPageID, WebPageProxyIdentifier webPageProxyID, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired, CompletionHandler<void(RequestStorageAccessResult)>&& completionHandler)
{
ASSERT(RunLoop::isMain());
@@ -425,7 +425,7 @@ void WebResourceLoadStatisticsStore::requestStorageAccess(RegistrableDomain&& su
return;
}
- if (hasOrShouldIgnoreUserGesture == HasOrShouldIgnoreUserGesture::No) {
+ if (hasUserGestureOrNoUserGestureRequired == HasUserGestureOrNoUserGestureRequired::No) {
auto it = m_domainsGrantedStorageAccessPermissionInPage.find(webPageProxyID);
if (it == m_domainsGrantedStorageAccessPermissionInPage.end() || !it->value.contains({ topFrameDomain, subFrameDomain }))
return completionHandler({ StorageAccessWasGranted::No, StorageAccessPromptWasShown::No, scope, topFrameDomain, subFrameDomain });
diff --git a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
index 27144ad291df..f8622ac1885f 100644
--- a/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
+++ b/Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
@@ -149,7 +149,7 @@ public:
void hasHadUserInteraction(RegistrableDomain&&, CompletionHandler<void(bool)>&&);
void hasStorageAccess(SubFrameDomain&&, TopFrameDomain&&, std::optional<WebCore::FrameIdentifier>, WebCore::PageIdentifier, CompletionHandler<void(bool)>&&);
bool hasStorageAccessForFrame(const SubFrameDomain&, const TopFrameDomain&, WebCore::FrameIdentifier, WebCore::PageIdentifier);
- void requestStorageAccess(SubFrameDomain&&, TopFrameDomain&&, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, StorageAccessScope, WebCore::HasOrShouldIgnoreUserGesture, CompletionHandler<void(RequestStorageAccessResult)>&&);
+ void requestStorageAccess(SubFrameDomain&&, TopFrameDomain&&, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, StorageAccessScope, WebCore::HasUserGestureOrNoUserGestureRequired, CompletionHandler<void(RequestStorageAccessResult)>&&);
void queryStorageAccessPermission(SubFrameDomain&&, TopFrameDomain&&, std::optional<WebPageProxyIdentifier>, CompletionHandler<void(WebCore::PermissionState)>&&);
void setLoginStatus(RegistrableDomain&&, IsLoggedIn, std::optional<LoginStatus>&&, CompletionHandler<void()>&&);
void isLoggedIn(RegistrableDomain&&, CompletionHandler<void(bool)>&&);
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
index 17eb67e55342..981445834cec 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
@@ -1376,11 +1376,11 @@ void NetworkConnectionToWebProcess::hasStorageAccess(RegistrableDomain&& subFram
completionHandler(false);
}
-void NetworkConnectionToWebProcess::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, FrameIdentifier frameID, PageIdentifier webPageID, WebPageProxyIdentifier webPageProxyID, StorageAccessScope scope, HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture, CompletionHandler<void(WebCore::RequestStorageAccessResult result)>&& completionHandler)
+void NetworkConnectionToWebProcess::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, FrameIdentifier frameID, PageIdentifier webPageID, WebPageProxyIdentifier webPageProxyID, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired, CompletionHandler<void(WebCore::RequestStorageAccessResult result)>&& completionHandler)
{
if (CheckedPtr networkSession = this->networkSession()) {
if (RefPtr resourceLoadStatistics = networkSession->resourceLoadStatistics()) {
- resourceLoadStatistics->requestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frameID, webPageID, webPageProxyID, scope, hasOrShouldIgnoreUserGesture, WTF::move(completionHandler));
+ resourceLoadStatistics->requestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frameID, webPageID, webPageProxyID, scope, hasUserGestureOrNoUserGestureRequired, WTF::move(completionHandler));
return;
}
}
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
index 20ab2d499c49..fda15c3bcac9 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
@@ -407,7 +407,7 @@ private:
void logUserInteraction(RegistrableDomain&&);
void resourceLoadStatisticsUpdated(Vector<WebCore::ResourceLoadStatistics>&&, CompletionHandler<void()>&&);
void hasStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebCore::FrameIdentifier, WebCore::PageIdentifier, CompletionHandler<void(bool)>&&);
- void requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, WebCore::StorageAccessScope, WebCore::HasOrShouldIgnoreUserGesture, CompletionHandler<void(WebCore::RequestStorageAccessResult)>&&);
+ void requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebCore::FrameIdentifier, WebCore::PageIdentifier, WebPageProxyIdentifier, WebCore::StorageAccessScope, WebCore::HasUserGestureOrNoUserGestureRequired, CompletionHandler<void(WebCore::RequestStorageAccessResult)>&&);
void queryStorageAccessPermission(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, std::optional<WebPageProxyIdentifier>, CompletionHandler<void(WebCore::PermissionState)>&&);
void storageAccessQuirkForTopFrameDomain(URL&& topFrameURL, CompletionHandler<void(Vector<RegistrableDomain>)>&&);
void requestStorageAccessUnderOpener(WebCore::RegistrableDomain&& domainInNeedOfStorageAccess, WebCore::PageIdentifier openerPageID, WebCore::RegistrableDomain&& openerDomain);
diff --git a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
index 8228094b0fab..cc292e637bc5 100644
--- a/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
+++ b/Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
@@ -89,7 +89,7 @@ messages -> NetworkConnectionToWebProcess WantsDispatchMessage {
LogUserInteraction(WebCore::RegistrableDomain domain)
ResourceLoadStatisticsUpdated(Vector<WebCore::ResourceLoadStatistics> statistics) -> ()
[EnabledBy=StorageAccessAPIEnabled] HasStorageAccess(WebCore::RegistrableDomain subFrameDomain, WebCore::RegistrableDomain topFrameDomain, WebCore::FrameIdentifier frameID, WebCore::PageIdentifier pageID) -> (bool hasStorageAccess)
- [EnabledBy=StorageAccessAPIEnabled] RequestStorageAccess(WebCore::RegistrableDomain subFrameDomain, WebCore::RegistrableDomain topFrameDomain, WebCore::FrameIdentifier frameID, WebCore::PageIdentifier webPageID, WebKit::WebPageProxyIdentifier webPageProxyID, enum:bool WebCore::StorageAccessScope scope, enum:bool WebCore::HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture) -> (struct WebCore::RequestStorageAccessResult result)
+ [EnabledBy=StorageAccessAPIEnabled] RequestStorageAccess(WebCore::RegistrableDomain subFrameDomain, WebCore::RegistrableDomain topFrameDomain, WebCore::FrameIdentifier frameID, WebCore::PageIdentifier webPageID, WebKit::WebPageProxyIdentifier webPageProxyID, enum:bool WebCore::StorageAccessScope scope, enum:bool WebCore::HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired) -> (struct WebCore::RequestStorageAccessResult result)
[EnabledBy=StorageAccessAPIEnabled] QueryStorageAccessPermission(WebCore::RegistrableDomain subFrameDomain, WebCore::RegistrableDomain topFrameDomain, std::optional<WebKit::WebPageProxyIdentifier> webPageProxyID) -> (enum:uint8_t WebCore::PermissionState permissionState)
[EnabledBy=StorageAccessAPIEnabled] StorageAccessQuirkForTopFrameDomain(URL topFrameURL) -> (Vector<WebCore::RegistrableDomain> domains)
[EnabledBy=StorageAccessAPIEnabled] RequestStorageAccessUnderOpener(WebCore::RegistrableDomain domainInNeedOfStorageAccess, WebCore::PageIdentifier openerPageID, WebCore::RegistrableDomain openerDomain)
diff --git a/Source/WebKit/Scripts/webkit/messages.py b/Source/WebKit/Scripts/webkit/messages.py
index bc1b7244a963..3d997b234e25 100644
--- a/Source/WebKit/Scripts/webkit/messages.py
+++ b/Source/WebKit/Scripts/webkit/messages.py
@@ -1278,7 +1278,7 @@ def headers_for_type(type, for_implementation_file=False):
'WebCore::GraphicsLayerKeyframeValueList': ['<WebCore/GraphicsLayerKeyframeValueList.h>'],
'WebCore::HasAvailableTargets': ['<WebCore/MediaSessionHelperIOS.h>'],
'WebCore::HasInsecureContent': ['<WebCore/FrameLoaderTypes.h>'],
- 'WebCore::HasOrShouldIgnoreUserGesture': ['<WebCore/DocumentStorageAccess.h>'],
+ 'WebCore::HasUserGestureOrNoUserGestureRequired': ['<WebCore/DocumentStorageAccess.h>'],
'WebCore::Headroom': ['<WebCore/ImageTypes.h>'],
'WebCore::HighlightRequestOriginatedInApp': ['<WebCore/AppHighlight.h>'],
'WebCore::HighlightVisibility': ['<WebCore/HighlightVisibility.h>'],
diff --git a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in
index 50c3e74ecfbe..b58ffa50d2b2 100644
--- a/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in
+++ b/Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in
@@ -2267,7 +2267,7 @@ enum class WebCore::StorageAccessWasGranted : uint8_t {
enum class WebCore::StorageAccessPromptWasShown : bool
-enum class WebCore::HasOrShouldIgnoreUserGesture : bool
+enum class WebCore::HasUserGestureOrNoUserGestureRequired : bool
enum class WebCore::StorageAccessScope : bool
diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp
index 1d1af9ff7dd6..a6f1b13095b9 100644
--- a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp
+++ b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp
@@ -2074,12 +2074,12 @@ void WebChromeClient::hasStorageAccess(RegistrableDomain&& subFrameDomain, Regis
completionHandler(false);
}
-void WebChromeClient::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, LocalFrame& frame, StorageAccessScope scope, HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture, CompletionHandler<void(RequestStorageAccessResult)>&& completionHandler)
+void WebChromeClient::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, LocalFrame& frame, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired, CompletionHandler<void(RequestStorageAccessResult)>&& completionHandler)
{
RefPtr webFrame = WebFrame::fromCoreFrame(frame);
ASSERT(webFrame);
if (RefPtr page = m_page.get())
- page->requestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), *webFrame, scope, hasOrShouldIgnoreUserGesture, WTF::move(completionHandler));
+ page->requestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), *webFrame, scope, hasUserGestureOrNoUserGestureRequired, WTF::move(completionHandler));
else
completionHandler({ });
}
diff --git a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h
index 19896b0cd0d5..e47d57813d3b 100644
--- a/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h
+++ b/Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h
@@ -458,7 +458,7 @@ private:
void didInvalidateDocumentMarkerRects() final;
void hasStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebCore::LocalFrame&, WTF::CompletionHandler<void(bool)>&&) final;
- void requestStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebCore::LocalFrame&, WebCore::StorageAccessScope, WebCore::HasOrShouldIgnoreUserGesture, WTF::CompletionHandler<void(WebCore::RequestStorageAccessResult)>&&) final;
+ void requestStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebCore::LocalFrame&, WebCore::StorageAccessScope, WebCore::HasUserGestureOrNoUserGestureRequired, WTF::CompletionHandler<void(WebCore::RequestStorageAccessResult)>&&) final;
bool hasPageLevelStorageAccess(const WebCore::RegistrableDomain& topLevelDomain, const WebCore::RegistrableDomain& resourceDomain) const final;
void setLoginStatus(WebCore::RegistrableDomain&&, WebCore::IsLoggedIn, WTF::CompletionHandler<void()>&&) final;
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.cpp b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
index f1043a817fad..14b7a11d8032 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.cpp
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.cpp
@@ -8962,9 +8962,9 @@ void WebPage::hasStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDo
WebProcess::singleton().ensureNetworkProcessConnection().connection().sendWithAsyncReply(Messages::NetworkConnectionToWebProcess::HasStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frame.frameID(), m_identifier), WTF::move(completionHandler));
}
-void WebPage::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebFrame& frame, StorageAccessScope scope, HasOrShouldIgnoreUserGesture hasOrShouldIgnoreUserGesture, CompletionHandler<void(WebCore::RequestStorageAccessResult)>&& completionHandler)
+void WebPage::requestStorageAccess(RegistrableDomain&& subFrameDomain, RegistrableDomain&& topFrameDomain, WebFrame& frame, StorageAccessScope scope, HasUserGestureOrNoUserGestureRequired hasUserGestureOrNoUserGestureRequired, CompletionHandler<void(WebCore::RequestStorageAccessResult)>&& completionHandler)
{
- WebProcess::singleton().ensureNetworkProcessConnection().connection().sendWithAsyncReply(Messages::NetworkConnectionToWebProcess::RequestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frame.frameID(), m_identifier, m_webPageProxyIdentifier, scope, hasOrShouldIgnoreUserGesture), [this, protectedThis = Ref { *this }, completionHandler = WTF::move(completionHandler), frame = Ref { frame }, pageID = m_identifier, frameID = frame.frameID()](RequestStorageAccessResult result) mutable {
+ WebProcess::singleton().ensureNetworkProcessConnection().connection().sendWithAsyncReply(Messages::NetworkConnectionToWebProcess::RequestStorageAccess(WTF::move(subFrameDomain), WTF::move(topFrameDomain), frame.frameID(), m_identifier, m_webPageProxyIdentifier, scope, hasUserGestureOrNoUserGestureRequired), [this, protectedThis = Ref { *this }, completionHandler = WTF::move(completionHandler), frame = Ref { frame }, pageID = m_identifier, frameID = frame.frameID()](RequestStorageAccessResult result) mutable {
if (result.wasGranted == StorageAccessWasGranted::Yes) {
switch (result.scope) {
case StorageAccessScope::PerFrame:
diff --git a/Source/WebKit/WebProcess/WebPage/WebPage.h b/Source/WebKit/WebProcess/WebPage/WebPage.h
index 6a01b62af0c9..4d6f03a245e1 100644
--- a/Source/WebKit/WebProcess/WebPage/WebPage.h
+++ b/Source/WebKit/WebProcess/WebPage/WebPage.h
@@ -254,7 +254,7 @@ enum class EventHandling : uint8_t;
enum class EventMakesGamepadsVisible : bool;
enum class ExceptionCode : uint8_t;
enum class FinalizeRenderingUpdateFlags : uint8_t;
-enum class HasOrShouldIgnoreUserGesture : bool;
+enum class HasUserGestureOrNoUserGestureRequired : bool;
enum class HighlightRequestOriginatedInApp : bool;
enum class IFrameUnloadReason : bool;
enum class ImageDecodingError : uint8_t;
@@ -1787,7 +1787,7 @@ public:
#endif
void hasStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebFrame&, CompletionHandler<void(bool)>&&);
- void requestStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebFrame&, WebCore::StorageAccessScope, WebCore::HasOrShouldIgnoreUserGesture, CompletionHandler<void(WebCore::RequestStorageAccessResult)>&&);
+ void requestStorageAccess(WebCore::RegistrableDomain&& subFrameDomain, WebCore::RegistrableDomain&& topFrameDomain, WebFrame&, WebCore::StorageAccessScope, WebCore::HasUserGestureOrNoUserGestureRequired, CompletionHandler<void(WebCore::RequestStorageAccessResult)>&&);
void setLoginStatus(WebCore::RegistrableDomain&&, WebCore::IsLoggedIn, CompletionHandler<void()>&&);
void isLoggedIn(WebCore::RegistrableDomain&&, CompletionHandler<void(bool)>&&);
bool hasPageLevelStorageAccess(const WebCore::RegistrableDomain& topLevelDomain, const WebCore::RegistrableDomain& resourceDomain) const;
diff --git a/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt
new file mode 100644
index 000000000000..38eed2435aec
--- /dev/null
+++ b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt
@@ -0,0 +1,10 @@
+Tests that rejecting requestStorageAccess() without a user gesture does not synthesize user activation.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Rejection handler correctly had no user activation.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html
new file mode 100644
index 000000000000..5b9aea0c27da
--- /dev/null
+++ b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html><!-- webkit-test-runner [ JavaScriptCanOpenWindowsAutomatically=false ] -->
+<html>
+<head>
+ <script src="/js-test-resources/js-test.js"></script>
+ <script src="/js-test-resources/ui-helper.js"></script>
+ <script src="/resourceLoadStatistics/resources/util.js"></script>
+ <script>
+ description("Tests that rejecting requestStorageAccess() without a user gesture does not synthesize user activation.");
+ jsTestIsAsync = true;
+
+ const hostUnderTest = "localhost:8000";
+ const statisticsUrl = "http://" + hostUnderTest + "/temp";
+
+ window.addEventListener("message", receiveMessage, false);
+
+ function receiveMessage(event) {
+ if (event.origin === "http://localhost:8000") {
+ if (event.data.indexOf("PASS ") !== -1)
+ testPassed(event.data.replace("PASS ", ""));
+ else
+ testFailed(event.data.replace("FAIL ", ""));
+ } else
+ testFailed("Received a message from an unexpected origin: " + event.origin);
+ setEnableFeature(false, finishJSTest);
+ }
+
+ function runTest() {
+ setEnableFeature(true, function() {
+ testRunner.setStatisticsPrevalentResource(statisticsUrl, true, function() {
+ if (!testRunner.isStatisticsPrevalentResource(statisticsUrl))
+ testFailed("Host did not get set as prevalent resource.");
+ testRunner.setStatisticsHasHadUserInteraction(statisticsUrl, false, function() {
+ if (testRunner.isStatisticsHasHadUserInteraction(statisticsUrl))
+ testFailed("Host did get logged for user interaction.");
+ testRunner.statisticsUpdateCookieBlocking(function() {
+ let iframeElement = document.createElement("iframe");
+ iframeElement.id = "TheIframeThatRequestsStorageAccess";
+ iframeElement.src = "http://localhost:8000/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html";
+ document.body.appendChild(iframeElement);
+ });
+ });
+ });
+ });
+ }
+ </script>
+</head>
+<body onload="runTest()">
+</body>
+</html>
diff --git a/LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html b/LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html
new file mode 100644
index 000000000000..5610afddfdf8
--- /dev/null
+++ b/LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ function makeRequestWithoutUserGesture() {
+ document.requestStorageAccess().then(
+ function () {
+ top.postMessage("FAIL requestStorageAccess() was granted without user gesture.", "http://127.0.0.1:8000");
+ },
+ function () {
+ let win = window.open("about:blank", "_blank");
+ if (win) {
+ win.close();
+ top.postMessage("FAIL Rejection handler had user activation despite no user gesture.", "http://127.0.0.1:8000");
+ } else
+ top.postMessage("PASS Rejection handler correctly had no user activation.", "http://127.0.0.1:8000");
+ }
+ );
+ }
+ </script>
+</head>
+<body onload="makeRequestWithoutUserGesture()">
+</body>
+</html>
Regression Test / PoC
diff --git a/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt
new file mode 100644
index 000000000000..38eed2435aec
--- /dev/null
+++ b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt
@@ -0,0 +1,10 @@
+Tests that rejecting requestStorageAccess() without a user gesture does not synthesize user activation.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Rejection handler correctly had no user activation.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html
new file mode 100644
index 000000000000..5b9aea0c27da
--- /dev/null
+++ b/LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html><!-- webkit-test-runner [ JavaScriptCanOpenWindowsAutomatically=false ] -->
+<html>
+<head>
+ <script src="/js-test-resources/js-test.js"></script>
+ <script src="/js-test-resources/ui-helper.js"></script>
+ <script src="/resourceLoadStatistics/resources/util.js"></script>
+ <script>
+ description("Tests that rejecting requestStorageAccess() without a user gesture does not synthesize user activation.");
+ jsTestIsAsync = true;
+
+ const hostUnderTest = "localhost:8000";
+ const statisticsUrl = "http://" + hostUnderTest + "/temp";
+
+ window.addEventListener("message", receiveMessage, false);
+
+ function receiveMessage(event) {
+ if (event.origin === "http://localhost:8000") {
+ if (event.data.indexOf("PASS ") !== -1)
+ testPassed(event.data.replace("PASS ", ""));
+ else
+ testFailed(event.data.replace("FAIL ", ""));
+ } else
+ testFailed("Received a message from an unexpected origin: " + event.origin);
+ setEnableFeature(false, finishJSTest);
+ }
+
+ function runTest() {
+ setEnableFeature(true, function() {
+ testRunner.setStatisticsPrevalentResource(statisticsUrl, true, function() {
+ if (!testRunner.isStatisticsPrevalentResource(statisticsUrl))
+ testFailed("Host did not get set as prevalent resource.");
+ testRunner.setStatisticsHasHadUserInteraction(statisticsUrl, false, function() {
+ if (testRunner.isStatisticsHasHadUserInteraction(statisticsUrl))
+ testFailed("Host did get logged for user interaction.");
+ testRunner.statisticsUpdateCookieBlocking(function() {
+ let iframeElement = document.createElement("iframe");
+ iframeElement.id = "TheIframeThatRequestsStorageAccess";
+ iframeElement.src = "http://localhost:8000/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html";
+ document.body.appendChild(iframeElement);
+ });
+ });
+ });
+ });
+ }
+ </script>
+</head>
+<body onload="runTest()">
+</body>
+</html>
diff --git a/LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html b/LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html
new file mode 100644
index 000000000000..5610afddfdf8
--- /dev/null
+++ b/LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ function makeRequestWithoutUserGesture() {
+ document.requestStorageAccess().then(
+ function () {
+ top.postMessage("FAIL requestStorageAccess() was granted without user gesture.", "http://127.0.0.1:8000");
+ },
+ function () {
+ let win = window.open("about:blank", "_blank");
+ if (win) {
+ win.close();
+ top.postMessage("FAIL Rejection handler had user activation despite no user gesture.", "http://127.0.0.1:8000");
+ } else
+ top.postMessage("PASS Rejection handler correctly had no user activation.", "http://127.0.0.1:8000");
+ }
+ );
+ }
+ </script>
+</head>
+<body onload="makeRequestWithoutUserGesture()">
+</body>
+</html>