Released July 27, 2026

CVE-2026-43804

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

Overview

Impact:

Description: Visiting a website may lead to an app denial-of-service

Researchers: Heiko Kiesel of SEEMOO, TU Darmstadt

Key insight. Programmatic focus of another window via a named-target window.open was gated on page visibility but not on consuming a transient user activation, letting a page refocus its opener (a popunder) with no fresh gesture.

Patch Details

diff --git a/Source/WebCore/loader/FrameLoader.cpp b/Source/WebCore/loader/FrameLoader.cpp
index b8b82805eb5b..0191bc145bab 100644
--- a/Source/WebCore/loader/FrameLoader.cpp
+++ b/Source/WebCore/loader/FrameLoader.cpp
@@ -4872,7 +4872,8 @@ std::pair<RefPtr<Frame>, CreatedNewPage> createWindow(LocalFrame& openerFrame, F
     if (!request.frameName().isEmpty() && !isBlankTargetFrameName(request.frameName())) {
         if (RefPtr frame = openerFrame.loader().findFrameForNavigation(request.frameName(), openerFrame.protectedDocument().get())) {
             if (!isSelfTargetFrameName(request.frameName())) {
-                if (RefPtr page = frame->page(); page && isInVisibleAndActivePage(openerFrame))
+                RefPtr openerWindow = openerFrame.window();
+                if (RefPtr page = frame->page(); page && isInVisibleAndActivePage(openerFrame) && openerWindow && openerWindow->consumeTransientActivation())
                     page->chrome().focus();
             }
             frame->updateOpener(openerFrame);
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm
index bfdac0f91dfd..70993eff08d7 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm
@@ -269,6 +269,70 @@ HTTPServer server({
     // during popup creation.
     EXPECT_FALSE(focusCalled);
 }
+
+TEST(VerifyUserGesture, PopunderPreventedViaDualEventListeners)
+{
+    auto openerHTML = "<script>"
+        "window.name = 'opener';"
+        "let popup;"
+        "let opened = false;"
+        "addEventListener('pointerdown', () => {"
+        "    if (!opened) {"
+        "        opened = true;"
+        "        popup = window.open('https://domain2.com/popup');"
+        "    }"
+        "});"
+        "addEventListener('click', () => {"
+        "    if (popup && popup.refocusOpener) popup.refocusOpener();"
+        "});"
+        "</script>"_s;
+    auto popupHTML = "<script>"
+        "function refocusOpener() { window.open('', 'opener'); }"
+        "</script>"_s;
+    HTTPServer server({
+        { "/opener"_s, { openerHTML } },
+        { "/popup"_s, { popupHTML } }
+    }, HTTPServer::Protocol::HttpsProxy);
+
+    RetainPtr configuration = server.httpsProxyConfiguration();
+    RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+    [navigationDelegate allowAnyTLSCertificate];
+    RetainPtr uiDelegate = adoptNS([TestUIDelegate new]);
+    RetainPtr openerWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [openerWebView setNavigationDelegate:navigationDelegate.get()];
+    [openerWebView setUIDelegate:uiDelegate.get()];
+    [openerWebView configuration].preferences.javaScriptCanOpenWindowsAutomatically = YES;
+
+    __block RetainPtr<TestWKWebView> popupWebView;
+    __block RetainPtr<TestNavigationDelegate> popupNavigationDelegate;
+    uiDelegate.get().createWebViewWithConfiguration = ^(WKWebViewConfiguration *configuration, WKNavigationAction *navigationAction, WKWindowFeatures *) {
+        [navigationAction._userInitiatedAction consume];
+        popupNavigationDelegate = adoptNS([TestNavigationDelegate new]);
+        [popupNavigationDelegate allowAnyTLSCertificate];
+        popupWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration]);
+        [popupWebView setNavigationDelegate:popupNavigationDelegate.get()];
+        return popupWebView.get();
+    };
+
+    __block bool focusCalled = false;
+    uiDelegate.get().focusWebView = ^(WKWebView *) {
+        focusCalled = true;
+    };
+
+    [openerWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/opener"]]];
+    [navigationDelegate waitForDidFinishNavigation];
+
+    [openerWebView mouseDownAtPoint:CGPointMake(50, 50) simulatePressure:NO];
+    [openerWebView mouseUpAtPoint:CGPointMake(50, 50)];
+    [openerWebView waitForPendingMouseEvents];
+    while (!popupWebView)
+        Util::spinRunLoop();
+    [popupNavigationDelegate waitForDidFinishNavigation];
+
+    Util::spinRunLoop(10);
+
+    EXPECT_FALSE(focusCalled);
+}
 #endif
 
 }

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-43804
Bugzilla 316816
Component WebCore
Bug Class Bypass
Severity medium
Commit 2057f457fb75d4e5…
Advisory Apple Advisory

Terminology

  • Transient activation — A short-lived ’the user just interacted’ state that gates privileged actions like opening or focusing windows; it can be consumed so a single gesture is not reused.
  • Named-target window.open — window.open(url, name) reuses an existing browsing context whose window.name matches, rather than creating a new one — the path createWindow handles here.
  • Popunder — An abuse where a page opens a popup and then refocuses itself, leaving the popup hidden beneath the main window.
  • consumeTransientActivation() — Checks that a valid transient activation exists and clears it, so the gesture cannot be spent twice.

Root Cause Analysis

createWindow in FrameLoader.cpp implements window.open(url, name) when the name targets an already-existing frame. When a matching frame is found and the name is not _self, the code raised/focused the target’s chrome with page->chrome().focus(), gated only on isInVisibleAndActivePage(openerFrame). It never required — or consumed — a transient user activation. As a result script could programmatically pull focus to an existing window (typically its opener) with no genuine user gesture, defeating the popup/popunder protections that are supposed to require a real activation: the opener spawns a popup and then, later, calls window.open('', 'opener') to yank focus back to itself, leaving the popup underneath (a ‘popunder’). The fix captures RefPtr openerWindow = openerFrame.window() and adds && openerWindow && openerWindow->consumeTransientActivation() to the focus condition. consumeTransientActivation() both verifies a live transient activation exists and spends it, so (a) script with no activation cannot focus another window at all, and (b) one user gesture can no longer be replayed to steal focus repeatedly. The added API test PopunderPreventedViaDualEventListeners reproduces the bypass: a pointerdown listener opens the popup and a separate click listener calls back into the opener via window.open('', 'opener'), a dual-listener split that previously slipped past the single-activation check. The restored invariant is that programmatic cross-window focus through a named-target window.open requires and consumes a fresh transient user activation. This is a logic / UI-integrity defect, not a memory-safety bug.

Attack Path

1. Register split listeners

Opener page adds a pointerdown listener and a click listener; browsers deliver pointerdown before click for one physical press.

2. Open the popup on pointerdown

On the first pointerdown the page calls window.open(‘https://victim/popup’) while it still holds activation.

3. Refocus the opener on click

On the subsequent click the page calls popup.refocusOpener(), which runs window.open(’’, ‘opener’) in the popup targeting the opener by name.

4. Steal focus without a fresh gesture

Pre-patch createWindow focuses the opener’s chrome gated only on visibility, so focus returns to the opener and the popup is left underneath — a popunder achieved without consuming an activation.

Impact Assessment

No memory-safety primitive is involved. The impact is UI-integrity abuse: a page can steal window focus / create popunders without a real user gesture, which is disruptive (the advisory frames it as an app denial-of-service) and can assist clickjacking or phishing by controlling which window is frontmost. It is confined to WebContent window-management behavior and does not lead to code execution. Rated medium (CVSS 6.5).

Audit Directions

Other script-reachable focus paths

grep loader/ and page/ for chrome().focus( and ->focus( reached from script without a nearby consumeTransientActivation() check.

Reusable-gesture checks

Find places that call hasTransientActivation()/isProcessingUserGesture() but never consume it — grep hasTransientActivation versus consumeTransientActivation — since those allow one gesture to authorize repeated privileged actions.

Other named-target behaviors

Audit updateOpener/setOpener and other createWindow branches gated on isInVisibleAndActivePage rather than on activation for the same missing-gesture gap.

Changed Functions

Function File Change Note
createWindow Source/WebCore/loader/FrameLoader.cpp modified When targeting an existing named (non-self) frame, now also requires openerFrame.window()->consumeTransientActivation() before calling page->chrome().focus(), so focus needs and spends a real user gesture.
VerifyUserGesture.PopunderPreventedViaDualEventListeners Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm added Regression test asserting focusWebView is not called when the opener uses the pointerdown/click dual-listener trick to refocus itself.

Files Changed

Source Files

  • Source/WebCore/loader/FrameLoader.cpp

Test Files

  • Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm

Regression Test

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

diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm
index bfdac0f91dfd..70993eff08d7 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/VerifyUserGestureFromUIProcess.mm
@@ -269,6 +269,70 @@ TEST(VerifyUserGesture, PopunderPreventedByConsumedAction)
     // during popup creation.
     EXPECT_FALSE(focusCalled);
 }
+
+TEST(VerifyUserGesture, PopunderPreventedViaDualEventListeners)
+{
+    auto openerHTML = "<script>"
+        "window.name = 'opener';"
+        "let popup;"
+        "let opened = false;"
+        "addEventListener('pointerdown', () => {"
+        "    if (!opened) {"
+        "        opened = true;"
+        "        popup = window.open('https://domain2.com/popup');"
+        "    }"
+        "});"
+        "addEventListener('click', () => {"
+        "    if (popup && popup.refocusOpener) popup.refocusOpener();"
+        "});"
+        "</script>"_s;
+    auto popupHTML = "<script>"
+        "function refocusOpener() { window.open('', 'opener'); }"
+        "</script>"_s;
+    HTTPServer server({
+        { "/opener"_s, { openerHTML } },
+        { "/popup"_s, { popupHTML } }
+    }, HTTPServer::Protocol::HttpsProxy);
+
+    RetainPtr configuration = server.httpsProxyConfiguration();
+    RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+    [navigationDelegate allowAnyTLSCertificate];
+    RetainPtr uiDelegate = adoptNS([TestUIDelegate new]);
+    RetainPtr openerWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [openerWebView setNavigationDelegate:navigationDelegate.get()];
+    [openerWebView setUIDelegate:uiDelegate.get()];
+    [openerWebView configuration].preferences.javaScriptCanOpenWindowsAutomatically = YES;
+
+    __block RetainPtr<TestWKWebView> popupWebView;
+    __block RetainPtr<TestNavigationDelegate> popupNavigationDelegate;
+    uiDelegate.get().createWebViewWithConfiguration = ^(WKWebViewConfiguration *configuration, WKNavigationAction *navigationAction, WKWindowFeatures *) {
+        [navigationAction._userInitiatedAction consume];
+        popupNavigationDelegate = adoptNS([TestNavigationDelegate new]);
+        [popupNavigationDelegate allowAnyTLSCertificate];
+        popupWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration]);
+        [popupWebView setNavigationDelegate:popupNavigationDelegate.get()];
+        return popupWebView.get();
+    };
+
+    __block bool focusCalled = false;
+    uiDelegate.get().focusWebView = ^(WKWebView *) {
+        focusCalled = true;
+    };
+
+    [openerWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/opener"]]];
+    [navigationDelegate waitForDidFinishNavigation];
+
+    [openerWebView mouseDownAtPoint:CGPointMake(50, 50) simulatePressure:NO];
+    [openerWebView mouseUpAtPoint:CGPointMake(50, 50)];
+    [openerWebView waitForPendingMouseEvents];
+    while (!popupWebView)
+        Util::spinRunLoop();
+    [popupNavigationDelegate waitForDidFinishNavigation];
+
+    Util::spinRunLoop(10);
+
+    EXPECT_FALSE(focusCalled);
+}
 #endif
 
 }