CVE-2025-43229
Overview
Background
- 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
- 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.
- 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.
- 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.
- UXSS execution The archive’s script executes as its spoofed origin, giving universal cross-site scripting against arbitrary sites.
- 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
WebPageProxy::receivedNavigationActionPolicyDecisionSource/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.htmlLayoutTests/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_runTools/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
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.htmlSource/WebKit/UIProcess/WebPageProxy.cppTools/Scripts/webkitpy/port/mac.py
Audit Directions
- Other content types reachable via file:// that skip quarantineIn 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/platformsThe 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 gatingCross-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.