CVE-2025-43240
Overview
Background
- DownloadProxy
- The UIProcess object representing an in-flight download, whose originating frame info is surfaced to clients (e.g. WKDownload.originatingFrame).
- originatingFrameInfo
- Metadata describing the frame (URL, security origin, webView) credited with initiating a navigation or download.
- targetItem
- A back/forward-list item that a navigation is traversing to; its presence marks the navigation as history traversal rather than a fresh content-initiated load.
- isRequestFromClientOrUserInput
- A navigation flag indicating the request originated from the embedding client or direct user input rather than from executing web content.
- PolicyAction::Download
- The navigation policy outcome telling WebKit to turn a navigation/response into a download instead of rendering it.
Root Cause Analysis
WebPageProxy::receivedPolicyDecision() and receivedNavigationResponsePolicyDecision() run in the UIProcess and, when the policy action is PolicyAction::Download, construct a DownloadProxy via ProcessPool::createDownloadProxy(). The last argument to that call is the originating frame info, which becomes the download’s originatingFrame / associated origin exposed to the client (e.g. WKDownload.originatingFrame). Before the patch, both paths unconditionally passed navigation->originatingFrameInfo() (or the navigationAction’s originatingFrameInfoData) as the originating frame. The invariant being violated is that a download’s originating frame/origin should reflect the actual frame that initiated the resource load, and only when that association is meaningful. For navigations that are back/forward list traversals (navigation->targetItem() is set) or that came from client/user input rather than from web content (navigation->isRequestFromClientOrUserInput()), there is no legitimate web-content frame origin to attribute the download to — yet the old code still stamped the download with the previous/opener frame’s originatingFrameInfo, incorrectly associating the download with an origin (e.g. the prior page’s host) that did not actually initiate it.
The fix adds a condition: when navigation exists and it is either a targetItem (go-back/forward) navigation or a client/user-input-initiated request, the download proxy is created with std::nullopt for the originating frame info, so no misleading origin is attached (the client sees an empty host / about:blank-like frame). Otherwise the original originatingFrameInfo is preserved. The added API tests (OriginatingFrameHostWhenDownloadComesFromGoBackNavigation and …FromClientInputNavigation, plus the isClientOrUserInitiated branch in the opener test) assert exactly this: for go-back and client-initiated downloads, download.originatingFrame.securityOrigin.host is empty, while genuinely frame-initiated downloads still equal the opener main frame. This restores correct, non-spoofable origin attribution for downloads.
Attack Path
- Set up navigation state The attacker gets the victim to load attacker or third-party pages so that a back/forward list entry or a client/user-input navigation can later resolve to a download.
- Trigger a download via a non-content-initiated navigation A go-back navigation (targetItem) or a client/user-input request results in a response the client turns into a download (PolicyAction::Download).
- Reach the vulnerable proxy creation receivedPolicyDecision or receivedNavigationResponsePolicyDecision creates a DownloadProxy and, pre-patch, stamps it with navigation->originatingFrameInfo() from an unrelated/prior frame.
- Cause origin misattribution The download is now associated with an originating frame/origin that did not initiate it, so download.originatingFrame reports the wrong (attacker-influenced or victim) host.
- Exploit downstream origin-based decisions Any client logic or security prompt that trusts the download’s originating origin (attribution UI, per-origin download policies, provenance checks) can be misled by the spoofed association.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifSource/WebKit/UIProcess/WebPageProxy.cpp |
modified | |
ifTools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm |
modified |
Files Changed
Source/WebKit/UIProcess/WebPageProxy.cppTools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
Audit Directions
- Every createDownloadProxy call siteGrep for createDownloadProxy across UIProcess and verify each passes originating frame info only when a genuine content frame initiated it; look for callers still using originatingFrameInfo unconditionally.
- Other consumers of originatingFrameInfo for provenanceSearch for originatingFrameInfo / originatingFrameInfoData and downloadOriginatingPage to find other features (e.g. app-initiated state, cross-origin checks) that may inherit an unrelated frame’s origin during back/forward or client-input navigations.
- targetItem / isRequestFromClientOrUserInput gating parityGrep for navigation->targetItem() and isRequestFromClientOrUserInput() to see where these distinctions matter and audit paths that credit an origin without checking them, especially security-relevant attribution.
- Origin association on other navigation-derived objectsLook at how UserMediaPermission, WebsiteDataStore attribution, and referrer/opener plumbing choose an origin for navigations lacking a real initiating frame, applying the same nullopt-when-no-real-origin reasoning.
Patch
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 7258568bbfd6..f736411a38ed 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -5118,7 +5118,13 @@ void WebPageProxy::receivedPolicyDecision(PolicyAction action, API::Navigation*
std::optional<DownloadID> downloadID;
if (action == PolicyAction::Download) {
// Create a download proxy.
- auto download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, navigationAction->request(), downloadOriginatingPage(navigation).ptr(), navigation ? navigation->originatingFrameInfo() : std::optional(navigationAction->data().originatingFrameInfoData));
+ RefPtr<DownloadProxy> download;
+
+ if (navigation && (navigation->targetItem() || navigation->isRequestFromClientOrUserInput()))
+ download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, navigationAction->request(), downloadOriginatingPage(navigation).ptr(), std::nullopt);
+ else
+ download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, navigationAction->request(), downloadOriginatingPage(navigation).ptr(), navigation ? navigation->originatingFrameInfo() : std::optional(navigationAction->data().originatingFrameInfoData));
+
download->setDidStartCallback([weakThis = WeakPtr { *this }, navigationAction = WTFMove(navigationAction)] (auto* downloadProxy) {
RefPtr protectedThis = weakThis.get();
if (!protectedThis || !downloadProxy)
@@ -5158,7 +5164,13 @@ void WebPageProxy::receivedNavigationResponsePolicyDecision(WebCore::PolicyActio
std::optional<DownloadID> downloadID;
if (action == PolicyAction::Download) {
- auto download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, request, downloadOriginatingPage(navigation).ptr(), navigation ? navigation->originatingFrameInfo() : std::nullopt);
+ RefPtr<DownloadProxy> download;
+
+ if (navigation && (navigation->targetItem() || navigation->isRequestFromClientOrUserInput()))
+ download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, request, downloadOriginatingPage(navigation).ptr(), std::nullopt);
+ else
+ download = m_configuration->protectedProcessPool()->createDownloadProxy(m_websiteDataStore, request, downloadOriginatingPage(navigation).ptr(), navigation ? navigation->originatingFrameInfo() : std::nullopt);
+
download->setDidStartCallback([weakThis = WeakPtr { *this }, navigationResponse = WTFMove(navigationResponse)] (auto* downloadProxy) {
RefPtr protectedThis = weakThis.get();
if (!protectedThis || !downloadProxy)
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
index 041298c0d6f2..c96a4e3b170a 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
@@ -3257,12 +3257,25 @@ TEST(WKDownload, OriginatingFrameWhenConvertingNavigationInNewWindow)
return openedWebView.get();
};
+ auto frameInfoShouldBeEqual = ^(WKFrameInfo *a, WKFrameInfo *b) {
+ EXPECT_EQ(a.isMainFrame, b.isMainFrame);
+ EXPECT_WK_STREQ(a.request.URL.absoluteString, b.request.URL.absoluteString);
+ EXPECT_WK_STREQ(a.securityOrigin.protocol, b.securityOrigin.protocol);
+ EXPECT_WK_STREQ(a.securityOrigin.host, b.securityOrigin.host);
+ EXPECT_EQ(a.securityOrigin.port, b.securityOrigin.port);
+ EXPECT_EQ(a.webView, b.webView);
+ };
+
+ __block bool isClientOrUserInitiated = false;
__block bool checkedDownload { false };
+
auto tryOpenerInitiatedDownloads = ^{
checkedDownload = false;
+ isClientOrUserInitiated = true;
[webView evaluateJavaScript:@"a = document.createElement('a'); a.href = 'https://webkit.org/download'; a.target = '_blank'; document.body.appendChild(a); a.click()" completionHandler:nil];
Util::run(&checkedDownload);
+ isClientOrUserInitiated = false;
checkedDownload = false;
[webView evaluateJavaScript:@"w = window.open('https://webkit.org/download')" completionHandler:nil];
Util::run(&checkedDownload);
@@ -3282,7 +3295,15 @@ TEST(WKDownload, OriginatingFrameWhenConvertingNavigationInNewWindow)
};
navigationDelegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *response, WKDownload *download) {
frameInfoShouldBeEqual(response._navigationInitiatingFrame, openerMainFrame.get());
- frameInfoShouldBeEqual(download.originatingFrame, openerMainFrame.get());
+
+ if (isClientOrUserInitiated) {
+ EXPECT_WK_STREQ(download.originatingFrame.request.URL.absoluteString, "about:blank");
+ EXPECT_WK_STREQ(download.originatingFrame.securityOrigin.host, "");
+
+ isClientOrUserInitiated = false;
+ } else
+ frameInfoShouldBeEqual(download.originatingFrame, openerMainFrame.get());
+
checkedDownload = true;
};
tryOpenerInitiatedDownloads();
@@ -3337,4 +3358,106 @@ TEST(WKDownload, OriginatingFrameWhenNavigatingToNewDomainWithRedirect)
TestWebKitAPI::Util::run(&checkedDownload);
}
+TEST(WKDownload, OriginatingFrameHostWhenDownloadComesFromGoBackNavigation)
+{
+ HTTPServer server({
+ { "/firstSite"_s, { "firstSite"_s } },
+ { "/secondSite"_s, { "secondSite"_s } }
+ }, HTTPServer::Protocol::HttpsProxy);
+
+ RetainPtr configuration = server.httpsProxyConfiguration();
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) configuration:configuration.get()]);
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ [webView setNavigationDelegate:navigationDelegate.get()];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://firstSite.com/firstSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://secondSite.com/secondSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ RetainPtr downloadDelegate = adoptNS([TestDownloadDelegate new]);
+ NSURL *expectedDownloadFile = tempFileThatDoesNotExist();
+ __block bool downloadDestinationDecided = false;
+
+ downloadDelegate.get().decideDestinationUsingResponse = ^(WKDownload *download, NSURLResponse *, NSString *, void (^completionHandler)(NSURL *)) {
+ EXPECT_STREQ(download.originatingFrame.securityOrigin.host.UTF8String, "");
+
+ downloadDestinationDecided = true;
+ completionHandler(expectedDownloadFile);
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationAction = ^(WKNavigationAction *, void (^completionHandler)(WKNavigationActionPolicy)) {
+ completionHandler(WKNavigationActionPolicyDownload);
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^completionHandler)(WKNavigationResponsePolicy)) {
+ completionHandler(WKNavigationResponsePolicyDownload);
+ };
+
+ navigationDelegate.get().navigationActionDidBecomeDownload = ^(WKNavigationAction *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ navigationDelegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ [webView goBack];
+ Util::run(&downloadDestinationDecided);
+}
+
+TEST(WKDownload, OriginatingFrameHostWhenDownloadComesFromClientInputNavigation)
+{
+ HTTPServer server({
+ { "/firstSite"_s, { "firstSite"_s } },
+ { "/secondSite"_s, { "secondSite"_s } },
+ { "/thirdSite"_s, { "thirdSite"_s } }
+ }, HTTPServer::Protocol::HttpsProxy);
+
+ RetainPtr configuration = server.httpsProxyConfiguration();
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) configuration:configuration.get()]);
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ [webView setNavigationDelegate:navigationDelegate.get()];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://firstSite.com/firstSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://secondSite.com/secondSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ RetainPtr downloadDelegate = adoptNS([TestDownloadDelegate new]);
+ __block bool downloadDestinationDecided = false;
+
+ downloadDelegate.get().decideDestinationUsingResponse = ^(WKDownload *download, NSURLResponse *, NSString *, void (^completionHandler)(NSURL *)) {
+ EXPECT_STREQ(download.originatingFrame.securityOrigin.host.UTF8String, "");
+
+ downloadDestinationDecided = true;
+ completionHandler(tempFileThatDoesNotExist());
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationAction = ^(WKNavigationAction *, void (^completionHandler)(WKNavigationActionPolicy)) {
+ completionHandler(WKNavigationActionPolicyDownload);
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^completionHandler)(WKNavigationResponsePolicy)) {
+ completionHandler(WKNavigationResponsePolicyDownload);
+ };
+
+ navigationDelegate.get().navigationActionDidBecomeDownload = ^(WKNavigationAction *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ navigationDelegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://thirdSite.com/thirdSite"]]];
+ Util::run(&downloadDestinationDecided);
+}
+
}
Regression Test / PoC
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
index 041298c0d6f2..c96a4e3b170a 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm
@@ -3257,12 +3257,25 @@ TEST(WKDownload, OriginatingFrameWhenConvertingNavigationInNewWindow)
return openedWebView.get();
};
+ auto frameInfoShouldBeEqual = ^(WKFrameInfo *a, WKFrameInfo *b) {
+ EXPECT_EQ(a.isMainFrame, b.isMainFrame);
+ EXPECT_WK_STREQ(a.request.URL.absoluteString, b.request.URL.absoluteString);
+ EXPECT_WK_STREQ(a.securityOrigin.protocol, b.securityOrigin.protocol);
+ EXPECT_WK_STREQ(a.securityOrigin.host, b.securityOrigin.host);
+ EXPECT_EQ(a.securityOrigin.port, b.securityOrigin.port);
+ EXPECT_EQ(a.webView, b.webView);
+ };
+
+ __block bool isClientOrUserInitiated = false;
__block bool checkedDownload { false };
+
auto tryOpenerInitiatedDownloads = ^{
checkedDownload = false;
+ isClientOrUserInitiated = true;
[webView evaluateJavaScript:@"a = document.createElement('a'); a.href = 'https://webkit.org/download'; a.target = '_blank'; document.body.appendChild(a); a.click()" completionHandler:nil];
Util::run(&checkedDownload);
+ isClientOrUserInitiated = false;
checkedDownload = false;
[webView evaluateJavaScript:@"w = window.open('https://webkit.org/download')" completionHandler:nil];
Util::run(&checkedDownload);
@@ -3282,7 +3295,15 @@ TEST(WKDownload, OriginatingFrameWhenConvertingNavigationInNewWindow)
};
navigationDelegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *response, WKDownload *download) {
frameInfoShouldBeEqual(response._navigationInitiatingFrame, openerMainFrame.get());
- frameInfoShouldBeEqual(download.originatingFrame, openerMainFrame.get());
+
+ if (isClientOrUserInitiated) {
+ EXPECT_WK_STREQ(download.originatingFrame.request.URL.absoluteString, "about:blank");
+ EXPECT_WK_STREQ(download.originatingFrame.securityOrigin.host, "");
+
+ isClientOrUserInitiated = false;
+ } else
+ frameInfoShouldBeEqual(download.originatingFrame, openerMainFrame.get());
+
checkedDownload = true;
};
tryOpenerInitiatedDownloads();
@@ -3337,4 +3358,106 @@ TEST(WKDownload, OriginatingFrameWhenNavigatingToNewDomainWithRedirect)
TestWebKitAPI::Util::run(&checkedDownload);
}
+TEST(WKDownload, OriginatingFrameHostWhenDownloadComesFromGoBackNavigation)
+{
+ HTTPServer server({
+ { "/firstSite"_s, { "firstSite"_s } },
+ { "/secondSite"_s, { "secondSite"_s } }
+ }, HTTPServer::Protocol::HttpsProxy);
+
+ RetainPtr configuration = server.httpsProxyConfiguration();
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) configuration:configuration.get()]);
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ [webView setNavigationDelegate:navigationDelegate.get()];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://firstSite.com/firstSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://secondSite.com/secondSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ RetainPtr downloadDelegate = adoptNS([TestDownloadDelegate new]);
+ NSURL *expectedDownloadFile = tempFileThatDoesNotExist();
+ __block bool downloadDestinationDecided = false;
+
+ downloadDelegate.get().decideDestinationUsingResponse = ^(WKDownload *download, NSURLResponse *, NSString *, void (^completionHandler)(NSURL *)) {
+ EXPECT_STREQ(download.originatingFrame.securityOrigin.host.UTF8String, "");
+
+ downloadDestinationDecided = true;
+ completionHandler(expectedDownloadFile);
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationAction = ^(WKNavigationAction *, void (^completionHandler)(WKNavigationActionPolicy)) {
+ completionHandler(WKNavigationActionPolicyDownload);
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^completionHandler)(WKNavigationResponsePolicy)) {
+ completionHandler(WKNavigationResponsePolicyDownload);
+ };
+
+ navigationDelegate.get().navigationActionDidBecomeDownload = ^(WKNavigationAction *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ navigationDelegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ [webView goBack];
+ Util::run(&downloadDestinationDecided);
+}
+
+TEST(WKDownload, OriginatingFrameHostWhenDownloadComesFromClientInputNavigation)
+{
+ HTTPServer server({
+ { "/firstSite"_s, { "firstSite"_s } },
+ { "/secondSite"_s, { "secondSite"_s } },
+ { "/thirdSite"_s, { "thirdSite"_s } }
+ }, HTTPServer::Protocol::HttpsProxy);
+
+ RetainPtr configuration = server.httpsProxyConfiguration();
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 300) configuration:configuration.get()]);
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+ [webView setNavigationDelegate:navigationDelegate.get()];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://firstSite.com/firstSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://secondSite.com/secondSite"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ RetainPtr downloadDelegate = adoptNS([TestDownloadDelegate new]);
+ __block bool downloadDestinationDecided = false;
+
+ downloadDelegate.get().decideDestinationUsingResponse = ^(WKDownload *download, NSURLResponse *, NSString *, void (^completionHandler)(NSURL *)) {
+ EXPECT_STREQ(download.originatingFrame.securityOrigin.host.UTF8String, "");
+
+ downloadDestinationDecided = true;
+ completionHandler(tempFileThatDoesNotExist());
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationAction = ^(WKNavigationAction *, void (^completionHandler)(WKNavigationActionPolicy)) {
+ completionHandler(WKNavigationActionPolicyDownload);
+ };
+
+ navigationDelegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^completionHandler)(WKNavigationResponsePolicy)) {
+ completionHandler(WKNavigationResponsePolicyDownload);
+ };
+
+ navigationDelegate.get().navigationActionDidBecomeDownload = ^(WKNavigationAction *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ navigationDelegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *download) {
+ download.delegate = downloadDelegate.get();
+ };
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://thirdSite.com/thirdSite"]]];
+ Util::run(&downloadDestinationDecided);
+}
+
}