Released June 29, 2026

CVE-2026-43721

CVSS 6.5 severity medium CWE-732 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N

Overview

Impact:

Description: A malicious website may be able to silently hijack clipboard data

Researchers: Idan Masas

Key insight. Preserving transient user activation was conditioned on the wrong signal — ’no prompt shown’ — instead of ’the caller actually had a gesture’, so a no-gesture, no-prompt rejection leaked activation the frame never earned; the fix restores the invariant by requiring the gesture state captured at call time.

Patch Details

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>
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

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-43721
Bugzilla 313478
Component WebCore
Bug Class Bypass
Severity medium
Commit d8576e6cceeb0595…
Advisory Apple Advisory

Terminology

  • 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

1. 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.

2. 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.

3. 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.

4. 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.

5. 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

This is a logic/security-policy bug, not a memory-safety primitive: it grants a page unearned transient user activation, which the CVE ties to silent clipboard access (and generically enables other gesture-gated actions such as popups). There is no memory corruption, OOB, or UAF here, so it does not escalate toward code execution; the realistic outcome is a privacy/integrity violation (reading or overwriting clipboard contents, launching a popup) driven entirely from the WebContent process within the normal renderer sandbox. Severity is medium: user-visible harm without a crash or sandbox escape.

Audit Directions

Other branches of the same completion handler

In 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 enum

Audit 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 handlers

Look 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 flows

Search 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.

Changed Functions

Function File Change Note
DocumentStorageAccess::requestStorageAccess Source/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::requestStorageAccessQuirk Source/WebCore/dom/DocumentStorageAccess.cpp modified Passes the renamed enum value HasUserGestureOrNoUserGestureRequired::Yes; mechanical rename only.
WebResourceLoadStatisticsStore::requestStorageAccess Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp modified Signature/parameter renamed and the No-branch check updated to the new enum name; behavior unchanged.
NetworkConnectionToWebProcess::requestStorageAccess Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp modified Forwards the renamed enum to resourceLoadStatistics; mechanical rename.
WebChromeClient::requestStorageAccess Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp modified Renamed parameter forwarded to WebPage; mechanical rename.
WebPage::requestStorageAccess Source/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

Source Files

  • Source/WebCore/dom/DocumentStorageAccess.cpp
  • Source/WebCore/dom/DocumentStorageAccess.h
  • Source/WebCore/page/ChromeClient.h
  • Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.cpp
  • Source/WebKit/NetworkProcess/Classifier/WebResourceLoadStatisticsStore.h
  • Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.cpp
  • Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.h
  • Source/WebKit/NetworkProcess/NetworkConnectionToWebProcess.messages.in
  • Source/WebKit/Scripts/webkit/messages.py
  • Source/WebKit/Shared/WebCoreArgumentCoders.serialization.in
  • Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.cpp
  • Source/WebKit/WebProcess/WebCoreSupport/WebChromeClient.h
  • Source/WebKit/WebProcess/WebPage/WebPage.cpp
  • Source/WebKit/WebProcess/WebPage/WebPage.h

Test Files

  • LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate-expected.txt
  • LayoutTests/http/tests/storageAccess/request-storage-access-rejected-without-gesture-should-not-activate.html
  • LayoutTests/http/tests/storageAccess/resources/request-storage-access-without-gesture-check-activation-iframe.html

Regression Test

The fix ships this proof-of-concept / regression test:

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>