Released May 11, 2026

CVE-2026-28971

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

Overview

Impact:

Description: A malicious iframe may use another website’s download settings

Researchers: Khiem Tran

Key insight. Download decisions were not gated on the completion of the navigation’s own Safe Browsing check, so a malicious iframe could slip a download past the warning; deferring the download until the per-navigation check completes and honoring a match closes it.

Patch Details

diff --git a/Source/WebKit/UIProcess/API/APINavigation.cpp b/Source/WebKit/UIProcess/API/APINavigation.cpp
index a65c4a21dfe6..28f2960c47ab 100644
--- a/Source/WebKit/UIProcess/API/APINavigation.cpp
+++ b/Source/WebKit/UIProcess/API/APINavigation.cpp
@@ -187,6 +187,21 @@ void Navigation::setSafeBrowsingWarning(RefPtr<WebKit::BrowsingWarning>&& safeBr
     m_safeBrowsingWarning = WTF::move(safeBrowsingWarning);
 }
 
+void Navigation::whenSafeBrowsingCheckCompletes(Function<void()>&& callback)
+{
+    if (!safeBrowsingCheckOngoing()) {
+        callback();
+        return;
+    }
+    m_safeBrowsingCheckCompletionCallbacks.append(WTF::move(callback));
+}
+
+void Navigation::fireSafeBrowsingCheckCompletionCallbacks()
+{
+    for (auto& callback : std::exchange(m_safeBrowsingCheckCompletionCallbacks, { }))
+        callback();
+}
+
 size_t Navigation::redirectChainIndex(const WTF::URL& url)
 {
     size_t index = m_redirectChain.find(url);
diff --git a/Source/WebKit/UIProcess/API/APINavigation.h b/Source/WebKit/UIProcess/API/APINavigation.h
index 6ae3d0fa8e37..96ec09585e65 100644
--- a/Source/WebKit/UIProcess/API/APINavigation.h
+++ b/Source/WebKit/UIProcess/API/APINavigation.h
@@ -188,6 +188,8 @@ public:
     bool NODELETE safeBrowsingCheckOngoing();
     void setSafeBrowsingWarning(RefPtr<WebKit::BrowsingWarning>&&);
     RefPtr<WebKit::BrowsingWarning> NODELETE safeBrowsingWarning();
+    void whenSafeBrowsingCheckCompletes(Function<void()>&&);
+    void fireSafeBrowsingCheckCompletionCallbacks();
     void setSafeBrowsingCheckTimedOut() { m_safeBrowsingCheckTimedOut = true; }
     bool safeBrowsingCheckTimedOut() { return m_safeBrowsingCheckTimedOut; }
     bool hadSafeBrowsingWarning() const { return m_hadSafeBrowsingWarning; }
@@ -250,6 +252,7 @@ private:
     MonotonicTime m_requestStart { MonotonicTime::now() };
     RefPtr<WebKit::BrowsingWarning> m_safeBrowsingWarning;
     ListHashSet<size_t> m_ongoingSafeBrowsingChecks;
+    Vector<Function<void()>> m_safeBrowsingCheckCompletionCallbacks;
     RefPtr<WebKit::FrameProcess> m_pendingSharedProcess;
     RefPtr<WebKit::FrameState> m_backForwardFrameState;
 };
diff --git a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
index e0ce751cde8e..d20e7d987e3e 100644
--- a/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
+++ b/Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
@@ -326,6 +326,9 @@ void WebPageProxy::beginSafeBrowsingCheck(const URL& url, API::Navigation& navig
                 }
             }
 
