Medium CVSS 6.5 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionA maliciously crafted webpage may be able to fingerprint the user
ComponentWebCore Entriesapi
Bug ClassLogic Error
Tracker266703
Fix commit17c0ad98bb1c (WebKit/WebKit) +65/-0
CWECWE-74
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
CISA KEVNot listed
Creditedan anonymous researcher
Disclosed2024-03-05

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).

Key insight
DOMFileSystem resolved and stat’d paths even when it had no granted root (m_rootPath empty), so a File synthesized via DataTransfer could probe local filesystem metadata; guarding on a non-empty root closes the leak.

Attack Path

  1. 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.
  2. Call entry.file()/getEntry() Invoke the Entries API, which pre-patch resolved and stat’d paths even though m_rootPath is empty.
  3. Probe local paths Use the metadata (existence/type) returned for resolved paths to test for local files.
  4. Fingerprint the user Infer host/user characteristics from which local files exist — an information leak.

Impact Assessment

A local-filesystem information disclosure / fingerprinting issue in the WebContent process with no memory corruption: a DataTransfer-synthesized File could be used to probe local path metadata without a granted root. Impact is privacy (host/user fingerprinting), reachable from ordinary web content with drag-drop.

Changed Functions

FunctionChangeNotes
DOMFileSystem::getEntry
Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp
modified Returns NotFoundError immediately when m_rootPath.isEmpty(), so no path resolution/stat occurs without a granted root.
DOMFileSystem::getFile
Source/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.txt
  • LayoutTests/http/tests/security/file-system-access-via-dataTransfer.html
  • Source/WebCore/Modules/entriesapi/DOMFileSystem.cpp

Audit Directions

  • Root-less filesystem access
    Audit DOMFileSystem and Entries API paths for operations that resolve/stat paths without first verifying a granted m_rootPath.
  • DataTransfer-synthesized files
    Grep for FileSystemEntry creation from DataTransfer/file inputs and confirm they cannot reach real filesystem metadata.
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 {
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker.