CVE-2025-24208
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.cppSource/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) {