+            if (!navigation->safeBrowsingCheckOngoing())
+                navigation->fireSafeBrowsingCheckCompletionCallbacks();
+
             if (!navigation->safeBrowsingCheckOngoing() && navigation->safeBrowsingWarning() && navigation->safeBrowsingCheckTimedOut()) {
                 protectedThis->setHasShownSafeBrowsingWarningAfterLastLoadCommit();
                 protectedThis->showBrowsingWarning(navigation->safeBrowsingWarning());
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index 5270ec0eabe5..30de1e0e1e55 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -9405,6 +9405,54 @@ void WebPageProxy::decidePolicyForNavigationAction(Ref<WebProcessProxy>&& proces
 
         protectedPageClient->clearBrowsingWarning();
 
+        if (policyAction == PolicyAction::Download && navigation->safeBrowsingCheckOngoing()) {
+            navigation->whenSafeBrowsingCheckCompletes([
+                this, protectedThis = WTF::move(protectedThis), navigation, completionHandlerWrapper = WTF::move(completionHandlerWrapper),
+                frame, frameInfo = WTF::move(frameInfo), protectedPageClient = WTF::move(protectedPageClient)
+            ] mutable {
+                if (RefPtr safeBrowsingWarning = navigation->safeBrowsingWarning()) {
+                    navigation->setSafeBrowsingWarning(nullptr);
+                    if (!frame->isMainFrame()) {
+                        auto error = interruptedForPolicyChangeError(navigation->currentRequest());
+                        m_navigationClient->didFailProvisionalNavigationWithError(*this, FrameInfoData { frameInfo }, navigation.get(), navigation->currentRequest().url(), error, nullptr);
+                        WEBPAGEPROXY_RELEASE_LOG(Loading, "decidePolicyForNavigationAction: Ignoring download because Safe Browsing found a match.");
+                        completionHandlerWrapper(PolicyAction::Ignore);
+                        return;
+                    }
+
+                    Ref protectedPageLoadState = pageLoadState();
+                    auto transaction = protectedPageLoadState->transaction();
+                    protectedPageLoadState->setTitleFromBrowsingWarning(transaction, safeBrowsingWarning->title());
+
+                    protectedPageClient->showBrowsingWarning(*safeBrowsingWarning, [protectedThis = WTF::move(protectedThis), completionHandlerWrapper = WTF::move(completionHandlerWrapper), protectedPageClient](auto&& result) mutable {
+                        Ref protectedPageLoadState = protectedThis->pageLoadState();
+                        auto transaction = protectedPageLoadState->transaction();
+                        protectedPageLoadState->setTitleFromBrowsingWarning(transaction, { });
+
+                        switchOn(result, [&](const URL& url) {
+                            completionHandlerWrapper(PolicyAction::Ignore);
+                            protectedThis->loadRequest({ URL { url } });
+                        }, [&protectedThis, &completionHandlerWrapper](ContinueUnsafeLoad continueUnsafeLoad) {
+                            switch (continueUnsafeLoad) {
+                            case ContinueUnsafeLoad::No:
+                                if (!protectedThis->hasCommittedAnyProvisionalLoads())
+                                    protectedThis->m_uiClient->close(protectedThis.ptr());
+                                completionHandlerWrapper(PolicyAction::Ignore);
+                                break;
+                            case ContinueUnsafeLoad::Yes:
+                                completionHandlerWrapper(PolicyAction::Download);
+                                break;
+                            }
+                        });
+                    });
+                    m_uiClient->didShowSafeBrowsingWarning();
+                    return;
+                }
+                completionHandlerWrapper(PolicyAction::Download);
+            });
+            return;
+        }
+
         if (RefPtr safeBrowsingWarning = navigation->safeBrowsingWarning()) {
             navigation->setSafeBrowsingWarning(nullptr);
             if (frame->isMainFrame() && safeBrowsingWarning->url().isValid()) {
@@ -9727,9 +9775,43 @@ void WebPageProxy::decidePolicyForResponseShared(Ref<WebProcessProxy>&& process,
 #if USE(QUICK_LOOK) && ENABLE(QUICKLOOK_SANDBOX_RESTRICTIONS)
         bool supportsMIMEType = PreviewConverter::supportsMIMEType(navigationResponse->response().mimeType());
 #endif
-        auto completionHandlerWrapper = [navigation, protectedThis, request, navigationResponse = WTF::move(navigationResponse), frameInfo = WTF::move(frameInfo), completionHandler = WTF::move(completionHandler)]  (PolicyAction policyAction) mutable {
+        auto completionHandlerWrapper = [navigation, protectedThis, request, navigationResponse = WTF::move(navigationResponse), completionHandler = WTF::move(completionHandler)](PolicyAction policyAction) mutable {
             protectedThis->receivedNavigationResponsePolicyDecision(policyAction, navigation.get(), request, WTF::move(navigationResponse), WTF::move(completionHandler));
         };
+        if (policyAction == PolicyAction::Download && navigation && navigation->safeBrowsingCheckOngoing()) {
+            navigation->whenSafeBrowsingCheckCompletes([
+                this, protectedThis = WTF::move(protectedThis), navigation, completionHandlerWrapper = WTF::move(completionHandlerWrapper),
+                frame, frameInfo = WTF::move(frameInfo), request
+            ] mutable {
+                if (RefPtr safeBrowsingWarning = navigation->safeBrowsingWarning()) {
+                    if (!frame->isMainFrame()) {
+                        auto error = interruptedForPolicyChangeError(navigation->currentRequest());
+                        m_navigationClient->didFailProvisionalNavigationWithError(*this, FrameInfoData { frameInfo }, navigation.get(), request.url(), error, nullptr);
+                        completionHandlerWrapper(PolicyAction::Ignore);
+                        return;
+                    }
+                    Ref protectedPageLoadState = pageLoadState();
+                    auto transaction = protectedPageLoadState->transaction();
+                    protectedPageLoadState->setTitleFromBrowsingWarning(transaction, safeBrowsingWarning->title());
+                    navigation->setSafeBrowsingWarning(nullptr);
+                    protect(protectedThis->pageClient())->showBrowsingWarning(*safeBrowsingWarning, [protectedThis = WTF::move(protectedThis), completionHandlerWrapper = WTF::move(completionHandlerWrapper)](auto&& result) mutable {
+                        switchOn(result, [&](const URL& url) {
+                            completionHandlerWrapper(PolicyAction::Ignore);
+                            protectedThis->loadRequest(URL { url });
+                        }, [&](ContinueUnsafeLoad continueUnsafeLoad) {
+                            if (continueUnsafeLoad == ContinueUnsafeLoad::No)
+                                completionHandlerWrapper(PolicyAction::Ignore);
+                            else
+                                completionHandlerWrapper(PolicyAction::Download);
+                        });
+                    });
+                    m_uiClient->didShowSafeBrowsingWarning();
+                    return;
+                }
+                completionHandlerWrapper(PolicyAction::Download);
+            });
+            return;
+        }
         if (navigation && navigation->safeBrowsingWarning()) {
             RefPtr safeBrowsingWarning = navigation->safeBrowsingWarning();
             if (frame->isMainFrame() && safeBrowsingWarning->url().isValid()) {
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm
index 4b417cd08322..c4894846a476 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm
@@ -1194,4 +1194,184 @@ TEST(SafeBrowsing, SetTimeoutNavigationFromWarningPage)
     EXPECT_FALSE(didCloseCalled);
 }
 
+TEST(SafeBrowsing, DownloadDeferredAndBlockedBySafeBrowsing)
+{
+    DelayedLookupContext.delayDuration = 50_ms;
+    TestWebKitAPI::HTTPServer server({
+        { "/malicious"_s, { "download-content"_s } },
+    }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+    RetainPtr configuration = server.httpsProxyConfiguration();
+
+    ClassMethodSwizzler swizzler(getSSBLookupContextClassSingleton(), @selector(sharedLookupContext), [DelayedLookupContext methodForSelector:@selector(sharedLookupContext)]);
+
+    RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES;
+    RetainPtr delegate = adoptNS([TestNavigationDelegate new]);
+    [delegate allowAnyTLSCertificate];
+
+    __block bool downloadStarted = false;
+    delegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^decisionHandler)(WKNavigationResponsePolicy)) {
+        decisionHandler(WKNavigationResponsePolicyDownload);
+    };
+    delegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *) {
+        downloadStarted = true;
+    };
+
+    [webView setNavigationDelegate:delegate.get()];
+    [webView evaluateJavaScript:@"window.location = 'https://example2.com/malicious'" completionHandler:nil];
+
+    while (![webView _safeBrowsingWarning])

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-28971
Bugzilla 311288
Component WebKit
Bug Class Bypass
Severity medium
Commit 4b574bf8287b6b57…
Advisory Apple Advisory

Terminology

  • Safe Browsing check — An asynchronous check that flags navigations to known-unsafe URLs and shows a warning before proceeding.
  • Download policy action — A navigation whose response is handled as a download rather than displayed; it must still respect Safe Browsing.
  • Main frame vs subframe — Downloads and warnings are handled differently for the top-level frame versus an embedded iframe.
  • Per-navigation state — Safe Browsing results are tied to a specific API::Navigation; using another navigation’s state confuses the security decision.

Root Cause Analysis

When a navigation resolves to a Download, WebKit must still honor the Safe Browsing check associated with that navigation. Pre-patch, a download policy decision could proceed without waiting for the navigation’s Safe Browsing check to finish (and without correctly applying its result per frame), so a download initiated from a subframe/iframe could bypass or mis-apply the Safe Browsing warning – described as a malicious iframe using another website’s download settings, i.e. the download was not gated on the correct, per-navigation Safe Browsing state. The fix adds a completion mechanism on API::Navigation – whenSafeBrowsingCheckCompletes(callback), which runs immediately if no check is ongoing else queues the callback, and fireSafeBrowsingCheckCompletionCallbacks(), invoked from WebPageProxy::beginSafeBrowsingCheck when the check finishes (using std::exchange to drain the queue). In WebPageProxy::decidePolicyForNavigationAction, when policyAction == Download and a Safe Browsing check is ongoing, the download decision is deferred via whenSafeBrowsingCheckCompletes; on completion, if a Safe Browsing warning matched, the download is refused: for a non-main frame it fails the provisional navigation and returns PolicyAction::Ignore, and for the main frame it shows the browsing warning and only proceeds per the user’s choice. The restored invariant is that a download is authorized only after the navigation’s own Safe Browsing check completes and only if it did not match, so a subframe cannot slip a download past the warning or inherit another frame’s settings.

Attack Path

1. Embed a malicious iframe

A page hosts a cross-origin iframe that initiates a navigation which will resolve to a download.

2. Race the Safe Browsing check

The iframe triggers the download policy decision while the navigation’s Safe Browsing check is still ongoing.

3. Bypass the warning

Pre-patch, the download proceeds without waiting for / correctly applying the per-navigation Safe Browsing result, effectively using the wrong frame’s download handling.

4. Unsafe download

A download that Safe Browsing would have blocked is allowed, or attributed to another site’s settings, in the UIProcess.

Impact Assessment

A security-policy bypass (not memory safety): a subframe-initiated download could proceed without correctly waiting for or applying the navigation’s Safe Browsing check, letting a malicious iframe evade the warning or use another site’s download handling. It executes in the UIProcess and affects download safety/attribution; no code-execution path. Rated medium.

Audit Directions

Other policy actions vs Safe Browsing

Audit decidePolicyForNavigationAction/Response branches (Use, Download, Ignore) to confirm each waits for and applies the navigation’s Safe Browsing result.

Per-frame attribution

Review how frame vs main-frame and frameInfo are used in download and warning handling to prevent one frame inheriting another’s settings.

Async check completion races

grep for safeBrowsingCheckOngoing()/completion callbacks to ensure no security decision proceeds while a check is pending.

Changed Functions

Function File Change Note
Navigation::whenSafeBrowsingCheckCompletes / fireSafeBrowsingCheckCompletionCallbacks Source/WebKit/UIProcess/API/APINavigation.cpp added Adds a per-navigation completion queue: run now if no check is ongoing, else queue; fire drains it via std::exchange when the check finishes.
WebPageProxy::beginSafeBrowsingCheck Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm modified Fires the navigation’s Safe Browsing completion callbacks once the check is no longer ongoing.
WebPageProxy::decidePolicyForNavigationAction Source/WebKit/UIProcess/WebPageProxy.cpp modified For PolicyAction::Download with an ongoing Safe Browsing check, defers via whenSafeBrowsingCheckCompletes and, on a match, ignores the download (subframe: fail provisional navigation; main frame: show warning).

Files Changed

Source Files

  • Source/WebKit/UIProcess/API/APINavigation.cpp
  • Source/WebKit/UIProcess/API/APINavigation.h
  • Source/WebKit/UIProcess/Cocoa/WebPageProxyCocoa.mm
  • Source/WebKit/UIProcess/WebPageProxy.cpp

Test Files

  • Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm

Regression Test

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

diff --git a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm
index 4b417cd08322..c4894846a476 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/WKWebView/SafeBrowsing.mm
@@ -1194,4 +1194,184 @@ TEST(SafeBrowsing, SetTimeoutNavigationFromWarningPage)
     EXPECT_FALSE(didCloseCalled);
 }
 
+TEST(SafeBrowsing, DownloadDeferredAndBlockedBySafeBrowsing)
+{
+    DelayedLookupContext.delayDuration = 50_ms;
+    TestWebKitAPI::HTTPServer server({
+        { "/malicious"_s, { "download-content"_s } },
+    }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+    RetainPtr configuration = server.httpsProxyConfiguration();
+
+    ClassMethodSwizzler swizzler(getSSBLookupContextClassSingleton(), @selector(sharedLookupContext), [DelayedLookupContext methodForSelector:@selector(sharedLookupContext)]);
+
+    RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES;
+    RetainPtr delegate = adoptNS([TestNavigationDelegate new]);
+    [delegate allowAnyTLSCertificate];
+
+    __block bool downloadStarted = false;
+    delegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^decisionHandler)(WKNavigationResponsePolicy)) {
+        decisionHandler(WKNavigationResponsePolicyDownload);
+    };
+    delegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *) {
+        downloadStarted = true;
+    };
+
+    [webView setNavigationDelegate:delegate.get()];
+    [webView evaluateJavaScript:@"window.location = 'https://example2.com/malicious'" completionHandler:nil];
+
+    while (![webView _safeBrowsingWarning])
+        TestWebKitAPI::Util::spinRunLoop();
+
+    EXPECT_FALSE(downloadStarted);
+}
+
+TEST(SafeBrowsing, DownloadDeferredAndBlockedBySafeBrowsingPostTimeout)
+{
+    // Use a delay longer than the ~250ms listener timeout to verify the deferral
+    // waits for the actual Safe Browsing result, not just the timeout.
+    DelayedLookupContext.delayDuration = 500_ms;
+    TestWebKitAPI::HTTPServer server({
+        { "/malicious"_s, { "download-content"_s } },
+    }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+    RetainPtr configuration = server.httpsProxyConfiguration();
+
+    ClassMethodSwizzler swizzler(getSSBLookupContextClassSingleton(), @selector(sharedLookupContext), [DelayedLookupContext methodForSelector:@selector(sharedLookupContext)]);
+
+    RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES;
+    RetainPtr delegate = adoptNS([TestNavigationDelegate new]);
+    [delegate allowAnyTLSCertificate];
+
+    __block bool downloadStarted = false;
+    delegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^decisionHandler)(WKNavigationResponsePolicy)) {
+        decisionHandler(WKNavigationResponsePolicyDownload);
+    };
+    delegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *) {
+        downloadStarted = true;
+    };
+
+    [webView setNavigationDelegate:delegate.get()];
+    [webView evaluateJavaScript:@"window.location = 'https://example2.com/malicious'" completionHandler:nil];
+
+    while (![webView _safeBrowsingWarning])
+        TestWebKitAPI::Util::spinRunLoop();
+
+    EXPECT_FALSE(downloadStarted);
+}
+
+TEST(SafeBrowsing, CleanDownloadProceedsAfterSafeBrowsingCheck)
+{
+    DelayedLookupContext.delayDuration = 50_ms;
+    TestWebKitAPI::HTTPServer server({
+        { "/safe"_s, { "download-content"_s } },
+    }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+    RetainPtr configuration = server.httpsProxyConfiguration();
+
+    ClassMethodSwizzler swizzler(getSSBLookupContextClassSingleton(), @selector(sharedLookupContext), [DelayedLookupContext methodForSelector:@selector(sharedLookupContext)]);
+
+    RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES;
+    RetainPtr delegate = adoptNS([TestNavigationDelegate new]);
+    [delegate allowAnyTLSCertificate];
+
+    __block bool downloadStarted = false;
+    delegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *, void (^decisionHandler)(WKNavigationResponsePolicy)) {
+        decisionHandler(WKNavigationResponsePolicyDownload);
+    };
+    delegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *) {
+        downloadStarted = true;
+    };
+
+    [webView setNavigationDelegate:delegate.get()];
+    [webView evaluateJavaScript:@"window.location = 'https://example2.com/safe'" completionHandler:nil];
+
+    TestWebKitAPI::Util::run(&downloadStarted);
+    EXPECT_NULL([webView _safeBrowsingWarning]);
+}
+
+TEST(SafeBrowsing, SubframeDownloadBlockedBySafeBrowsing)
+{
+    DelayedLookupContext.delayDuration = 50_ms;
+    TestWebKitAPI::HTTPServer server({
+        { "/safe"_s, { "<html><body><iframe src='https://evil.com/malicious'></iframe></body></html>"_s } },
+        { "/malicious"_s, { "download-content"_s } },
+    }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+    RetainPtr configuration = server.httpsProxyConfiguration();
+
+    ClassMethodSwizzler swizzler(getSSBLookupContextClassSingleton(), @selector(sharedLookupContext), [DelayedLookupContext methodForSelector:@selector(sharedLookupContext)]);
+
+    RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES;
+    RetainPtr delegate = adoptNS([TestNavigationDelegate new]);
+    [delegate allowAnyTLSCertificate];
+
+    __block bool downloadStarted = false;
+    __block bool subframeNavigationFailed = false;
+    __block bool mainNavigationFinished = false;
+
+    delegate.get().decidePolicyForNavigationResponse = ^(WKNavigationResponse *response, void (^decisionHandler)(WKNavigationResponsePolicy)) {
+        if ([response.response.URL.path isEqualToString:@"/malicious"])
+            decisionHandler(WKNavigationResponsePolicyDownload);
+        else
+            decisionHandler(WKNavigationResponsePolicyAllow);
+    };
+    delegate.get().navigationResponseDidBecomeDownload = ^(WKNavigationResponse *, WKDownload *) {
+        downloadStarted = true;
+    };
+    delegate.get().didFailProvisionalLoadInSubframeWithError = ^(WKWebView *, WKFrameInfo *, NSError *error) {
+        EXPECT_NOT_NULL(error);
+        subframeNavigationFailed = true;
+    };
+    delegate.get().didFinishNavigation = ^(WKWebView *, WKNavigation *) {
+        mainNavigationFinished = true;
+    };
+
+    [webView setNavigationDelegate:delegate.get()];
+    [webView evaluateJavaScript:@"window.location = 'https://example2.com/safe'" completionHandler:nil];
+
+    TestWebKitAPI::Util::run(&mainNavigationFinished);
+
+    while (!subframeNavigationFailed && !downloadStarted)
+        TestWebKitAPI::Util::spinRunLoop();
+
+    EXPECT_TRUE(subframeNavigationFailed);
+    EXPECT_FALSE(downloadStarted);
+}
+
+TEST(SafeBrowsing, NavigationActionDownloadDeferredBySafeBrowsing)
+{
+    DelayedLookupContext.delayDuration = 50_ms;
+    TestWebKitAPI::HTTPServer server({
+        { "/malicious"_s, { "content"_s } },
+    }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+    RetainPtr configuration = server.httpsProxyConfiguration();
+
+    ClassMethodSwizzler swizzler(getSSBLookupContextClassSingleton(), @selector(sharedLookupContext), [DelayedLookupContext methodForSelector:@selector(sharedLookupContext)]);
+
+    RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+    [webView configuration].preferences.fraudulentWebsiteWarningEnabled = YES;
+    RetainPtr delegate = adoptNS([TestNavigationDelegate new]);
+    [delegate allowAnyTLSCertificate];
+
+    __block bool downloadStarted = false;
+    delegate.get().decidePolicyForNavigationAction = ^(WKNavigationAction *action, void (^decisionHandler)(WKNavigationActionPolicy)) {
+        if ([action.request.URL.path isEqualToString:@"/malicious"])
+            decisionHandler(WKNavigationActionPolicyDownload);
+        else
+            decisionHandler(WKNavigationActionPolicyAllow);
+    };
+    delegate.get().navigationActionDidBecomeDownload = ^(WKNavigationAction *, WKDownload *) {
+        downloadStarted = true;
+    };
+
+    [webView setNavigationDelegate:delegate.get()];
+    [webView evaluateJavaScript:@"window.location = 'https://example2.com/malicious'" completionHandler:nil];
+
+    while (![webView _safeBrowsingWarning])
+        TestWebKitAPI::Util::spinRunLoop();
+
+    EXPECT_FALSE(downloadStarted);
+}
+
 #endif // HAVE(SAFE_BROWSING)