CVE-2025-24143
Overview
Background
- Entries API (webkitEntries)
- A DOM API exposing FileSystemEntry/FileSystemDirectoryEntry objects from a DataTransfer or file input, allowing scripted navigation of dropped files and folders.
- DOMFileSystem m_rootPath
- The base filesystem path that scopes all path resolution for a DOMFileSystem instance to authorized content; an empty value means no legitimate root was established.
- getParent / readEntries
- FileSystemEntry operations that walk to a containing directory or enumerate a directory’s children, both of which reach real-filesystem metadata calls in WebCore.
- Fingerprinting oracle
- A side channel where success-vs-error callbacks (or returned metadata) let a page infer the existence or attributes of local files without directly reading their contents.
- Work queue dispatch
- The pattern of moving blocking filesystem I/O to a background thread and marshalling the result back to the main thread via callOnMainThread; the guard is added before this dispatch.
Root Cause Analysis
The patch touches Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp, the implementation behind the Entries API (webkitEntries / FileSystemEntry) that a page can obtain from a DataTransfer during drag-and-drop or a file input. Each DOMFileSystem is created with an m_rootPath that scopes all path resolution to files the user actually granted. In DOMFileSystem::listDirectory and DOMFileSystem::getParent, the code resolved a virtual path and then dispatched work to a background queue to enumerate directory children (listDirectoryWithMetadata) or validate/stat the parent directory on the real filesystem. The violated invariant is that these filesystem operations must only run when the object has a valid, non-empty root path scoping it to authorized content; when m_rootPath is empty (for example an entry synthesized from a dropped File whose real path was never actually admitted), path evaluation can still resolve to something outside the intended sandbox, letting the background job touch and report metadata about the local filesystem.
The fix adds an explicit guard at the top of both functions: if m_rootPath.isEmpty(), immediately invoke the completion handler/callback with a NotFoundError (‘Path does not exist’) and return, before any work is dispatched to the work queue. This ensures that an un-rooted DOMFileSystem cannot be used to enumerate a directory’s children or walk to a parent directory. The updated LayoutTest confirms the intent: it now also adds a directory File to the DataTransfer and, for each entry, calls entry.file(), entry.getParent(), and (for directories) createReader().readEntries(), all of which must fail with ‘Should not receive file’ errors. Notably file() was already guarded (the pre-existing single PASS), but getParent() and readEntries() were the newly-closed holes, which is exactly what the two new guards in listDirectory and getParent cover.
The result is that previously an attacker could distinguish existence/metadata of local paths (a fingerprinting/info-leak oracle) via these two entry points; the fix removes the oracle by refusing the operation outright when there is no legitimate root.
Attack Path
- Obtain a FileSystemEntry without a real root Get the page to receive entries via webkitEntries from a DataTransfer (drag-and-drop or a crafted file input), producing a DOMFileSystem whose m_rootPath is empty.
- Walk to a parent directory Call entry.getParent(success, error); before the fix this dispatched a background job that resolved and validated a full filesystem path despite the empty root.
- Enumerate directory children For a directory entry, call entry.createReader().readEntries(…) which reached listDirectory and ran listDirectoryWithMetadata against a path outside the intended sandbox.
- Read back the outcome as an oracle Observe whether the success or error callback fires (and any returned metadata) to infer the existence and attributes of local files/directories, building a fingerprint of the user’s system.
- Repeat across paths Probe multiple candidate paths to accumulate fingerprinting signal about installed software, usernames, or directory layout.
Impact Assessment
Files Changed
Source/WebCore/Modules/entriesapi/DOMFileSystem.cppLayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txtLayoutTests/http/tests/security/file-system-access-via-dataTransfer.htmlLayoutTests/platform/mac-wk1/http/tests/security/file-system-access-via-dataTransfer-expected.txt
Audit Directions
- Other DOMFileSystem entry pointsReview every public method of DOMFileSystem.cpp that resolves or evaluates a path (grep for ’evaluatePath’, ‘resolveRelativeVirtualPath’, ’m_workQueue->dispatch’, ’listDirectoryWithMetadata’, ‘validatePathIsExpectedType’) and confirm each has the m_rootPath.isEmpty() guard before dispatching filesystem work.
- Root/precondition consistency across the Entries APICheck FileSystemEntry, FileSystemDirectoryEntry, FileSystemDirectoryReader and DOMFileSystemSync for the same class of missing guard; grep for ‘rootPath’, ‘isEmpty()’ near path evaluation and ensure new operations added later inherit the check.
- Empty/unvalidated base-path assumptions elsewhereSearch WebCore filesystem and File/Blob code for callbacks that return metadata based on a stored base path without validating it is non-empty and in-sandbox; grep for ‘crossThreadCopy’ of a fullPath immediately after ’evaluatePath’ without a preceding emptiness/authorization check.
Patch
diff --git a/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp b/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
index 5ad5504c9958..b95ebefae7ae 100644
--- a/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
+++ b/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
@@ -246,6 +246,9 @@ void DOMFileSystem::listDirectory(ScriptExecutionContext& context, FileSystemDir
return;
}
+ if (m_rootPath.isEmpty())
+ return completionHandler(Exception { ExceptionCode::NotFoundError, "Path does not exist"_s });
+
m_workQueue->dispatch([protectedThis = Ref { *this }, context = Ref { context }, completionHandler = WTFMove(completionHandler), fullPath = crossThreadCopy(WTFMove(fullPath)), directoryVirtualPath = crossThreadCopy(WTFMove(directoryVirtualPath))]() mutable {
auto listedChildren = listDirectoryWithMetadata(fullPath);
callOnMainThread([protectedThis = WTFMove(protectedThis), context = WTFMove(context), completionHandler = WTFMove(completionHandler), listedChildren = crossThreadCopy(WTFMove(listedChildren)), directoryVirtualPath = WTFMove(directoryVirtualPath).isolatedCopy()]() mutable {
@@ -261,6 +264,10 @@ void DOMFileSystem::getParent(ScriptExecutionContext& context, FileSystemEntry&
auto virtualPath = resolveRelativeVirtualPath(entry.virtualPath(), ".."_s);
ASSERT(virtualPath[0] == '/');
auto fullPath = evaluatePath(virtualPath);
+
+ if (m_rootPath.isEmpty())
+ return completionCallback(Exception { ExceptionCode::NotFoundError, "Path does not exist"_s });
+
m_workQueue->dispatch([protectedThis = Ref { *this }, context = Ref { context }, fullPath = crossThreadCopy(WTFMove(fullPath)), virtualPath = crossThreadCopy(WTFMove(virtualPath)), completionCallback = WTFMove(completionCallback)]() mutable {
auto validatedVirtualPath = validatePathIsExpectedType(fullPath, WTFMove(virtualPath), FileSystem::FileType::Directory);
callOnMainThread([protectedThis = WTFMove(protectedThis), context = WTFMove(context), validatedVirtualPath = crossThreadCopy(WTFMove(validatedVirtualPath)), completionCallback = WTFMove(completionCallback)]() mutable {
diff --git a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt
index 641ddaebab3e..46b70858a378 100644
--- a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt
+++ b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt
@@ -3,6 +3,8 @@ Test that accessing local file system metadata is not allowed
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+PASS Should not receive file
+PASS Should not receive file
PASS Should not receive file
PASS successfullyParsed is true
diff --git a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
index caf05e1f85fc..313b0d46c118 100644
--- a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
+++ b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
@@ -15,31 +15,44 @@ function runTest() {
window.jsTestIsAsync = true;
let path = location.pathname.split("/");
- let targetFileName = internals.createTemporaryFile(`${path[path.length - 1]}`, "");
+ let filename = path[path.length - 1];
+ let targetFileName = internals.createTemporaryFile(`${filename}`, "");
+ let targetDirectory = targetFileName.substring(0, targetFileName.length - filename);
let input = document.createElement("input");
input.type = "file";
let file = new File([], targetFileName, {"type":"text/plain"});
+ let directory = new File([], targetDirectory, {"type":"text/plain"});
dataTransfer = new DataTransfer();
dataTransfer.items.add(file)
+ dataTransfer.items.add(directory)
input.files = dataTransfer.files;
+ let resultCount = 0;
+
var functionOnSuccess = function (file)
{
- testFailed("Should not receive file");
- finishJSTest()
+ testFailed("Received file");
+ if (resultCount == 2)
+ finishJSTest()
+ resultCount++;
}
var functionOnError = function (value)
{
testPassed("Should not receive file");
- finishJSTest()
+ if (resultCount == 2)
+ finishJSTest()
+ resultCount++;
}
input.webkitEntries.forEach((entry) => {
entry.file(functionOnSuccess, functionOnError)
+ entry.getParent(functionOnSuccess, functionOnError)
+ if (entry.isDirectory)
+ entry.createReader().readEntries(functionOnSuccess, functionOnError)
});
}
diff --git a/LayoutTests/platform/mac-wk1/http/tests/security/file-system-access-via-dataTransfer-expected.txt b/LayoutTests/platform/mac-wk1/http/tests/security/file-system-access-via-dataTransfer-expected.txt
new file mode 100644
index 000000000000..95663e9c7030
--- /dev/null
+++ b/LayoutTests/platform/mac-wk1/http/tests/security/file-system-access-via-dataTransfer-expected.txt
@@ -0,0 +1,13 @@
+Test that accessing local file system metadata is not allowed
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Should not receive file
+PASS Should not receive file
+PASS Should not receive file
+PASS Should not receive file
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
Regression Test / PoC
diff --git a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt
index 641ddaebab3e..46b70858a378 100644
--- a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt
+++ b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt
@@ -3,6 +3,8 @@ Test that accessing local file system metadata is not allowed
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+PASS Should not receive file
+PASS Should not receive file
PASS Should not receive file
PASS successfullyParsed is true
diff --git a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
index caf05e1f85fc..313b0d46c118 100644
--- a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
+++ b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
@@ -15,31 +15,44 @@ function runTest() {
window.jsTestIsAsync = true;
let path = location.pathname.split("/");
- let targetFileName = internals.createTemporaryFile(`${path[path.length - 1]}`, "");
+ let filename = path[path.length - 1];
+ let targetFileName = internals.createTemporaryFile(`${filename}`, "");
+ let targetDirectory = targetFileName.substring(0, targetFileName.length - filename);
let input = document.createElement("input");
input.type = "file";
let file = new File([], targetFileName, {"type":"text/plain"});
+ let directory = new File([], targetDirectory, {"type":"text/plain"});
dataTransfer = new DataTransfer();
dataTransfer.items.add(file)
+ dataTransfer.items.add(directory)
input.files = dataTransfer.files;
+ let resultCount = 0;
+
var functionOnSuccess = function (file)
{
- testFailed("Should not receive file");
- finishJSTest()
+ testFailed("Received file");
+ if (resultCount == 2)
+ finishJSTest()
+ resultCount++;
}
var functionOnError = function (value)
{
testPassed("Should not receive file");
- finishJSTest()
+ if (resultCount == 2)
+ finishJSTest()
+ resultCount++;
}
input.webkitEntries.forEach((entry) => {
entry.file(functionOnSuccess, functionOnError)
+ entry.getParent(functionOnSuccess, functionOnError)
+ if (entry.isDirectory)
+ entry.createReader().readEntries(functionOnSuccess, functionOnError)
});
}
diff --git a/LayoutTests/platform/mac-wk1/http/tests/security/file-system-access-via-dataTransfer-expected.txt b/LayoutTests/platform/mac-wk1/http/tests/security/file-system-access-via-dataTransfer-expected.txt
new file mode 100644
index 000000000000..95663e9c7030
--- /dev/null
+++ b/LayoutTests/platform/mac-wk1/http/tests/security/file-system-access-via-dataTransfer-expected.txt
@@ -0,0 +1,13 @@
+Test that accessing local file system metadata is not allowed
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS Should not receive file
+PASS Should not receive file
+PASS Should not receive file
+PASS Should not receive file
+PASS successfullyParsed is true
+
+TEST COMPLETE
+