Released March 31, 2025

CVE-2025-24208

CVSS 6.1 severity medium CWE-79 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Overview

Impact:

Description: Loading a malicious iframe may lead to a cross-site scripting attack

Researchers: Muhammad Zaid Ghifari (Mr.ZheeV) and Kalimantan Utara

Key insight. A javascript: URL passed to window.open could be loaded through the createNewPage path and execute in the wrong origin; refusing javascript: loads while createNewPage is in progress blocks the XSS.

Patch Details

diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 1ed72a79e47b..9c880f410184 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -1978,6 +1978,11 @@ RefPtr<API::Navigation> WebPageProxy::loadRequest(WebCore::ResourceRequest&& req
 
     WEBPAGEPROXY_RELEASE_LOG(Loading, "loadRequest:");
 
+    if (m_isCallingCreateNewPage && request.url().protocolIsJavaScript()) {
+        WEBPAGEPROXY_RELEASE_LOG(Loading, "loadRequest: Not loading javascript URL during createNewPage.");
+        return nullptr;
+    }
+
     if (!hasRunningProcess())
         launchProcess(Site { request.url() }, ProcessLaunchReason::InitialProcess);
 
@@ -8407,6 +8412,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w
         openedBlobURL,
         wantsNoOpener = windowFeatures.wantsNoOpener()
     ] (RefPtr<WebPageProxy> newPage) mutable {
+        m_isCallingCreateNewPage = false;
         if (!newPage) {
             reply(std::nullopt, std::nullopt);
             return;
@@ -8481,6 +8487,7 @@ void WebPageProxy::createNewPage(IPC::Connection& connection, WindowFeatures&& w
     }
 
     trySOAuthorization(configuration.copyRef(), WTFMove(navigationAction), *this, WTFMove(completionHandler), [this, protectedThis = Ref { *this }, configuration] (Ref<API::NavigationAction>&& navigationAction, CompletionHandler<void(RefPtr<WebPageProxy>&&)>&& completionHandler) mutable {
+        m_isCallingCreateNewPage = true;
         m_uiClient->createNewPage(*this, WTFMove(configuration), WTFMove(navigationAction), WTFMove(completionHandler));
     });
 }
diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h
index e59999486597..fb0f5231ef06 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.h
+++ b/Source/WebKit/UIProcess/WebPageProxy.h
@@ -3403,6 +3403,8 @@ private:
     String m_failingProvisionalLoadURL;
     bool m_isLoadingAlternateHTMLStringForFailingProvisionalLoad { false };
 
+    bool m_isCallingCreateNewPage { false };
+
     std::unique_ptr<WebPageLoadTiming> m_pageLoadTiming;
     HashSet<WebCore::FrameIdentifier> m_framesWithSubresourceLoadingForPageLoadTiming;
     RunLoop::Timer m_generatePageLoadTimingTimer;
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
index 470859c0c5d1..c5b9f945482e 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
@@ -26,16 +26,21 @@
 #import "config.h"
 
 #import "DeprecatedGlobalValues.h"
+#import "HTTPServer.h"
 #import "PlatformUtilities.h"
+#import "Test.h"
 #import "TestNavigationDelegate.h"
 #import "TestUIDelegate.h"
 #import "TestWKWebView.h"
+#import <WebKit/WKFrameInfo.h>
+#import <WebKit/WKFrameInfoPrivate.h>
 #import <WebKit/WKNavigationActionPrivate.h>
 #import <WebKit/WKPreferences.h>
 #import <WebKit/WKUIDelegatePrivate.h>
 #import <WebKit/WKWebViewConfiguration.h>
 #import <WebKit/WKWebViewPrivate.h>
 #import <WebKit/WKWindowFeaturesPrivate.h>
+#import <WebKit/_WKFrameTreeNode.h>
 #import <wtf/RetainPtr.h>
 
 @class OpenAndCloseWindowUIDelegate;
@@ -451,6 +456,33 @@ TEST(WebKit, TryClose)
     EXPECT_FALSE([webView _tryClose]);
 }
 
