Released July 29, 2025

CVE-2025-43240

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

Overview

Impact:

Description: A download’s origin may be incorrectly associated

Researchers: Syarif Muhammad Sajjad

Key insight. Downloads arising from history traversal (targetItem) or client/user-input navigations have no legitimate web-content originating frame, yet the old code still attributed them to a prior frame’s origin; the fix passes std::nullopt in exactly those cases so no false origin is associated.

Patch Details

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);
+}
+
 }

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43240
Bugzilla 293994
Component WebKit
Bug Class CrossOrigin
Severity medium
Commit 0578185b8a7484c5…
Advisory Apple Advisory

Terminology

  • 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

1. 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.

2. 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).

3. Reach the vulnerable proxy creation

receivedPolicyDecision or receivedNavigationResponsePolicyDecision creates a DownloadProxy and, pre-patch, stamps it with navigation->originatingFrameInfo() from an unrelated/prior frame.

4. 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.

5. 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

This is a logic/origin-attribution bug in the UIProcess download path, not a memory-safety issue; there is no corruption primitive and no path to code execution. The concrete impact is that a download can be labeled with an originating origin that did not initiate it, misleading origin-based UI or client policy and undermining download provenance (a spoofing/CrossOrigin-attribution weakness). It is confined to UIProcess metadata handling and its severity is medium because it can defeat security decisions keyed on the download’s origin without granting broader compromise.

Audit Directions

Every createDownloadProxy call site

Grep 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 provenance

Search 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 parity

Grep 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 objects

Look 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.

Changed Functions

Function File Change Note
WebPageProxy::receivedPolicyDecision Source/WebKit/UIProcess/WebPageProxy.cpp modified For Download actions, now passes std::nullopt as originating frame info when the navigation is a targetItem (back/forward) or isRequestFromClientOrUserInput, otherwise keeps the original originatingFrameInfo.
WebPageProxy::receivedNavigationResponsePolicyDecision Source/WebKit/UIProcess/WebPageProxy.cpp modified Same conditional fix applied to the navigation-response download path: nullopt originating frame for back/forward or client/user-input navigations.
WKDownload OriginatingFrame tests Tools/TestWebKitAPI/Tests/WebKitCocoa/Download.mm modified Adds a local frameInfoShouldBeEqual block and isClientOrUserInitiated flag, plus two new tests asserting that go-back and client-input downloads report an empty originatingFrame host while genuinely frame-initiated downloads still match the opener frame.

Files Changed

Source Files

  • Source/WebKit/UIProcess/WebPageProxy.cpp

Test Files

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

Regression Test

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

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);
+}
+
 }