CVE-2024-44212
Overview
Background
- Web archive (.webarchive)
- A single-file snapshot of a page and its subresources that WebKit can load, with its own baseURL origin.
- Ephemeral / non-persistent data store
- A website data store with no on-disk persistence, swapped in for web archive loads to isolate their cookies and storage.
- URL string vs filesystem path
- url.string() is the full serialized URL (scheme, path, query, fragment, encoding); url.fileSystemPath() is the decoded local path only.
- Navigation policy decision
- The UIProcess step that decides how a navigation proceeds, including which data store backs it.
Root Cause Analysis
WebPageProxy::receivedNavigationActionPolicyDecision decides whether a navigation should be loaded in a swapped-in non-persistent (ephemeral) website data store, which is done for web archives so their content is isolated. The webarchive detection tested webarchiveURL.string().endsWith(".webarchive") — i.e. it matched the suffix against the full URL string, which includes the query and fragment and percent-encoding, not just the file path. A file URL for a genuine .webarchive that carries a query or fragment (e.g. file:///path/x.webarchive?a or ...x.webarchive#a) has a .string() that does NOT end in .webarchive, so the archive was not recognized and the ephemeral-store swap was skipped; the archive then loaded in the normal persistent data store, so cookies associated with the archive’s origin were read from / written to the shared persistent store and could reach another origin.
The fix matches against webarchiveURL.fileSystemPath(), the decoded filesystem path with query/fragment stripped, so the .webarchive classification reflects the real file rather than an attacker-manipulable URL string, and the ephemeral-store isolation is applied to exactly the webarchive loads it is meant for.
The restored invariant is that ‘is this a .webarchive load’ is decided from the file path, not from a URL string that can be altered without changing the file. This runs in the UIProcess navigation policy path, not the sandboxed WebContent.
Attack Path
- Host a web archive Get the victim to open a .webarchive file (local file load or substitute data) that should be isolated in an ephemeral data store.
- Perturb the URL string Reference the archive with a URL whose string does not end in ‘.webarchive’ though the file path does — e.g. append a query or fragment (x.webarchive?a / x.webarchive#a).
- Defeat archive detection endsWith on url.string() fails to match, so receivedNavigationActionPolicyDecision does not swap in the non-persistent store.
- Leak cookies across origins The archive loads in the persistent store, so cookies tied to the archive’s baseURL origin are read/written in the shared store and can be sent to another origin.
Impact Assessment
Files Changed
Source/WebKit/UIProcess/WebPageProxy.cpp
Audit Directions
- Other suffix/extension checks on url.string()grep WebKit/WebCore for
.string().endsWith(/.startsWith(/contains(on URLs used for security decisions; prefer fileSystemPath()/path() where a file is meant. - Data-store / storage-partition selectionAudit other branches choosing persistent vs ephemeral stores or storage partitions based on URL shape rather than resolved origin.
- webarchive handlingReview all isWebArchiveMIMEType / .webarchive handling for consistent path-based classification across load, save, and policy paths.
Patch
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index a36d0ca506c9..fd0ead8b1a10 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -4674,7 +4674,7 @@ void WebPageProxy::receivedNavigationActionPolicyDecision(WebProcessProxy& proce
if (preferences().loadWebArchiveWithEphemeralStorageEnabled() && navigation && policyAction == PolicyAction::Use && navigationAction->navigationType() != NavigationType::Reload && !isPolicyDataStore) {
bool isSubstituteWebArchive = navigation->substituteData() && MIMETypeRegistry::isWebArchiveMIMEType(navigation->substituteData()->MIMEType);
auto webarchiveURL = isSubstituteWebArchive ? URL { navigation->substituteData()->baseURL } : navigation->currentRequest().url();
- if (isSubstituteWebArchive || (webarchiveURL.protocolIsFile() && webarchiveURL.string().endsWith(".webarchive"_s))) {
+ if (isSubstituteWebArchive || (webarchiveURL.protocolIsFile() && webarchiveURL.fileSystemPath().endsWith(".webarchive"_s))) {
WEBPAGEPROXY_RELEASE_LOG(Loading, "receivedNavigationActionPolicyDecision: Swapping in non-persistent websiteDataStore for web archive.");
if (navigation->targetItem() && navigation->targetItem()->dataStoreForWebArchive())
websiteDataStore = *navigation->targetItem()->dataStoreForWebArchive();