Released July 29, 2025

CVE-2025-43229

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

Overview

Impact:

Description: Processing maliciously crafted web content may lead to universal cross site scripting

Researchers: Martin Bajanik of Fingerprint, Ammar Askar

Key insight. The fix closes a gap in the web-archive isolation work: local .webarchive loads honored the isolation data-store swap but not macOS quarantine/Gatekeeper, so a downloaded, unapproved archive could still execute and achieve UXSS; the guard now cancels the navigation when the file is quarantined and not user-approved.

Patch Details

diff --git a/LayoutTests/platform/mac-wk1/TestExpectations b/LayoutTests/platform/mac-wk1/TestExpectations
index 52e14e87eb4e..70e2e5b03b61 100644
--- a/LayoutTests/platform/mac-wk1/TestExpectations
+++ b/LayoutTests/platform/mac-wk1/TestExpectations
@@ -1733,6 +1733,8 @@ webkit.org/b/244308 imported/w3c/web-platform-tests/cookies/attributes/attribute
 # Only flakey in WK1
 webarchive/loading/test-loading-archive-subresource-null-mimetype.html [ Failure Pass ]
 
+webarchive/loading/test-loading-archive-with-link.html [ Skip ]
+
 # WebKit1 frames use native scrollbars causing these tests to fail.
 
 # Lack of ways to control the cross-frame scroll chaining in WK1.
