CVE-2025-43229
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.cppTools/Scripts/webkitpy/port/mac.py
Test Files
LayoutTests/platform/mac-wk1/TestExpectationsLayoutTests/webarchive/loading/resources/quarantined_top.webarchiveLayoutTests/webarchive/loading/test-loading-archive-with-link-expected.txtLayoutTests/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>