CVE-2024-23280
Overview
Background
- Entries API / DOMFileSystem
- A web API exposing FileSystemEntry objects (from drag-drop/file inputs) that resolve virtual paths against a root directory.
- m_rootPath
- The granted sandbox root for the file system; empty means no legitimate directory was granted.
- Filesystem metadata probing
- Testing existence/type of local paths to infer host/user characteristics (fingerprinting).
Root Cause Analysis
This fixes a local-filesystem information leak (user fingerprinting) in the Entries API’s DOMFileSystem. A File can be introduced via DataTransfer (drag-and-drop) with an attacker-influenced name, and webkitEntries / FileSystemEntry.file()/getEntry() then resolve paths against a DOMFileSystem. DOMFileSystem::getEntry and getFile evaluate a virtual path against m_rootPath and dispatch work to a background queue that stats the resolved full path (fileTypeIgnoringHiddenFiles).
Pre-patch, when m_rootPath was empty — i.e. the file system has no legitimately granted sandbox root (as happens for a File synthesized via DataTransfer rather than a real user-granted directory) — the code still proceeded to resolve and stat paths, letting a page probe the existence/type (metadata) of local filesystem paths it was never granted access to, which can be used to fingerprint the user/host.
The fix adds an early guard in both getEntry and getFile: if (m_rootPath.isEmpty()) return completionCallback(Exception { NotFoundError, “Path does not exist” }); so no filesystem probing occurs without a valid root.
The restored invariant is that DOMFileSystem resolves/stats paths only when it has a granted root, so a synthesized File cannot be used to enumerate local filesystem metadata. The regression test creates a temporary file, adds it to a file input via DataTransfer, and calls entry.file(), expecting the access to be denied (no file returned).
Attack Path
- Synthesize a File via DataTransfer Create a File with a crafted name, add it to a DataTransfer/file input so it exposes a FileSystemEntry with no granted root.
- Call entry.file()/getEntry() Invoke the Entries API, which pre-patch resolved and stat’d paths even though m_rootPath is empty.
- Probe local paths Use the metadata (existence/type) returned for resolved paths to test for local files.
- Fingerprint the user Infer host/user characteristics from which local files exist — an information leak.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
DOMFileSystem::getEntrySource/WebCore/Modules/entriesapi/DOMFileSystem.cpp |
modified | Returns NotFoundError immediately when m_rootPath.isEmpty(), so no path resolution/stat occurs without a granted root. |
DOMFileSystem::getFileSource/WebCore/Modules/entriesapi/DOMFileSystem.cpp |
modified | Same early m_rootPath.isEmpty() guard before dispatching filesystem work. |
Files Changed
LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txtLayoutTests/http/tests/security/file-system-access-via-dataTransfer.htmlSource/WebCore/Modules/entriesapi/DOMFileSystem.cpp
Audit Directions
- Root-less filesystem accessAudit DOMFileSystem and Entries API paths for operations that resolve/stat paths without first verifying a granted m_rootPath.
- DataTransfer-synthesized filesGrep for FileSystemEntry creation from DataTransfer/file inputs and confirm they cannot reach real filesystem metadata.
Patch
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
new file mode 100644
index 000000000000..641ddaebab3e
--- /dev/null
+++ b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer-expected.txt
@@ -0,0 +1,10 @@
+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 successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
new file mode 100644
index 000000000000..caf05e1f85fc
--- /dev/null
+++ b/LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
@@ -0,0 +1,50 @@
+<html>
+<head>
+<script src="/js-test-resources/js-test.js"></script>
+<body>
+
+<script>
+description("Test that accessing local file system metadata is not allowed");
+
+function runTest() {
+ if (!window.internals) {
+ alert("This test depends on Internals");
+ return;
+ }
+
+ window.jsTestIsAsync = true;
+
+ let path = location.pathname.split("/");
+ let targetFileName = internals.createTemporaryFile(`${path[path.length - 1]}`, "");
+
+ let input = document.createElement("input");
+ input.type = "file";
+
+ let file = new File([], targetFileName, {"type":"text/plain"});
+
+ dataTransfer = new DataTransfer();
+ dataTransfer.items.add(file)
+ input.files = dataTransfer.files;
+
+ var functionOnSuccess = function (file)
+ {
+ testFailed("Should not receive file");
+ finishJSTest()
+ }
+
+ var functionOnError = function (value)
+ {
+ testPassed("Should not receive file");
+ finishJSTest()
+ }
+
+ input.webkitEntries.forEach((entry) => {
+ entry.file(functionOnSuccess, functionOnError)
+ });
+}
+
+runTest();
+
+</script>
+</body>
+</html>
diff --git a/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp b/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
index 0c08ddbec609..e86b3dcc356c 100644
--- a/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
+++ b/Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
@@ -303,6 +303,9 @@ void DOMFileSystem::getEntry(ScriptExecutionContext& context, FileSystemDirector
return;
}
+ 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)), resolvedVirtualPath = crossThreadCopy(WTFMove(resolvedVirtualPath)), completionCallback = WTFMove(completionCallback)]() mutable {
auto entryType = fileTypeIgnoringHiddenFiles(fullPath);
callOnMainThread([protectedThis = WTFMove(protectedThis), context = WTFMove(context), resolvedVirtualPath = crossThreadCopy(WTFMove(resolvedVirtualPath)), entryType, completionCallback = WTFMove(completionCallback)]() mutable {
@@ -327,6 +330,8 @@ void DOMFileSystem::getEntry(ScriptExecutionContext& context, FileSystemDirector
void DOMFileSystem::getFile(ScriptExecutionContext& context, FileSystemFileEntry& fileEntry, GetFileCallback&& completionCallback)
{
+ if (m_rootPath.isEmpty())
+ return completionCallback(Exception { ExceptionCode::NotFoundError, "Path does not exist"_s });
auto virtualPath = fileEntry.virtualPath();
auto fullPath = evaluatePath(virtualPath);
m_workQueue->dispatch([fullPath = crossThreadCopy(WTFMove(fullPath)), virtualPath = crossThreadCopy(WTFMove(virtualPath)), context = Ref { context }, completionCallback = WTFMove(completionCallback)]() mutable {