diff --git a/LayoutTests/webarchive/loading/resources/quarantined_top.webarchive b/LayoutTests/webarchive/loading/resources/quarantined_top.webarchive
new file mode 100644
index 000000000000..c636935d035c
Binary files /dev/null and b/LayoutTests/webarchive/loading/resources/quarantined_top.webarchive differ
diff --git a/LayoutTests/webarchive/loading/test-loading-archive-with-link-expected.txt b/LayoutTests/webarchive/loading/test-loading-archive-with-link-expected.txt
new file mode 100644
index 000000000000..2779121027bf
--- /dev/null
+++ b/LayoutTests/webarchive/loading/test-loading-archive-with-link-expected.txt
@@ -0,0 +1,7 @@
+main frame - didStartProvisionalLoadForFrame
+main frame - didCommitLoadForFrame
+main frame - didFinishDocumentLoadForFrame
+main frame - didHandleOnloadEventsForFrame
+main frame - didFinishLoadForFrame
+test-loading-archive-with-link.html - didFinishLoading
+Click me This tests that clicking a link to a webarchive is cancelled.
diff --git a/LayoutTests/webarchive/loading/test-loading-archive-with-link.html b/LayoutTests/webarchive/loading/test-loading-archive-with-link.html
new file mode 100644
index 000000000000..08366f9b9e5c
--- /dev/null
+++ b/LayoutTests/webarchive/loading/test-loading-archive-with-link.html
@@ -0,0 +1,23 @@
+<html><!-- webkit-test-runner [ AlwaysAllowLocalWebarchive=true ] -->
+<script>
+    if (window.testRunner) {
+        testRunner.dumpResourceLoadCallbacks();
+        testRunner.dumpAsText();
+        testRunner.waitUntilDone();
+    }
+    if (window.internals)
+        internals.setAlwaysAllowLocalWebarchive(true);
+    function timedout() {
+        if (window.testRunner)
+            testRunner.notifyDone();
+    }
+</script>
+<body>
+    <a href="resources/quarantined_top.webarchive" target="_blank">Click me</a>
+    <script>
+    document.querySelector("a").click()
+    setTimeout(timedout, 100);
+    </script>
+    This tests that clicking a link to a webarchive is cancelled.
+</body>
+</html>
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index c42687a93ccd..8f6f94d7bb5e 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -4922,6 +4922,15 @@ void WebPageProxy::receivedNavigationActionPolicyDecision(WebProcessProxy& proce
         bool isSubstituteWebArchive = navigation->substituteData() && MIMETypeRegistry::isWebArchiveMIMEType(navigation->substituteData()->MIMEType);
         auto webarchiveURL = isSubstituteWebArchive ? URL { navigation->substituteData()->baseURL } : navigation->currentRequest().url();
         if (isSubstituteWebArchive || (webarchiveURL.protocolIsFile() && webarchiveURL.fileSystemPath().endsWith(".webarchive"_s))) {
+#if PLATFORM(MAC)
+            if (!isSubstituteWebArchive && isQuarantinedAndNotUserApproved(webarchiveURL.fileSystemPath())) {
+                WEBPAGEPROXY_RELEASE_LOG(Loading, "receivedNavigationActionPolicyDecision: file cannot be opened because it is from an unidentified developer.");
+                auto error = WebKit::cancelledError(URL { navigation->currentRequest().url() });
+                error.setType(WebCore::ResourceError::Type::Cancellation);
+                m_navigationClient->didFailProvisionalNavigationWithError(*this, FrameInfoData { frameInfo }, navigation, requestURL, error, nullptr);
+                return;
+            }
+#endif
             WEBPAGEPROXY_RELEASE_LOG(Loading, "receivedNavigationActionPolicyDecision: Swapping in non-persistent websiteDataStore for web archive.");
             if (navigation->targetItem() && navigation->targetItem()->dataStoreForWebArchive())
                 websiteDataStore = *navigation->targetItem()->dataStoreForWebArchive();
diff --git a/Tools/Scripts/webkitpy/port/mac.py b/Tools/Scripts/webkitpy/port/mac.py
index e11124a48048..2a9b92d0d729 100644
--- a/Tools/Scripts/webkitpy/port/mac.py
+++ b/Tools/Scripts/webkitpy/port/mac.py
@@ -349,6 +349,8 @@ class MacPort(DarwinPort):
             _log.debug('Warming up the runner ...')
             warmup_driver = self.create_driver(0)
             warmup_driver.run_test(DriverInput('file:///warmup-does-not-exist', 60000., None, should_run_pixel_test=False), stop_when_done=True)
+        # rdar://132098879, ensure webarchive is quarantined.
+        self.host.executive.run_command(['/usr/bin/xattr', self.layout_tests_dir() + '/webarchive/loading/resources/quarantined_top.webarchive']).rstrip()
 
 
 class MacCatalystPort(MacPort):

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-43229
Bugzilla 285927
Component WebKit
Bug Class CrossOrigin
Severity medium
Commit feb7725d8aedfa96…
Advisory Apple Advisory

Terminology

  • com.apple.quarantine / Gatekeeper — A macOS extended attribute set on files from untrusted sources, plus the Gatekeeper mechanism that requires explicit user approval before such content from an unidentified developer is opened.
  • Web Archive (.webarchive) — A single-file page snapshot whose main resource can claim an arbitrary origin and contain script, making its execution equivalent to running attacker code as a spoofed origin.
  • UXSS (universal cross-site scripting) — A bug class where attacker script bypasses the same-origin policy and runs against or reads state of arbitrary origins.
  • Provisional navigation cancellation — Failing an in-progress navigation by delivering a Cancellation-type ResourceError to didFailProvisionalNavigationWithError, which aborts the load before content is committed.

Root Cause Analysis

This is a Gatekeeper/quarantine-bypass logic bug on macOS in the Web Archive loading path that leads to universal cross-site scripting (UXSS). A .webarchive main resource can declare an arbitrary origin and carry script, so opening one runs content with the authority of spoofed origins; macOS normally protects users from such downloaded content via the com.apple.quarantine extended attribute and Gatekeeper’s “unidentified developer” approval prompt. The violated invariant is that a quarantined, not-yet-user-approved local .webarchive must not be silently loaded and executed. This CVE is a follow-up to the web-archive isolation work (CVE-2024-40857): that patch made WebPageProxy::receivedNavigationActionPolicyDecision() swap in a non-persistent data store for substitute or file:// .webarchive Use decisions, but it did not check the file’s quarantine state, so a webarchive delivered to disk and reached via a file:// navigation (for example a link click, as the added test exercises) would load and execute despite being quarantined and unapproved. The fix adds, under PLATFORM(MAC), a guard inside that same branch: for a non-substitute (file://) web archive, if isQuarantinedAndNotUserApproved(webarchiveURL.fileSystemPath()) is true, it logs, builds a cancellation ResourceError, reports didFailProvisionalNavigationWithError, and returns before any data-store swap or load — cancelling the navigation exactly as Gatekeeper intends. The accompanying layout test loads a page that programmatically clicks a link to a quarantined .webarchive and asserts the load is cancelled, and mac.py is changed to set the com.apple.quarantine xattr on the test resource so the quarantine path is actually exercised. Note: isQuarantinedAndNotUserApproved(), cancelledError(), and the exact ResourceError plumbing live outside the shown diff, so their internals are inferred from the call site rather than shown by the patch.

Attack Path

1. Deliver a quarantined web archive

The attacker gets a crafted .webarchive onto the victim’s disk through a channel that sets com.apple.quarantine (download, email attachment, messaging), with a main resource spoofing a sensitive origin and embedding script.

2. Induce a file:// navigation to it

The victim (or attacker-controlled content, as the test’s document.querySelector(‘a’).click() shows) navigates to the local .webarchive via a file:// URL ending in .webarchive.

3. Pre-patch: load proceeds despite quarantine

receivedNavigationActionPolicyDecision reaches the web-archive branch and, without a quarantine check, swaps in the data store and loads the archive, running its script without the Gatekeeper unidentified-developer approval.

4. UXSS execution

The archive’s script executes as its spoofed origin, giving universal cross-site scripting against arbitrary sites.

5. Fix cancels the load

Post-patch, isQuarantinedAndNotUserApproved() causes the navigation to fail with a cancellation error before any load, so an unapproved quarantined archive never executes.

Impact Assessment

This is a same-origin-policy bypass (UXSS) gated on a quarantine/Gatekeeper check, not a memory-safety primitive: the diff contains no allocation or bounds logic and cannot be groomed toward memory corruption. Exploited pre-patch, a quarantined .webarchive that the user was never prompted to approve executes script as spoofed origins, a confidentiality/integrity break in the WebContent process and browsing profile rather than a route to native code execution. Because it requires getting a quarantined archive onto disk and a file:// navigation to it, real-world exploitation depends on delivery and some user interaction, consistent with the medium rating.

Audit Directions

Other content types reachable via file:// that skip quarantine

In receivedNavigationActionPolicyDecision and nearby load paths, check whether the isQuarantinedAndNotUserApproved() check covers all archive/executable-content branches (substitute-data archives, back/forward reloads, non-.webarchive extensions); grep isQuarantinedAndNotUserApproved, protocolIsFile, .webarchive.

Quarantine enforcement across ports/platforms

The guard is PLATFORM(MAC)-only; audit whether iOS/Catalyst or other file-load entry points need equivalent quarantine/approval checks, and whether loadFile / substitute-data paths bypass it entirely.

Consistency with disallowWebArchive gating

Cross-check the UIProcess quarantine cancel against WebCore’s DocumentLoader::disallowWebArchive / allowsWebArchiveForMainFrame and AlwaysAllowLocalWebarchive to ensure no combination of settings re-enables an unapproved local archive; grep disallowWebArchive, alwaysAllowLocalWebarchive, dataStoreForWebArchive.

Changed Functions

Function File Change Note
WebPageProxy::receivedNavigationActionPolicyDecision Source/WebKit/UIProcess/WebPageProxy.cpp modified Adds a PLATFORM(MAC) guard in the file:// web-archive branch: when the archive file isQuarantinedAndNotUserApproved(), it cancels the navigation via didFailProvisionalNavigationWithError and returns before swapping the data store or loading.
test-loading-archive-with-link.html LayoutTests/webarchive/loading/test-loading-archive-with-link.html added New test that clicks a link to resources/quarantined_top.webarchive and expects the load to be cancelled (the archive content never renders).
MacPort.setup_test_run Tools/Scripts/webkitpy/port/mac.py modified Runs xattr on the test’s quarantined_top.webarchive so the quarantine attribute is present when the test runs, exercising the new guard (rdar://132098879).

Files Changed

Source Files

  • Source/WebKit/UIProcess/WebPageProxy.cpp
  • Tools/Scripts/webkitpy/port/mac.py

Test Files

  • LayoutTests/platform/mac-wk1/TestExpectations
  • LayoutTests/webarchive/loading/resources/quarantined_top.webarchive
  • LayoutTests/webarchive/loading/test-loading-archive-with-link-expected.txt
  • LayoutTests/webarchive/loading/test-loading-archive-with-link.html

Regression Test

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

diff --git a/LayoutTests/platform/mac-wk1/TestExpectations b/LayoutTests/platform/mac-wk1/TestExpectations
index 52e14e87eb4e..70e2e5b03b61 100644
--- a/LayoutTests/platform/mac-wk1/TestExpectations
+++ b/LayoutTests/platform/mac-wk1/TestExpectations
@@ -1733,6 +1733,8 @@ webkit.org/b/244308 imported/w3c/web-platform-tests/cookies/attributes/attribute
 # Only flakey in WK1
 webarchive/loading/test-loading-archive-subresource-null-mimetype.html [ Failure Pass ]
 
+webarchive/loading/test-loading-archive-with-link.html [ Skip ]
+
 # WebKit1 frames use native scrollbars causing these tests to fail.
 
 # Lack of ways to control the cross-frame scroll chaining in WK1.
diff --git a/LayoutTests/webarchive/loading/resources/quarantined_top.webarchive b/LayoutTests/webarchive/loading/resources/quarantined_top.webarchive
new file mode 100644
index 000000000000..c636935d035c
Binary files /dev/null and b/LayoutTests/webarchive/loading/resources/quarantined_top.webarchive differ
diff --git a/LayoutTests/webarchive/loading/test-loading-archive-with-link-expected.txt b/LayoutTests/webarchive/loading/test-loading-archive-with-link-expected.txt
new file mode 100644
index 000000000000..2779121027bf
--- /dev/null
+++ b/LayoutTests/webarchive/loading/test-loading-archive-with-link-expected.txt
@@ -0,0 +1,7 @@
+main frame - didStartProvisionalLoadForFrame
+main frame - didCommitLoadForFrame
+main frame - didFinishDocumentLoadForFrame
+main frame - didHandleOnloadEventsForFrame
+main frame - didFinishLoadForFrame
+test-loading-archive-with-link.html - didFinishLoading
+Click me This tests that clicking a link to a webarchive is cancelled.
diff --git a/LayoutTests/webarchive/loading/test-loading-archive-with-link.html b/LayoutTests/webarchive/loading/test-loading-archive-with-link.html
new file mode 100644
index 000000000000..08366f9b9e5c
--- /dev/null
+++ b/LayoutTests/webarchive/loading/test-loading-archive-with-link.html
@@ -0,0 +1,23 @@
+<html><!-- webkit-test-runner [ AlwaysAllowLocalWebarchive=true ] -->
+<script>
+    if (window.testRunner) {
+        testRunner.dumpResourceLoadCallbacks();
+        testRunner.dumpAsText();
+        testRunner.waitUntilDone();
+    }
+    if (window.internals)
+        internals.setAlwaysAllowLocalWebarchive(true);
+    function timedout() {
+        if (window.testRunner)
+            testRunner.notifyDone();
+    }
+</script>
+<body>
+    <a href="resources/quarantined_top.webarchive" target="_blank">Click me</a>
+    <script>
+    document.querySelector("a").click()
+    setTimeout(timedout, 100);
+    </script>
+    This tests that clicking a link to a webarchive is cancelled.
+</body>
+</html>