CVE-2025-24208
Overview
Background
- 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
- Embed a cross-origin iframe A page loads a cross-origin subframe.
- Call window.open('javascript:...') The subframe calls window.open with a javascript: URL.
- Load during createNewPage The single-window UI client responds by calling loadRequest with the javascript: URL during createNewPage.
- Cross-site script execution The javascript: URL executes in the host/opener page’s context, a cross-site scripting attack.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/WebKit/UIProcess/WebPageProxy.cpp |
modified |
Files Changed
Source/WebKit/UIProcess/WebPageProxy.cppSource/WebKit/UIProcess/WebPageProxy.hTools/TestWebKitAPI/Tests/WebKitCocoa/OpenAndCloseWindow.mm
Audit Directions
- javascript: URL entry pointsGrep WebPageProxy for loadRequest/loadData paths that can receive a protocolIsJavaScript() URL from content without an origin/context gate.
- createNewPage side effectsAudit the createNewPage / UI-client flow for other requests that can be loaded into the host page during window.open.
Patch
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) {
Regression Test / PoC
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) {