+TEST(WebKit, TryWindowOpenJavascriptURLInIframeSingleWindowApp)
+{
+    TestWebKitAPI::HTTPServer server({
+        { "/mainframe"_s, { "<iframe src='https://domain2.com/subframe'></iframe>"_s } },
+        { "/subframe"_s, { ""_s } }
+    }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+    auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
+    [navigationDelegate allowAnyTLSCertificate];
+    auto webView = adoptNS([TestWKWebView new]);
+    webView.get().configuration.preferences.javaScriptCanOpenWindowsAutomatically = YES;
+    auto uiDelegate = adoptNS([TestUIDelegate new]);
+    [webView setUIDelegate:uiDelegate.get()];
+    webView.get().navigationDelegate = navigationDelegate.get();
+
+    __block bool calledCreateWebViewWithConfiguration { false };
+    uiDelegate.get().createWebViewWithConfiguration = ^WKWebView *(WKWebViewConfiguration *, WKNavigationAction *navigationAction, WKWindowFeatures *) {
+        EXPECT_NULL([webView loadRequest:[navigationAction request]]);
+        calledCreateWebViewWithConfiguration = true;
+        return nil;
+    };
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
+    [webView objectByEvaluatingJavaScript:@"window.open(\"javascript:alert('test');\")" inFrame:[webView firstChildFrame]];
+    TestWebKitAPI::Util::run(&calledCreateWebViewWithConfiguration);
+}
+
 TEST(WEBKIT, NavigationActionHasOpener)
 {
     auto runHasOpenerTest = [](NSString *js, bool expectsOpener) {

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-24208
Bugzilla 286381
Component WebKit
Bug Class CrossOrigin
Severity medium
Commit 609e8c7a932f28b7…
Advisory Apple Advisory

Terminology

  • createNewPage / window.open — The UI-process flow that creates a new page when content calls window.open, delegating to the app’s UI client.
  • javascript: URL — A URL scheme whose body executes as script in the loading document’s context; loading it in the wrong context is XSS.
  • Single-window app — A UI client that handles window.open by loading into the existing view, which is where the javascript: URL slipped in.

Root Cause Analysis

This fixes a cross-site-scripting vector where a javascript: URL could be loaded during the window.open (createNewPage) flow. When web content calls window.open, WebPageProxy::createNewPage asks the app’s UI client to create a new page; in a single-window app the client may respond by loading the requested URL into the existing view via WebPageProxy::loadRequest. A cross-origin subframe calling window.open(‘javascript:…’) could thereby get a javascript: URL loaded through this path and executed in the opener/host page’s context — a universal/cross-site scripting condition, because the javascript: URL runs with the wrong document’s origin rather than being blocked. The fix adds an m_isCallingCreateNewPage flag: it is set true immediately before invoking the UI client’s createNewPage and cleared in the createNewPage completion handler, and WebPageProxy::loadRequest now returns nullptr (refusing the load) when m_isCallingCreateNewPage is true and request.url().protocolIsJavaScript(). The restored invariant is that javascript: URLs cannot be loaded through the createNewPage window. The regression test loads a javascript: URL via window.open from a cross-origin iframe and expects the loadRequest to be refused (NULL).

Attack Path

1. Embed a cross-origin iframe

A page loads a cross-origin subframe.

2. Call window.open(‘javascript:…’)

The subframe calls window.open with a javascript: URL.

3. Load during createNewPage

The single-window UI client responds by calling loadRequest with the javascript: URL during createNewPage.

4. Cross-site script execution

The javascript: URL executes in the host/opener page’s context, a cross-site scripting attack.

Impact Assessment

A cross-site scripting condition in the WebContent process reachable from a crafted iframe via window.open, letting attacker script run in another origin’s context — a serious same-origin-policy bypass. No memory corruption; the impact is universal XSS gated by the app’s window.open handling.

Audit Directions

javascript: URL entry points

Grep WebPageProxy for loadRequest/loadData paths that can receive a protocolIsJavaScript() URL from content without an origin/context gate.

createNewPage side effects

Audit the createNewPage / UI-client flow for other requests that can be loaded into the host page during window.open.

Changed Functions

Function File Change Note
WebPageProxy::loadRequest Source/WebKit/UIProcess/WebPageProxy.cpp modified Returns nullptr for a javascript: URL while m_isCallingCreateNewPage is true, blocking the load during the window.open flow.
WebPageProxy::createNewPage Source/WebKit/UIProcess/WebPageProxy.cpp modified Sets m_isCallingCreateNewPage around the UI client’s createNewPage call and clears it in the completion handler.
WebPageProxy::m_isCallingCreateNewPage Source/WebKit/UIProcess/WebPageProxy.h modified New flag marking the window that a javascript: URL must not be loaded through.

Files Changed

Source Files

  • Source/WebKit/UIProcess/WebPageProxy.cpp
  • Source/WebKit/UIProcess/WebPageProxy.h

Test Files

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

Regression Test

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

diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
index 470859c0c5d1..c5b9f945482e 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
@@ -26,16 +26,21 @@
 #import "config.h"
 
 #import "DeprecatedGlobalValues.h"
+#import "HTTPServer.h"
 #import "PlatformUtilities.h"
+#import "Test.h"
 #import "TestNavigationDelegate.h"
 #import "TestUIDelegate.h"
 #import "TestWKWebView.h"
+#import <WebKit/WKFrameInfo.h>
+#import <WebKit/WKFrameInfoPrivate.h>
 #import <WebKit/WKNavigationActionPrivate.h>
 #import <WebKit/WKPreferences.h>
 #import <WebKit/WKUIDelegatePrivate.h>
 #import <WebKit/WKWebViewConfiguration.h>
 #import <WebKit/WKWebViewPrivate.h>
 #import <WebKit/WKWindowFeaturesPrivate.h>
+#import <WebKit/_WKFrameTreeNode.h>
 #import <wtf/RetainPtr.h>
 
 @class OpenAndCloseWindowUIDelegate;
@@ -451,6 +456,33 @@ TEST(WebKit, TryClose)
     EXPECT_FALSE([webView _tryClose]);
 }
 
+TEST(WebKit, TryWindowOpenJavascriptURLInIframeSingleWindowApp)
+{
+    TestWebKitAPI::HTTPServer server({
+        { "/mainframe"_s, { "<iframe src='https://domain2.com/subframe'></iframe>"_s } },
+        { "/subframe"_s, { ""_s } }
+    }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+    auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
+    [navigationDelegate allowAnyTLSCertificate];
+    auto webView = adoptNS([TestWKWebView new]);
+    webView.get().configuration.preferences.javaScriptCanOpenWindowsAutomatically = YES;
+    auto uiDelegate = adoptNS([TestUIDelegate new]);
+    [webView setUIDelegate:uiDelegate.get()];
+    webView.get().navigationDelegate = navigationDelegate.get();
+
+    __block bool calledCreateWebViewWithConfiguration { false };
+    uiDelegate.get().createWebViewWithConfiguration = ^WKWebView *(WKWebViewConfiguration *, WKNavigationAction *navigationAction, WKWindowFeatures *) {
+        EXPECT_NULL([webView loadRequest:[navigationAction request]]);
+        calledCreateWebViewWithConfiguration = true;
+        return nil;
+    };
+
+    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://domain1.com/mainframe"]]];
+    [webView objectByEvaluatingJavaScript:@"window.open(\"javascript:alert('test');\")" inFrame:[webView firstChildFrame]];
+    TestWebKitAPI::Util::run(&calledCreateWebViewWithConfiguration);
+}
+
 TEST(WEBKIT, NavigationActionHasOpener)
 {
     auto runHasOpenerTest = [](NSString *js, bool expectsOpener) {